Calling t?.GetHashCode() in generic method emits longer code than needed
- Dominant language
- C#
- Stars
- 20.7k
- Forks
- 4.3k
- PR merge metrics
- PR metrics pending
Description
**Version Used**: 4.10.0-3.25064.8 (85262f5f) in .NET SDK 8.0.309
**Steps to Reproduce**:
1. Create `global.json`:
```JSON
{
"sdk": {
"version": "8.0.309"
}
}
```
2. Create `GenericCallDemo.csproj`:
```XML
netstandard2.0
6.0
```
3. Create `Demo.cs`:
```csharp
public struct Demo
{
int total;
// Some dummy code to use the parameter.
void Add(int i) { total += ~i; }
public void Add_6_0_0(T value)
{
Add(value?.GetHashCode() ?? 0);
}
public void Add_1_1_1(T value)
{
Add(value != null ? value.GetHashCode() : 0);
}
}
```
4. Run `dotnet build --configuration=Release`
5. Disassemble `bin/Release/netstandard2.0/GenericCallDemo.dll`
**Diagnostic Id**: None
**Expected Behavior**:
Should have the same IL in Add_6_0_0 and Add_1_1_1. This is what happens in .NET SDK 6.0.428:
```
.method public hidebysig instance void Add_1_1_1(!!T 'value') cil managed
{
// Code size 31 (0x1f)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: box !!T
IL_0007: brtrue.s IL_000c
IL_0009: ldc.i4.0
IL_000a: br.s IL_0019
IL_000c: ldarga.s 'value'
IL_000e: constrained. !!T
IL_0014: callvirt instance int32 [netstandard]System.Object::GetHashCode()
IL_0019: call instance void Demo::Add(int32)
IL_001e: ret
} // end of method Demo::Add_1_1_1
.method public hidebysig instance void Add_6_0_0(!!T 'value') cil managed
{
// Code size 31 (0x1f)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: box !!T
IL_0007: brtrue.s IL_000c
IL_0009: ldc.i4.0
IL_000a: br.s IL_0019
IL_000c: ldarga.s 'value'
IL_000e: constrained. !!T
IL_0014: callvirt instance int32 [netstandard]System.Object::GetHashCode()
IL_0019: call instance void Demo::Add(int32)
IL_001e: ret
} // end of method Demo::Add_6_0_0
```
**Actual Behavior**:
`Add_6_0_0` has 80% longer IL. This is what happens in .NET SDK 8.0.309 and 9.0.200:
```
.method public hidebysig instance void Add_1_1_1(!!T 'value') cil managed
{
// Code size 31 (0x1f)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: box !!T
IL_0007: brtrue.s IL_000c
IL_0009: ldc.i4.0
IL_000a: br.s IL_0019
IL_000c: ldarga.s 'value'
IL_000e: constrained. !!T
IL_0014: callvirt instance int32 [netstandard]System.Object::GetHashCode()
IL_0019: call instance void Demo::Add(int32)
IL_001e: ret
} // end of method Demo::Add_1_1_1
.method public hidebysig instance void Add_6_0_0(!!T 'value') cil managed
{
// Code size 56 (0x38)
.maxstack 3
.locals init (!!T V_0)
IL_0000: ldarg.0
IL_0001: ldarga.s 'value'
IL_0003: ldloca.s V_0
IL_0005: initobj !!T
IL_000b: ldloc.0
IL_000c: box !!T
IL_0011: brtrue.s IL_0027
IL_0013: ldobj !!T
IL_0018: stloc.0
IL_0019: ldloca.s V_0
IL_001b: ldloc.0
IL_001c: box !!T
IL_0021: brtrue.s IL_0027
IL_0023: pop
IL_0024: ldc.i4.0
IL_0025: br.s IL_0032
IL_0027: constrained. !!T
IL_002d: callvirt instance int32 [netstandard]System.Object::GetHashCode()
IL_0032: call instance void Demo::Add(int32)
IL_0037: ret
} // end of method Demo::Add_6_0_0
```
[SharpLab.io shows the x86 assembly code for Add_6_0_0\ is likewise longer.](https://sharplab.io/#v2:C4LghgzgtgPgAgZgAQWAJwK4GNhICICmUA9kgN4CwAUErUgJYB2uwxwYANgNzV1K90A9IKQBlYlAJIAJhihQAnkizFpU1kgwR1ACykAHMGjCTgBNADoBtOABYkAQWnSAFE1z0AlOSSt2HJABqAF4kAD96LiQAX2t+GjoAbVEdI30AGTAAIwsAJQxmekkLACl6YABxAkZzeiwXYAV9AmIAMxcOYkYAcwB+T08AXTjEJDtHZwB9ADZJgAZ5gB4AFQA+F2WkADdODAJvSgS+WidXHY493osq4AAJSB0AYVUCF29e3qQ5zx4jmOo4slUmgMtk8gVgEUCKVylUamg6g0mi12p0ev0hiNkONTpMAIz4/Erdabc57A5xPinFxkqQAQlCjAwHACn1p1wIdwezzUbyQIC+PzisSoIqAA=)
**Notes**:
Found while comparing IL between versions 1.1.1 and 6.0.0 of the Microsoft.Bcl.HashCode package. The source code of the HashCode.Add\(T value) method is identical in both versions and , but the IL is longer in 6.0.0.
Contributor guide
Research direction
Reproduce the issue using the global.json, GenericCallDemo.csproj, and Demo.cs steps, then disassemble the Release netstandard2.0 assembly and compare Add_6_0_0 with Add_1_1_1. Trace the compiler lowering for the null-conditional GetHashCode call and determine why it emits the extra default-value checks; done means restoring equivalent IL and covering the regression with a compiler test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100