-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve codegen for delegate cast / type check? #55841
Comments
Note the difference is larger when the delegate is a generic, involving a call to a cast helper in one case but not the other: using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Runtime.CompilerServices;
[DisassemblyDiagnoser]
[MemoryDiagnoser]
public class Program
{
public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
private object _o = (Action<object>)(_ => { });
[Benchmark] public void Cast() => M1(_o);
[Benchmark] public void TypeCheck() => M2(_o);
private void M1(object o)
{
if (o is Action<object> action)
action(o);
}
private void M2(object o)
{
if (o is not null && o.GetType() == typeof(Action<object>))
Unsafe.As<Action<object>>(o)(o);
}
}
.NET 6.0.0 (42.42.42.42424), X64 RyuJIT; Program.Cast()
push rsi
sub rsp,20
mov rsi,[rcx+8]
mov rdx,rsi
mov rcx,offset MT_System.Action`1[[System.Object, System.Private.CoreLib]]
call CORINFO_HELP_ISINSTANCEOFARRAY
test rax,rax
je short M00_L00
mov rcx,[rax+8]
mov rdx,rsi
mov rax,[rax+18]
add rsp,20
pop rsi
jmp rax
M00_L00:
add rsp,20
pop rsi
ret
; Total bytes of code 57 .NET 6.0.0 (42.42.42.42424), X64 RyuJIT; Program.TypeCheck()
mov rdx,[rcx+8]
test rdx,rdx
je short M00_L00
mov rcx,offset MT_System.Action`1[[System.Object, System.Private.CoreLib]]
cmp [rdx],rcx
jne short M00_L00
mov rax,rdx
mov rcx,[rax+8]
mov rax,[rax+18]
jmp rax
M00_L00:
ret
; Total bytes of code 39 |
I'm also confused by (at least the naming of) the helper that's emitted here:
given that we're testing for a delegate, not an array. |
hm.. it shows IsInstanceOfAny here (same in .NET 6.0 Main locally). |
Interesting. I'm using main as well (as of yesterday). Maybe an artifact of the disassembler used by benchmarkdotnet? |
Ok, cool, so ignore that part. Don't ignore the part where there's a helper here at all 😄 |
The first issue reminds me discussion in #47920 and looks like it could be handled via jump-threading? cc @AndyAyersMS That edge from BB01 to BB04 (but a PHI is involved). V02 is V04 if it comes from BB01 |
@stephentoub regarding this case: private void M1(object o)
{
if (o is Action<object> action)
action(o);
}
private void M2(object action)
{
if (action is object o && o.GetType() == typeof(Action<object>))
Unsafe.As<Action<object>>(o)(o);
} I don't think it's a correct transformation, e.g.: public static void Main(string[] args)
{
Action<object> action = p => Console.WriteLine("Hello");
M1(action); // prints Hello
M2(action); // does not print anything
}
private static void M1(object o)
{
if (o is Action<Program> action)
action(null);
}
private static void M2(object action)
{
if (action.GetType() == typeof(Action<Program>))
Unsafe.As<Action<Program>>(action)(null);
} but we still can optimize it to like
via PGO + GDV #55325 |
I was typing too quickly. It should have been: if (o is not null && o.GetType() == typeof(Action<object>))
Unsafe.As<Action<object>>(o)(o); I updated the posts. Doesn't change the issue, though. |
Why is IsInstanceOfAny required at all? The delegate is a sealed type. |
The delegate is, but its T has namespace System
{
public delegate void Action<in T>(T obj);
} so you can cast |
Gaaahh, of course 😦 |
it's not the first place where performance suffers from Covariance/Contravariance's existence 😄 |
In #49713 I started working on walking RBO back through PHIs but I hit some snags... also weren't you going to clean some of this up earlier? |
@EgorBo, given the explanation, why does this still emit a call to the helper? using System;
public class C {
public bool M(object o) => o is Action<int>;
}
|
Will try to revise that attempt (branch).
From my understanding, we can optimize it when T is a value type like in your case (but not when it's a ref type even if it's sealed) |
Right, that's because it's contravariant, not covariant. So, remind me why the |
|
The jit only knows that some classes have variance, not any of the particulars (co- / contra- / both; which type vars can vary, etc). So I'm not sure how we'd take advantage of this case; we might need new jit interface methods or find ways to pose this as a question that existing methods can answer. |
I'm going to mark this as future; @EgorBo you can change if you think there's some low-risk way we could do this. |
This looks fixed, the codegen for ; Method C:M1(System.Object):this
G_M55036_IG01:
;; size=0 bbWeight=1 PerfScore 0.00
G_M55036_IG02:
test rdx, rdx
je SHORT G_M55036_IG04
;; size=5 bbWeight=1 PerfScore 1.25
G_M55036_IG03:
mov rcx, 0xD1FFAB1E ; System.Action
cmp qword ptr [rdx], rcx
je SHORT G_M55036_IG05
;; size=15 bbWeight=0.25 PerfScore 1.06
G_M55036_IG04:
ret
;; size=1 bbWeight=0.50 PerfScore 0.50
G_M55036_IG05:
mov rcx, gword ptr [rdx+08H]
;; size=4 bbWeight=0.50 PerfScore 1.00
G_M55036_IG06:
tail.jmp [rdx+18H]System.Action:Invoke():this
;; size=4 bbWeight=0.50 PerfScore 1.00
; Total bytes of code: 29
Presumably was fixed by #76283. |
Consider:
On my machine, this results in:
with the disassembly:
.NET 6.0.0 (42.42.42.42424), X64 RyuJIT
.NET 6.0.0 (42.42.42.42424), X64 RyuJIT
Is there anything we could do to eliminate the (small) gap / extra branch?
The text was updated successfully, but these errors were encountered: