与其他结构相比,为什么 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.
只是好奇为什么 Int64
和 Decimal
默认以这种方式实现该方法,而不是 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
。
constrained.callvirt
→ call
优化的问题在于,这意味着删除覆盖会成为二进制重大更改,因此无法普遍应用优化。但它可以应用于编译器可以确保覆盖不会被删除的类型。
旧的行为是对 "intrinsic types"(在 C# 中具有关键字的那些)和一些特殊的很少使用的类型使用此优化。新行为是对所有 "special types" 使用优化,其中包括内部类型以及 DateTime
.
考虑以下 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.
只是好奇为什么 Int64
和 Decimal
默认以这种方式实现该方法,而不是 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
。
constrained.callvirt
→ call
优化的问题在于,这意味着删除覆盖会成为二进制重大更改,因此无法普遍应用优化。但它可以应用于编译器可以确保覆盖不会被删除的类型。
旧的行为是对 "intrinsic types"(在 C# 中具有关键字的那些)和一些特殊的很少使用的类型使用此优化。新行为是对所有 "special types" 使用优化,其中包括内部类型以及 DateTime
.