问题描述
internal struct Point
{
private readonly Int32 m_x,m_y;
public Point(Int32 x,Int32 y) {
m_x = x;
m_y = y;
}
//Override ToString method inherited from System.ValueType
public override string ToString() {
return String.Format("({0},{1})",m_x.ToString(),m_y.ToString());
}
}
class Program
{
static void Main(string[] args) {
Point p1 = new Point(10,10);
p1.ToString();
}
}
作者说:
在对 ToString 的调用中,p1 不必装箱。起初,您会认为 p1 必须装箱,因为 ToString
是从基类型 System.ValueType 继承的虚方法。通常,要调用虚拟方法,CLR 需要确定对象的类型以定位该类型的方法表。因为 p1 是未装箱的值类型,所以没有类型对象指针。但是,即时 (JIT) 编译器会看到 Point 覆盖了 ToString 方法,并且它会发出直接(非虚拟)调用 ToString 的代码,而无需进行任何装箱。编译器知道多态在这里不能发挥作用,因为 Point 是一种值类型,并且没有任何类型可以从它派生以提供此虚方法的另一种实现。
我有点明白它的意思,因为 Point
从 ToString
覆盖 System.ValueType
,CLR 不需要检查类型对象来定位类型的方法表,编译器可以发出直接调用 ToString 的 IL 代码。很公平。
但假设 p1
也将 GetHashCode
从 System.ValueType
中调用为:
class Program
{
static void Main(string[] args) {
Point p1 = new Point(10,10);
p1.ToString();
p1.GetHashCode();
}
}
由于 Point
结构体不会从 GetHashCode()
覆盖 System.ValueType
,所以编译器这次不能直接发出 IL 代码,CLR 需要定位类型的方法表来查找 {{1 }} 方法,但是正如作者所说的p1是未装箱的值类型,没有类型对象指针,那么CLR如何在堆中的GetHashCode
结构体的类型对象中查找GetHashCode
方法?>
解决方法
如果我们查看生成的 MSIL,我们会看到以下内容:
IL_0000: ldloca.s 00 // p1
IL_0002: ldc.i4.s 0A
IL_0004: ldc.i4.s 0A
IL_0006: call System.Drawing.Point..ctor
IL_000B: ldloca.s 00 // p1
IL_000D: constrained. System.Drawing.Point
IL_0013: callvirt System.Object.ToString
IL_0018: pop
IL_0019: ldloca.s 00 // p1
IL_001B: constrained. System.Drawing.Point
IL_0021: callvirt System.Object.GetHashCode
IL_0026: pop
让我们在 constrained.
上查找 ECMA-335 Part III.2.1:
constrained. 前缀只允许在 callvirt 指令中使用。 ptr 的类型必须是指向 thisType 的托管指针 (&)。受约束的前缀旨在允许以统一的方式生成 callvirt 指令,而与 thisType 是值类型还是引用类型无关。
如果 thisType 是一个值类型并且 thisType 实现了 method 那么ptr
未经修改地作为“this”指针传递给由 thisType
如果 thisType 是一个值类型并且 thisType 没有实现 method 那么ptr
被取消引用、装箱并作为“this”指针传递给 method
最后一种情况只能在方法是在 System.Object
、System.ValueType
或 System.Enum
上定义并且未被 thisType 覆盖时发生。在最后一种情况下,装箱会生成原始对象的副本,但是由于 System.Object
、System.ValueType
和 System.Enum
上的所有方法都不会修改对象的状态,无法检测到这一事实。
所以,是的,这确实会导致装箱,但仅当没有覆盖时,因为 System.Object
方法需要一个类,而不是值类型。但是如果它被覆盖,那么该方法的 this
指针必须是一个托管指针,与任何其他值类型方法相同。
相关答案:
How does the heap and stack work for instances and members of struct in C#?
How boxing a value type work internally in C#?
Is everything in .NET an object?
先为我的英文不好和我肯定会写的错误或不确定性道歉,这些东西离我很远,而且我在 IL/CLR/CTS 方面没有那么先进......还有什么时候我会说说 808x,它是我在 6809 之外的来源,它是为了简化关于 RISC 和 CISC 历史的事情。我尽力为这幅画画一幅肖像,让我们可以创作音乐,开辟研究道路。
这些关于代码和数据、堆栈和堆、类和结构等的问题,是一个非常有趣和基础的问题,但复杂而艰巨,广泛而广泛的主题:它是现代计算技术的一大根源基于 transistors 和 silicon integrated circuit,适用于我们基于 microprocessors 的计算机、服务器、智能手机......以及越来越普遍的任何具有 electronic components 的设备。
它们是关于 CPU 底层如何工作的,无论是高层构建于低层,OOP 发明于非 OOP,结构化创建于非结构化之上,功能模仿于我们使用的过程性技术。
我们还没有支持 .NET 的微处理器,此外还有以下信息:
What and where are the stack and heap?
Memory allocation: Stack vs Heap?
How does the heap and stack work for instances and members of struct in C#?
https://stackoverflow.com/questions/65929369/does-structs-have-type-objects-created-in-heap
Why methods return just one kind of parameter in normal conditions?
How would the memory look like for this object?
从根本上说,类和结构体的方法代码不在堆或堆栈中定位或分配。基本上,除了 2GB 的 DATA SEGMENT(我希望我不不要说错误,那是旧的...)当我们点击 .exe 文件时,它们会加载一些 DLL。
换句话说:方法的实现代码是在进程启动时从二进制文件EXE和DLLs中加载的,并存储在CODE SEGMENT中,作为所有数据,静态(文字)和动态(实例)在 DATA SEGMENT 中,甚至由 JIT 翻译,我想,或者类似的东西,如果自 x32 和保护模式以来事情没有改变。非虚拟方法的表以及 virtual 方法的表不存储在每个对象实例的数据段中。我不记得细节了,但这些表格是用于代码的。
还有,继承一个类的类继承了概念中的成员数据和行为。这是一个架构,一个计划,一个平局。当我们创建一个对象时,您可以在一个地方获得所有数据。对于层次结构中的每个类,我们没有为每个对象设置多个对象:它只是我们头脑中和源代码中的一个概念设计。
Where in memory is vtable stored?
此外,对象的每个实例的数据都是其定义及其祖先的投影,在一个地方,一个完整的实例。引用是指向数据段中相关其他空间的“指针”。想象一个对象是火车上的一辆货车(内存):文字和引用是椅子,而引用(类和结构、类的特殊情况或 contrary)指向另一个货车。
堆栈是我写的here并解释了here:一个大房间(堆)中的橱柜(堆栈),并且这个堆栈位置不是使用标准内存访问使用慢{{ 3}} 但是 CPU 堆栈寄存器很快,所以速度更快,但空间有限。
.NET IL 代码被翻译成依赖机器的代码,因此在我们的技术上,例如 Intel-like 或 ARM 我想,例如,所有 CPU 在某种方式上都是相同的(这是硅技术),它是相同的正如我们在 C 或 x86 ASM 中学到的一样……DotNet 是一个虚拟机。执行 IL 代码的 CLR 将其转换为类似 INTEL 的代码,简而言之。就这样。 MOVs 是物理 CPU 寄存器,即使我们想要别的东西,它们也不是别的东西。而 CPU Stack 就是 CPU 粘性。等等。
如果有一天我们有新一代计算机(在大约 8086/8088 之后的当前芯片之后),事情可能会发生变化,.NET 将生成不同的代码,例如量子计算或 DNA 计算。因此,任何有关 .NET 和 CLR 的问题实际上最终都会得到标准和经典的808x
答案,因为 .NET 不会改变 CPU 寄存器的工作方式、总线大小或其他所有内容。
我看到一些帮助者在某些情况下对 .NET 规范的详细信息非常了解这些事情关于某些主题的某些问题,回答“不能保证”,因为文档打开了大门对新一代来说什么都不说,能够在新一代计算机上生成不同的机器代码......
CPU registers 只是将虚拟代码转换为目标架构的真实机器代码。 CLR 不是真正的机器,它是一个 CLR。这个虚拟机不能像在 CPU 上那样在现实世界中存在和工作。没有这样一台真正的 .NET 机器。它尚不存在,我希望有一天它会存在,这会很棒(据我所知,创建 .NET CPU 是微软多年来的计划之一)。
换句话说:尚不存在 .NET CPU。所以,所有 .NET 技术都被翻译成 x86/x32/i64 技术……仅此而已。因此,关于堆栈、堆、方法、类、结构的所有问题都无法回答这种想象中的 .NET-ready CPU 是一种不同于实际芯片的新技术。
我们的实际机器只能执行 virtual machine,因此汇编代码可以在我们的 CPU 上运行。没有其他的。其余的都是人类的概念。所有语言和所有虚拟机都需要翻译成适用于 CPU 架构的机器代码。我们所有的高级语言和任何虚拟架构,如 Java 和 .NET 或其他,甚至是 80 年代的解释型 BASIC,在现实中都不存在。永远不会为我们的硅一代。我们的 CPU 不存在 IL 代码:它只是一个虚拟的 ASM。
因此,.NET“不存在”以及 C 或我们源代码以外的任何语言。甚至 machine code 也最终被翻译成机器代码供 CPU 执行,这是一个非常基本、简单、机械、自动且不太进化的东西,即使是现代的。我们可以发明的任何语言的所有源代码和中间代码指令都被翻译成我们当前的 Intel、AMD、ARM 机器可以理解的机器代码......甚至还有保护模式、x64 模式、多核、超线程等等,1970年和2020年的CPU基本上是一回事,就像老款大众甲壳虫和上一代保时捷的区别。
如果不去想象不存在的事物,就不可能通过认为 .NET 可以从机器的不同角度存在并且 CPU 可以与当前架构不同的运行方式来回答这样的问题。通过代码段和数据,以及堆栈和堆,以及所有其他方面都不同于进程。 DotNet 在 CPU 上的运行方式与使用 CPU 本身的运行方式不同。不可能。即使是量子模拟平台,也最终会在当前硅 CPU 的机器代码中进行翻译。
CLR 使用真机架构。所以,CPU 及其寄存器以及 RAM 内存,以及我们所知道的一切。 CLR 将 IL 代码转换为 CPU 代码。就是这样:它一直在玩杂耍。
我们可以使用 Visual Studio 窗口 Debug > Windows > Machine code 来查看这个机器码,以检查在我们的 CPU 上实时执行的真实代码,是的,那就是 x86/x32/i64 ASM。 .. 不是 MSIL 我们可以看到例如使用 ILSpy。
除了机器码之外的所有东西都不存在,除了在我们的头脑中和我们的工作文件中,比如 sourec 代码和字节码,而且从 CPU 的角度来看,它“根本不真实”。
>要了解 .NET 的内部工作原理,您可以阅读以下书籍和在线资源:
还有:
The Concept of Stack and Its Usage in Microprocessors
Introduction of Stack based CPU Organization
而且,一切都在这里,说话的方式,还有其他例子:
What is the role of stack in a microprocessor?
Protected mode software architecture
为了更好地理解并提高您的计算技能,您可能会感兴趣地研究什么是 Operating System concepts 以及 assembly language 的工作原理。您可以从 CPU 和 IL 开始,但从过去的 modern Intel 开始可能更简单、更具形成性和互补性。
祝你阅读愉快!
也许您可以在 8086 to i386/i486 上提出有关这些主题的高级问题: