Uh oh!
There was an error while loading. Please reload this page.
Treat Guid and Int128 types as bitwise equatable - #130644
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR updates CoreCLR’s EE-provided intrinsic for RuntimeHelpers.IsBitwiseEquatable<T>() so that Guid is treated as bitwise-equatable, allowing downstream span/sequence equality paths to use memcmp-based optimizations for Guid element comparisons.
Changes:
- Extend the hardcoded “known bitwise-equatable” type allowlist to include
System.Guidwhen the EE substitutes IL forRuntimeHelpers.IsBitwiseEquatable<T>().
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
huoyaoyuan
commented
Jul 14, 2026
I wonder whether we can implement such optimization without explicit JIT support, namely for |
jkotas
commented
Jul 14, 2026
Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL. |
huoyaoyuan
commented
Jul 14, 2026
Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable. |
EgorBo
commented
Jul 14, 2026
@jkotas anything else needed here? |
jkotas
commented
Jul 14, 2026
It does not sound particularly hard to me... |
EgorBo
commented
Jul 14, 2026
Technically, JIT could optimize with SIMD too (with some caveats), e.g. publicrecordFoo(longA,longB,longC,longD);today emits: movrcx, qword ptr [rsi+0x08]cmprcx, qword ptr [rbx+0x08]jne SHORT G_M13556_IG06movrcx, qword ptr [rsi+0x10]cmprcx, qword ptr [rbx+0x10]jne SHORT G_M13556_IG06movrcx, qword ptr [rsi+0x18]cmprcx, qword ptr [rbx+0x18]jne SHORT G_M13556_IG06movrcx, qword ptr [rsi+0x20]cmprcx, qword ptr [rbx+0x20] sete almovzxrax,al(although, current code is faster if two records have different values in the first field) |
jkotas
commented
Jul 14, 2026
Is this PR regressing Guid performance in some cases then? |
jkotas
commented
Jul 14, 2026
This is performance optimization. It should come with numbers. |
@jkotas um.. not sure how, Guid.Equals is already SIMDified. All it changes is when we call SequenceEqual on a span of Guids with this change we can compare 4 of them at a time (via avx512) while in baseline only 1 at a time with SSE2 (also, with this change, |
EgorBo
commented
Jul 14, 2026
@EgorBot -arm -amd usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;// Case 1: Jit doesn't see the length of the spanpublicclassGuidSequenceEqualBenchmarks{privateGuid[]_left=null!;privateGuid[]_same=null!;privateGuid[]_completelyDifferent=null!;[Params(1,2,4,64)]publicintLength{get;set;}[GlobalSetup]publicvoidSetup(){_left=newGuid[Length];_same=newGuid[Length];_completelyDifferent=_same.Select(i =>newGuid("ffffffff-ffff-ffff-ffff-ffffffffffff")).ToArray();}[Benchmark]publicboolSame(){Span<Guid>left=_left;Span<Guid>right=_same;returnleft.SequenceEqual(right);}[Benchmark]publicboolCompletelyDifferent(){Span<Guid>left=_left;Span<Guid>right=_completelyDifferent;returnleft.SequenceEqual(right);}}publicclassInt128SequenceEqualBenchmarks{privateInt128[]_left=null!;privateInt128[]_same=null!;privateInt128[]_completelyDifferent=null!;[Params(1,2,4,64)]publicintLength{get;set;}[GlobalSetup]publicvoidSetup(){_left=newInt128[Length];_same=newInt128[Length];_completelyDifferent=_same.Select(i =>newInt128(0xffffffffffffffff,0xffffffffffffffff)).ToArray();}[Benchmark]publicboolSame(){Span<Int128>left=_left;Span<Int128>right=_same;returnleft.SequenceEqual(right);}[Benchmark]publicboolCompletelyDifferent(){Span<Int128>left=_left;Span<Int128>right=_completelyDifferent;returnleft.SequenceEqual(right);}}// Case 2: JIT-friendly (JIT sees the length of the span)publicclassSingleGuidSequenceEqualBenchmarks{privateGuid_left;privateGuid_same;privateGuid_completelyDifferent;[GlobalSetup]publicvoidSetup(){_left=Guid.Empty;_same=Guid.Empty;_completelyDifferent=newGuid("ffffffff-ffff-ffff-ffff-ffffffffffff");}[Benchmark]publicboolSameGuid(){Guidg1=_left;Guidg2=_same;return((Span<Guid>)[g1]).SequenceEqual([g2]);}[Benchmark]publicboolCompletelyDifferentGuid(){Guidg1=_left;Guidg2=_completelyDifferent;return((Span<Guid>)[g1]).SequenceEqual([g2]);}}publicclassSingleInt128SequenceEqualBenchmarks{privateInt128_left;privateInt128_same;privateInt128_completelyDifferent;[GlobalSetup]publicvoidSetup(){_left=0;_same=0;_completelyDifferent=-1;}[Benchmark]publicboolSameInt128(){Int128i1=_left;Int128i2=_same;return((Span<Int128>)[i1]).SequenceEqual([i2]);}[Benchmark]publicboolCompletelyDifferentInt128(){Int128i1=_left;Int128i2=_completelyDifferent;return((Span<Int128>)[i1]).SequenceEqual([i2]);}} |
jkotas
commented
Jul 14, 2026
Do we know how to explain this?
|
tannergooding
commented
Jul 14, 2026
I've kicked off a local copilot task to look at it. It seems to have done a good job and to have handled the key cases for an MVP at this point. I'm doing some more testing and local review before I'll put up a PR |
EgorBo
commented
Jul 14, 2026
Not sure, the codegen for main is: ; Assembly listing for method SingleInt128SequenceEqualBenchmarks:CompletelyDifferentInt128():bool:this (Tier1); Emitting BLENDED_CODE for x64 + VEX + EVEX on Windows; Tier1 code; optimized code; optimized using Synthesized PGO; rsp based frame; partially interruptible; with Synthesized PGO: fgCalledCount is 100; No PGO data; 1 inlinees with PGO data; 9 single block inlinees; 0 inlinees without PGO dataG_M000_IG01: ;; offset=0x0000subrsp,72xoreax,eaxmov qword ptr [rsp+0x28],rax vxorps xmm4,xmm4,xmm4 vmovdqa xmmword ptr [rsp+0x30],xmm4mov qword ptr [rsp+0x40],raxG_M000_IG02: ;; offset=0x001Amovrdx, qword ptr [rcx+0x08]movr8, qword ptr [rcx+0x10]movrax, qword ptr [rcx+0x28]movrcx, qword ptr [rcx+0x30]mov qword ptr [rsp+0x38],rdxmov qword ptr [rsp+0x40],r8mov qword ptr [rsp+0x28],raxmov qword ptr [rsp+0x30],rcxlearcx, bword ptr [rsp+0x38]leardx, bword ptr [rsp+0x28]movr8d,1call[System.SpanHelpers:SequenceEqual[System.Int128](byref,byref,int):bool]nopG_M000_IG03: ;; offset=0x0055addrsp,72retfor PR it is: G_M41373_IG01: ;; offset=0x0000subrsp,40xoreax,eaxmov qword ptr [rsp+0x08],rax vxorps xmm4,xmm4,xmm4 vmovdqa xmmword ptr [rsp+0x10],xmm4mov qword ptr [rsp+0x20],rax ;; size=26 bbWeight=1 PerfScore 3.83G_M41373_IG02: ;; offset=0x001Amovrax, qword ptr [rcx+0x08]movrdx, qword ptr [rcx+0x10]movr8, qword ptr [rcx+0x28]movrcx, qword ptr [rcx+0x30]mov qword ptr [rsp+0x18],raxmov qword ptr [rsp+0x20],rdxmov qword ptr [rsp+0x08],r8mov qword ptr [rsp+0x10],rcxlearax, bword ptr [rsp+0x18]learcx, bword ptr [rsp+0x08]cmprax,rcxje SHORT G_M41373_IG04vmovupsxmm0, xmmword ptr [rsp+0x18] vpcmpeqb k1,xmm0, xmmword ptr [rsp+0x08]kortestw k1, k1jb SHORT G_M41373_IG04xoreax,eax ;; size=76 bbWeight=1 PerfScore 19.50G_M41373_IG03: ;; offset=0x0066addrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25G_M41373_IG04: ;; offset=0x006Bmoveax,1jmp SHORT G_M41373_IG03 ;; size=7 bbWeight=0 PerfScore 0.00I'd assume Int128's: publicstaticbooloperator==(Int128left,Int128right)=>(left._lower==right._lower)&&(left._upper==right._upper);would be indeed faster than SIMD (potentially, unaligned with a penalty) for case when _lower is different, but in this case we also pay for SequenceEqual and size checks inside it 🤔 (unless it's inlined and my codegen from win-x64 is not the same) |
EgorBo
commented
Jul 14, 2026
@EgorBot -intel -amd -profiler --envvars DOTNET_JitDisasm:CompletelyDifferentInt128 usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;publicclassSingleInt128SequenceEqualBenchmarks{privateInt128_left;privateInt128_same;privateInt128_completelyDifferent;[GlobalSetup]publicvoidSetup(){_left=0;_same=0;_completelyDifferent=-1;}[Benchmark]publicboolCompletelyDifferentInt128(){Int128i1=_left;Int128i2=_completelyDifferent;return((Span<Int128>)[i1]).SequenceEqual([i2]);}} |
@jkotas so the PR struggles from stall-forwarding as @jakobbotsch suspected offline. We have a legacy struct promotion that we eventually want to remove, e.g. with vmovupsxmm0, xmmword ptr [rcx+0x08]vmovupsxmm1, xmmword ptr [rcx+0x28]vmovups xmmword ptr [rsp+0x18],xmm0vmovups xmmword ptr [rsp+0x08],xmm1vmovupsxmm0, xmmword ptr [rsp+0x18] vpcmpnequq k1,xmm0, xmmword ptr [rsp+0x08]kortestb k1, k1 sete almovzxrax,alinstead of current: movrax, qword ptr [rcx+0x08]movrdx, qword ptr [rcx+0x10]movr8, qword ptr [rcx+0x28]movrcx, qword ptr [rcx+0x30]mov qword ptr [rsp+0x18],raxmov qword ptr [rsp+0x20],rdxmov qword ptr [rsp+0x08],r8mov qword ptr [rsp+0x10],rcxlearax, bword ptr [rsp+0x18]learcx, bword ptr [rsp+0x08]cmprax,rcxje SHORT G_M41373_IG04vmovupsxmm0, xmmword ptr [rsp+0x18] vpcmpeqb k1,xmm0, xmmword ptr [rsp+0x08]kortestw k1, k1jb SHORT G_M41373_IG04xoreax,eaxSo 2 options:
|
EgorBo
commented
Jul 14, 2026
I think for most other benchmarks for Int128 PR still makes it faster judging by the results. |
Uh oh!
There was an error while loading. Please reload this page.
Treat
Guid,Int128, andUInt128as bitwise equatable, enablingSequenceEqualto use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.The goal is just to be able to theoretically implement
Guid.Equalsentirely in 100% memory safe code. It would be nice if C# could allow us to write it like this:[g1].SequenceEqual([g2])