sizeof(T)和Unsafe.SizeOf<T>()有什么区别?

38
首先,在实际问题之前,有一个小免责声明:
我知道有很多关于 `sizeof` 操作符和 `Marshal.SizeOf` 方法之间的区别的封闭/重复问题,而且我确实理解两者之间的区别。这里我谈论的是新的 `Unsafe` 类中的 `SizeOf` 方法。
因此,我不确定我理解这两个操作之间的实际区别,以及在特定情况下在结构体/类上使用该方法是否存在特定差异。
`sizeof` 操作符接受一个类型名称并返回分配时应占用的托管字节数(例如,`Int32` 将返回 4)。
另一方面,`Unsafe.SizeOf` 方法像 `Unsafe` 类中的所有其他方法一样在 IL 中实现,并且查看代码后,以下是它所做的事情:
.method public hidebysig static int32 SizeOf<T>() cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 1
    sizeof !!T
    ret
}

现在,如果我没错的话,代码只是调用sizeof !!T这与sizeof(T)相同(使用类型名T调用sizeof运算符),那么它们两个难道不完全等效吗?
另外,我看到该方法还在第一行分配了一个无用的对象(NonVersionableAttribute),那么这是否也会导致少量内存被堆分配?
我的问题是:

可以安全地说这两种方法是完全等价的,因此最好使用经典的sizeof运算符,因为它还避免了在SizeOf<T>方法中分配该属性吗?在这一点上,SizeOf<T>方法是添加到Unsafe类中仅仅为了方便吗?


1
.custom语句只是告诉您该方法存在自定义属性。由于自定义属性只是元数据,因此它们不参与常规方法调用。 - thehennyy
只是澄清一下,这是我不知道的BCL类,还是一些第三方扩展?你说得对,它什么也没做,也许想法是为将来的跨平台魔法提供一个“钩子”? - vgru
2
@Groo 我相信这是在.NET Core 2.0中添加到CoreFX的新类(不确定,但无论如何它在.NET Core 2.0中都存在)。 请参见此处:https://learn.microsoft.com/it-it/dotnet/api/system.runtime.compilerservices.unsafe?view=netcore-2.0 - Sergio0694
1
这是实现System.Memory所必需的一个hack。核心CLR跟踪问题在此处(https://github.com/dotnet/coreclr/pull/15497)。这是一个纯实现细节,它返回的值取决于运行时使用的Jitter。C#编译器仍然会拒绝任何尝试获取非可插入类型大小的尝试。 - Hans Passant
1个回答

30
虽然这种方法确实只使用了sizeof IL指令,但与常规的sizeof运算符有所不同,因为该运算符不能应用于任意类型:

用于获取非托管类型的字节大小。非托管类型包括以下表格中列出的内置类型,以及以下类型:

枚举类型

指针类型

不包含任何引用类型字段或属性的用户定义结构体

如果您尝试编写Unsafe.SizeOf的类比版本,它将无法正常工作:
public static int SizeOf<T>()
{
    // nope, will not compile
    return sizeof(T);
}

所以 Unsafe.SizeOf 解除了 sizeof 运算符的限制,允许您使用任意类型(包括引用类型)的 IL sizeof 指令(它将返回引用的大小)。

至于您在 IL 中看到的属性结构 - 这并不意味着每次调用都会实例化属性 - 这只是将属性与各种成员(在这种情况下为方法)关联的 IL 语法。

示例:

public struct Test {
    public int Int1;
}

static void Main() {
    // works
    var s1 = Unsafe.SizeOf<Test>();
    // doesn't work, need to mark method with "unsafe"
    var s2 = sizeof(Test);            
}

另一个例子:
public struct Test {
    public int Int1;
    public string String1;
}


static unsafe void Main() {
    // works, return 16 in 64bit process - 4 for int, 4 for padding, because
    // alignment of the type is the size of its largest element, which is 8
    // and 8 for string
    var s1 = Unsafe.SizeOf<Test>();
    // doesn't work even with unsafe, 
    // cannot take size of variable of managed type "Test"
    // because Test contains field of reference type (string)
    var s2 = sizeof(Test);                        
} 

太好了,谢谢!不过有点奇怪,为什么他们选择不更新C#编译器让sizeof操作符能够在没有额外IL方法的情况下执行相同的操作。另外,等一下,作为一个Int32结构,int不应该始终是4字节长吗,即使在64位进程中也是如此吗?Test的大小不应该是12字节吗? - Sergio0694
1
@Sergio0694 当然可以,int类型占用4个字节,另外4个字节是填充位,已经修复了。至于为什么不改变sizeof运算符 - 我当然不知道确切的原因,但我猜测这需要更改编译器,而Unsafe.SizeOf则不需要任何更改。此外,一开始就有理由使sizeof工作方式如此,所以我想当你使用Unsafe.SizeOf时,你需要真正知道自己在做什么(毕竟它是不安全的)。 - Evk
有道理,感谢额外的解释!完全忘记了字段偏移对齐,自从我上次编写C代码以来已经过了一段时间,当使用托管语言时,你往往会忘记这些微小的细节,哈哈哈。 - Sergio0694
@Sergio0694 这就是为什么它被称为“不安全”。最好有一种安全的语言和可能不安全的 API,而不是单独使用一种不安全的语言。 - IS4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接