为什么:
short a=0;
Console.Write(Marshal.SizeOf(a));
显示2
但如果我看到IL代码,我会看到:
/*1*/ IL_0000: ldc.i4.0
/*2*/ IL_0001: stloc.0
/*3*/ IL_0002: ldloc.0
/*4*/ IL_0003: box System.Int16
/*5*/ IL_0008: call System.Runtime.InteropServices.Marshal.SizeOf
/*6*/ IL_000D: call System.Console.Write
第1行的LDC表示:
将0推入堆栈 int32 。
因此必须占用4
个字节。
但sizeOf
显示2
字节...
我在这里缺少什么?短片实际占用了多少字节?
我听说有一个填充到4个字节的情况,因此处理起来会更快。是这样的吗?
(请忽略syncRoot和GC根标志字节i' m只询问2对4)
答案 0 :(得分:7)
CLI规范非常明确地允许在堆栈上的数据类型。短16位整数不是其中之一,因此这些整数类型在加载到堆栈时会转换为32位整数(4个字节)。
分区III.1.1包含所有细节:
1.1数据类型
虽然CTS定义了富类型系统,但CLS指定了可用于语言的子集 在互操作性方面,CLI本身处理的是一组更简单的类型。这些类型包括用户定义的值 类型和内置类型的子集。该子集统称为“基本CLI类型”,包含 以下类型:
- 完整数字类型的子集(
int32
,int64
,native int
和F
)。- 对象引用(
O
),不区分引用的对象类型。- 指针类型(
native unsigned int
和&
),与指向的类型无区别。请注意,可以为对象引用和指针类型指定值
null
。这在整个CLI中定义为零(所有位为零的位模式)。1.1.1数字数据类型
CLI仅对数字类型
int32
(4字节有符号整数)int64
(8字节)进行操作 有符号整数),native int
(原生大小整数)和F
(原生大小的浮点数) 号)。但是,CIL指令集允许实现其他数据类型:短整数:评估堆栈仅保存4或8字节整数,但其他位置 (参数,局部变量,静态,数组元素,字段)可以保存1或2字节整数。为了 堆栈操作的目的是bool和char类型 分别作为无符号1字节和2字节整数处理。从这些位置加载到 stack通过以下方式将它们转换为4字节值:
- 对unsigned int8,unsigned int16,bool和char;
类型进行零扩展- 对类型int8和int16进行符号扩展;
- 零扩展,用于无符号间接和元素加载(
ldind.u*
,ldelem.u*
等);;和- sign-extends用于签名的间接和元素加载(
ldind.i*
,ldelem.i*
等)。存储整数,布尔值和字符(
stloc
,stfld
,stind.i1
,stelem.i2
等)会截断。使用conv.ovf.*
指令检测此截断何时导致值无法正确表示原始值。[注意:短(即1字节和2字节)整数在所有体系结构上作为4字节数加载,并且这些4字节数始终跟踪不同于8字节数。这有助于代码的可移植性,确保默认的算术行为(即,当没有执行
conv
或conv.ovf
指令时)将在所有实现上具有相同的结果。]转换产生短整数值的指令实际上会在堆栈上留下
int32
(32位)值,但保证只有低位有意义(即,更高有效位全部为零)已签名转化的未签名转化或签名扩展名。要正确模拟完整的短整数运算集,需要在div
,rem
,shr
之前转换为短整数,比较 和条件分支指令。
......等等。
在推测性地说,这个决定可能是为了简化建筑或者为了速度(或两者兼而有之)。与使用16位整数相比,现代32位和64位处理器可以更有效地使用32位整数,并且因为所有可以用2个字节表示的整数也可以用4个字节表示,这种行为是合理的
使用2字节整数而不是4字节整数才真正有意义的是,如果你更关心内存使用而不是执行速度/效率。在这种情况下,你需要拥有一大堆这些值,可能包含在一个结构中。那就是你关心Marshal.SizeOf
的结果。
答案 1 :(得分:5)
通过查看available LDC instructions可以很容易地说出发生了什么。请注意可用的有限操作数类型集,有无版本可用于加载类型为short的常量。只是int,long,float和double。这些限制在其他地方可见,例如Opcodes.Add指令同样受限制,不支持添加其中一个较小类型的变量。
IL指令集是以这种方式有意设计的,它反映了简单的32位处理器的功能。想到的处理器类型是RISC类型,它们在九十年代就有它们的干草日。许多32位cpu寄存器只能操作32位整数和IEEE-754浮点类型。 Intel x86内核不是一个很好的例子,虽然非常常用,但它是一种CISC设计,实际上支持在8位和16位操作数上加载和算术运算。但这更像是一次历史性事故,它使程序的机械翻译变得简单,从8位8080和16位8086处理器开始。但是这种能力并不是免费的,操纵16位值实际上需要额外的cpu周期。
使IL与32位处理器功能完美匹配显然使得实现抖动的人的工作变得更加简单。存储位置仍然可以更小,但只需要支持加载,存储和转换。只有在需要的时候,您才能获得' a'变量是一个局部变量,无论如何都在堆栈帧或cpu寄存器上占用32位。只有内存存储需要被截断为正确的大小。
在代码段中没有歧义。变量值需要加框,因为Marshal.SizeOf()接受类型 object 的参数。盒装值通过类型句柄标识值的类型,它将指向System.Int16。 Marshal.SizeOf()具有内置的知识,知道它需要2个字节。
这些限制确实反映在C#语言上并导致不一致。这种编译错误永远困扰并惹恼C#程序员:
byte b1 = 127;
b1 += 1; // no error
b1 = b1 + 1; // error CS0266
这是IL限制的结果,没有采用字节操作数的add运算符。在这种情况下,需要将它们转换为下一个更大的兼容类型 int 。因此它适用于32位RISC处理器。现在出现问题,需要将32位 int 结果重新打造成一个只能存储8位的变量。 C#语言在第一个任务中应用了锤子本身,但在第二个任务中不合逻辑地需要一个投掷锤。
答案 2 :(得分:1)
C#语言规范定义了程序的行为方式。它没有说明如何实现这一点,只要行为是正确的。如果您询问short
的大小,则始终会2
。
实际上,C#编译为CIL,其中小于32位的整数类型在堆栈 1 上表示为32位整数。
然后JITer再次将其重新映射到适合目标硬件的任何内容,通常是堆栈上的一块内存或寄存器。
只要这些转变都没有改变可观察的行为,它们就是合法的。
实际上,局部变量的大小在很大程度上是无关紧要的,重要的是数组的大小。一百万short
s的数组通常占用2 MB。
1 这是IL操作的虚拟堆栈,与机器代码操作的堆栈不同。
答案 3 :(得分:1)
CLR本身只能在堆栈上使用32位和64位整数。答案在于这条指令:
box System.Int16
这意味着值类型被装箱为Int16。 C#编译器自动发出这个装箱来调用Marshal.SizeOf(object),后者又在盒装值上调用GetType(),返回typeof(System.Int16)。