6

为什么 :

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字节...

我在这里想念什么?short 实际上在 mem 中占用多少字节?

我听说过填充到 4 个字节的情况,因此处理起来会更快。这里也是这样吗?

(请忽略我只是询问 2 vs 4 的 syncRoot 和 GC 根标志字节)

4

4 回答 4

7

CLI 规范对允许在堆栈上的数据类型非常明确。短的 16 位整数不是其中之一,因此此类整数在加载到堆栈时会转换为 32 位整数(4 个字节)。

第 III.1.1 部分包含所有详细信息:

1.1 数据类型

虽然 CTS 定义了丰富的类型系统,而 CLS 指定了一个可用于语言互操作性的子集,但 CLI 本身处理的类型要简单得多。这些类型包括用户定义的值类型和内置类型的子集。该子集统称为“基本 CLI 类型”,包含以下类型:

  • 完整数字类型(int32int64native intF)的子集。
  • 对象引用 ( 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 字节整数。从这些位置加载到堆栈中,通过以下方式将它们转换为 4 字节值:

    • 对 unsigned int8、unsigned int16、bool 和 char 类型进行零扩展;
    • int8 和 int16 类型的符号扩展;
    • 零扩展无符号间接和元素负载(ldind.u*,,ldelem.u*等);;和
    • 符号扩展用于有符号的间接载荷和元素载荷(ldind.i*,ldelem.i*等)

存储为整数、布尔值和字符(stlocstfldstind.i1stelem.i2等)会截断。使用conv.ovf.*说明来检测此截断何时导致无法正确表示原始值的值。

[注意:短(即,1 字节和 2 字节)整数在所有架构上都作为 4 字节数字加载,并且这些 4 字节数字始终与 8 字节数字不同地被跟踪。这通过确保默认算术行为(即,当不执行convconv.ovf不执行指令时)将在所有实现上具有相同的结果来帮助代码的可移植性。]

产生短整数值的转换指令实际上int32在堆栈上留下一个(32 位)值,但保证只有低位有意义(即,对于无符号转换,更高有效位全为零或签名的转换)。为了正确模拟整套短整数运算,需要在divremshr、 比较和条件分支指令之前转换为短整数。

…等等。

推测性地说,这个决定可能是为了架构简单或速度(或可能两者兼而有之)。现代 32 位和 64 位处理器使用 32 位整数可以比使用 16 位整数更有效地工作,并且由于所有可以用 2 个字节表示的整数也可以用 4 个字节表示,因此这种行为是合理的.

唯一真正有意义的是使用 2 字节整数而不是 4 字节整数是,如果您更关心内存使用而不是执行速度/效率。在这种情况下,您需要拥有一大堆这些值,可能会打包到一个结构中。那是你关心的结果的时候Marshal.SizeOf

于 2013-07-07T11:39:55.240 回答
5

通过查看可用的 LDC 指令很容易知道发生了什么。请注意,可用的操作数类型有限,没有可用的版本可以加载 short 类型的常量。只是 int、long、float 和 double。这些限制在其他地方可见,例如 Opcodes.Add 指令同样受到限制,不支持添加较小类型之一的变量。

IL 指令集是有意设计的,它反映了一个简单的 32 位处理器的能力。需要考虑的处理器类型是 RISC 类型,它们在 19 世纪时曾是全盛期。许多 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 限制的结果,没有使用字节操作数的加法运算符。它们需要转换为下一个更大的兼容类型,在这种情况下为int 。所以它适用于 32 位 RISC 处理器。现在有一个问题,需要将 32 位的int结果锤回到一个只能存储 8 位的变量中。C# 语言在第一个作业中应用了锤子本身,但在第二个作业中不合逻辑地要求使用铸锤。

于 2013-07-07T12:41:28.237 回答
1

C# 语言规范定义了程序的行为方式。只要行为正确,它并没有说明如何实现这一点。如果你问short你总是得到的大小2

实际上,C# 编译为 CIL,其中小于 32 位的整数类型在堆栈1上表示为 32 位整数。

然后 JITer 将其重新映射到适合目标硬件的任何位置,通常是堆栈或寄存器上的一块内存。

只要这些转换都不改变可观察到的行为,它们就是合法的。

实际上,局部变量的大小在很大程度上是无关紧要的,重要的是数组的大小。一百万short的数组通常会占用 2MB。


1这是 IL 操作的虚拟堆栈,与机器代码操作的堆栈不同。

于 2013-07-07T11:45:39.213 回答
1

CLR 在本机上只能处理堆栈上的 32 位和 64 位整数。答案就在这条指令中:

box System.Int16

这意味着值类型被装箱为 Int16。C# 编译器自动发出此装箱以调用 Marshal.SizeOf(object),后者又在装箱值上调用 GetType(),返回 typeof(System.Int16)。

于 2013-07-07T11:47:00.733 回答