Repository navigation
Nullable<T> interface check / dispatch is comparatively very slow #50915
Copy link
Copy link
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIin-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is merged
Milestone
Description
Repro:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args);
private S _s = default(S);
private C _c = new C();
private S? _ns = (S?)default(S);
[Benchmark] public int Struct() => CallM(_s);
[Benchmark] public int Class() => CallM(_c);
[Benchmark] public int Nullable() => CallM(_ns);
[Benchmark] public int NullableSpecialized() => CallMSpecial(_ns);
static int CallM<T>(T t)
{
if (t is IMyInterface)
{
return ((IMyInterface)t).M();
}
return 0;
}
static int CallMSpecial<T>(T? t) where T : struct
{
if (t.HasValue)
{
return CallM(t.GetValueOrDefault());
}
return 0;
}
}
interface IMyInterface
{
int M();
}
struct S : IMyInterface
{
public int M() => 42;
}
class C : IMyInterface
{
public int M() => 42;
}| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | Code Size |
|---|---|---|---|---|---|---|---|---|
| Struct | 0.4807 ns | 0.0044 ns | 0.0037 ns | - | - | - | - | 16 B |
| Class | 3.0842 ns | 0.0045 ns | 0.0038 ns | - | - | - | - | 86 B |
| Nullable | 88.6692 ns | 0.5664 ns | 0.4729 ns | 0.0076 | - | - | 48 B | 132 B |
| NullableSpecialized | 1.5149 ns | 0.0087 ns | 0.0077 ns | - | - | - | - | 64 B |
Is there anything we can do to make the nullable case faster and less inclined to box without needing a special code path to handle it (e.g. my CallMSpecial, which in my current actual code is another overload so that a nullable binds to it more tightly).
Here's what the codegen for the nullable case looks like:
; Program.CallM[[System.Nullable`1[[S, Benchmarks]], System.Private.CoreLib]](System.Nullable`1<S>)
sub rsp,28
mov [rsp+30],rcx
lea rdx,[rsp+30]
mov rcx,offset MT_System.Nullable`1[[S, Benchmarks]]
call CORINFO_HELP_BOX_NULLABLE
mov rdx,rax
mov rcx,offset MT_IMyInterface
call CORINFO_HELP_ISINSTANCEOFINTERFACE
test rax,rax
je short M01_L00
lea rdx,[rsp+30]
mov rcx,offset MT_System.Nullable`1[[S, Benchmarks]]
call CORINFO_HELP_BOX_NULLABLE
mov rdx,rax
mov rcx,offset MT_IMyInterface
call CORINFO_HELP_CHKCASTINTERFACE
mov rcx,rax
mov r11,7FFCDCEE05D0
call qword ptr [7FFCDD2705D0]
nop
add rsp,28
ret
M01_L00:
xor eax,eax
add rsp,28
ret
; Total bytes of code 122category:cq
theme:basic-cq
skill-level:intermediate
cost:medium
impact:small
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIin-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is merged
Type
Projects
- StatusShow more project fieldsDone
Activity
JulieLeeMSFT commentedon Apr 8, 2021
cc @dotnet/jit-contrib.
EgorBo commentedon Apr 9, 2021
I can take a look, so basically some of our optimizations are just not
Nullable<>friendly, e.g.impBoxPatternMatchWe need to handle IL sequences like this:
to be optimized to:
(same for box+isinst+ldnull+cgt.un).
Another issue here is that we might want to optimize boxing for Nullables from this:
to something like this:
Joe4evr commentedon Apr 9, 2021
Would that really make a difference? IIRC, boxing a no-value
Nullable<T>already makes the reference point to actualnull.EgorBo commentedon Apr 9, 2021
It would allow us to avoid "call" overhead (by slightly regressing size and additional check for non-null case - so I agree that one is questionable and requires some analysis or how frequent we pass null there in a general case) branch: EgorBo@a0d8ee9
stephentoub commentedon Apr 9, 2021
Thanks for looking at this, @EgorBo. Knowing whether these can and will (eventually) be improved will impact what overloads we choose to expose in #50635. This is a significant enough difference that if we don't think this can be improved meaningfully, we'll need to expose the T? overloads there. If we can make it work better, we can leave those out.
What's particularly interesting to me is how much worse the nullable case is than just boxing it from the start:
EgorBo commentedon Apr 9, 2021
Yeah that's funny, you box it once while original
Nullable()benchmark actually boxes it twice (in isinst and then in cast) 🙂EgorBo commentedon Apr 9, 2021
I think this specific case definitely can be improved as part of 6.0, the first part is already ready: #50997
the second one (to basically handle this: sharplab.io) also looks doable.
13 remaining items
EgorBot commentedon Jul 28, 2024
EgorBo commentedon Sep 3, 2024
@EgorBot -intel -arm64 --runtimes net6.0 net7.0 net8.0 net9.0
Inline nullable allocators (dotnet#122167)