与其他结构相比,为什么 DateTime 的 GetHashCode() 方法编译方式不同?

Why is GetHashCode() method compiled differently for DateTime compared to other structs?

考虑以下 C# 中的方法:

public static int HashCodeFunction(Decimal value)
{
    return value.GetHashCode();
}
public static int HashCodeFunction(Int64 value)
{
    return value.GetHashCode();
}
public static int HashCodeFunction(DateTime value)
{
    return value.GetHashCode();
}

我们来看看编译器生成的指令:

对于Decimal方法:

ldarga.s Parameter:System.Decimal value
call Method:System.Decimal.GetHashCode()
ret

对于Int64方法:

ldarga.s Parameter:System.Int64 value
call Method:System.Int64.GetHashCode()
ret

对于DateTime方法:

ldarga.s Parameter:System.DateTime value
constrained Type:System.DateTime
callvirt Method:System.Object.GetHashCode()
ret

为什么 DateTime.GetHashCode() 方法被视为 Object.GetHashCode() 的虚拟调用,考虑到 DateTime 结构有一个重写的 GetHashCode() 方法?

此外,我可以使用以下代码创建一个直接调用 System.DateTime.GetHashCode() 方法而无需虚拟调用的方法:

DynamicMethod myDynamicMethod = new DynamicMethod("myHashCodeMethod", typeof(int), new[] { typeof(DateTime) });

ILGenerator gen = myDynamicMethod.GetILGenerator();
LocalBuilder local = gen.DeclareLocal(typeof(DateTime));
gen.Emit(OpCodes.Ldarga_S, local);
gen.Emit(OpCodes.Call, typeof(DateTime).GetMethod("GetHashCode"));
gen.Emit(OpCodes.Ret);

然后创建一个委托来测试它:

Func<DateTime, int> myNewHashCodeFunction = (Func<DateTime,int>)myDynamicMethod.CreateDelegate(typeof(Func<DateTime, int>));

DateTime dt = DateTime.Now;

int myHashCode = myNewHashCodeFunction(dt);
int theirHashCode = dt.GetHashCode();
// These values are the same.

只是好奇为什么 Int64Decimal 默认以这种方式实现该方法,而不是 DateTime

我在我的机器上测试了你的代码,所有三种方法都发出 call 而不是 callvirt,所以我想这可能是特定于编译器的。

我的猜测是,早期版本的 Csc 仅针对 simple type 虚方法发出 call,因此实际上是这些简单类型变得特殊,而不是 DateTime。后来,他们决定为值类型方法调用发出 callvirt 毫无价值,因为它们永远不会被覆盖。因此,所有值类型方法调用都使用 call 发出,而引用类型虚方法调用使用 callvirt.

PS。我的机器上有 Visual Studio 2015 和 .NET Framework 4.6.1。我用 .NET 2.0 到 4.6.1 进行了测试,它们都生成相同的 IL 代码(DateTime.GetHashCode 没有 callvirt)。

谈到 Roslyn,您所描述的是一种旧行为(Roslyn 1.1.0 及更早版本)。新行为(版本 1.2.0 和更新版本)也将 call 用于 DateTime

更改于 pull request String concat with char and similar primitives should call overriden ToString directly (#7080)

constrained.callvirtcall 优化的问题在于,这意味着删除覆盖会成为二进制重大更改,因此无法普遍应用优化。但它可以应用于编译器可以确保覆盖不会被删除的类型。

旧的行为是对 "intrinsic types"(在 C# 中具有关键字的那些)和一些特殊的很少使用的类型使用此优化。新行为是对所有 "special types" 使用优化,其中包括内部类型以及 DateTime.