dotnet / dotnet/roslyn

Calling t?.GetHashCode() in generic method emits longer code than needed

Open
#77,386 1 comment 0 reactions 0 assignees View on GitHub
Area-Compilers Code Gen Quality
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.