Search code examples
c#.netcil

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


Consider the following methods in 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();
}

Let's look at the instructions generated by the compiler:

For the Decimal method:

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

For the Int64 method:

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

For the DateTime method:

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

Why is the DateTime.GetHashCode() method being treated as a virtual call of Object.GetHashCode(), considering there is an overridden GetHashCode() method for the DateTime struct?

Furthermore, I can create a method that directly calls the System.DateTime.GetHashCode() method without the virtual call using the following code:

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);

Then create a delegate to test it:

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.

Just curious why the method is implemented this way by default for Int64 and Decimal, but not DateTime.


Solution

  • When it comes to Roslyn, what you're describing is an old behavior (Roslyn version 1.1.0 and older). The new behavior (version 1.2.0 and newer) is to use call for DateTime too.

    The change was made in pull request String concat with char and similar primitives should call overriden ToString directly (#7080).

    The problem with the constrained.callvirtcall optimization is that it means removing the override becomes a binary breaking change, so the optimization can't be applied universally. But it can be applied to types where the compiler can be sure that the override is not going to be removed.

    The old behavior was to use this optimization for "intrinsic types" (those that have keywords in C#) and some special rarely used types. The new behavior is to use the optimization for all "special types", which include intrinsic types and also DateTime.