Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Treat Guid and Int128 types as bitwise equatable - #130644

Merged
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable
Jul 14, 2026
Merged

Treat Guid and Int128 types as bitwise equatable#130644
EgorBo merged 8 commits into
dotnet:mainfrom
EgorBo:egorbo/guid-bitwise-equatable

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 13, 2026

Copy link
Copy Markdown
Member

Treat Guid, Int128, and UInt128 as bitwise equatable, enabling SequenceEqual to use the existing memcmp unrolling. Also correct the integer comparison pseudo-name used by JitDisasm.

voidFoo(Guidg1,Guidg2){if(((Span<Guid>)[g1]).SequenceEqual((Span<Guid>)[g2])){Console.WriteLine("Equal");}}
- lea rdi, [rbp-0x30]- lea rsi, [rbp-0x40]- mov edx, 1- call [System.SpanHelpers:SequenceEqual[System.Guid](byref,byref,int):bool]- test eax, eax+ vmovups xmm0, xmmword ptr [rbp-0x30]+ vpcmpnequq k1, xmm0, xmmword ptr [rbp-0x40]+ kortestb k1, k1+ sete dil+ movzx rdi, dil+ test edi, edi

The goal is just to be able to theoretically implement Guid.Equals entirely in 100% memory safe code. It would be nice if C# could allow us to write it like this: [g1].SequenceEqual([g2])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:09
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 13, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Guid when the EE substitutes IL for RuntimeHelpers.IsBitwiseEquatable<T>().

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:20
@EgorBoEgorBo changed the title Treat Guid as bitwise equatableTreat Guid and Int128 types as bitwise equatableJul 13, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9156991f-d238-4aef-b89d-bdd9ad98ae22
CopilotAI review requested due to automatic review settings July 13, 2026 20:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/vm/jitinterface.cpp
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d
CopilotAI review requested due to automatic review settings July 13, 2026 21:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b47f1ec-b319-48a6-8f7e-b621b088fd2d

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings July 13, 2026 22:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
Comment threadsrc/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs Outdated
@huoyaoyuan

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap. It may be complex to (recursively) identify the field-wise equation tests.

@jkotas

Copy link
Copy Markdown
Member

I wonder whether we can implement such optimization without explicit JIT support, namely for record struct when every field is bitwise equatable and there's no gap.

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

@huoyaoyuan

Copy link
Copy Markdown
Member

Records allow overriding of equality. I do not think you can reliably tell whether the default was overridden without analyzing the IL.

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@jkotas anything else needed here?

@jkotas

Copy link
Copy Markdown
Member

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

@EgorBo

Copy link
Copy Markdown
MemberAuthor

Yes, and that's what makes it hard. The IL has to be analyzed recursively to ensure every field are indeed bitwise equatable.

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
Member

although, current code is faster if two records have different values in the first field

Is this PR regressing Guid performance in some cases then?

@jkotas
jkotas self-requested a review July 14, 2026 13:35
@jkotas

Copy link
Copy Markdown
Member

This is performance optimization. It should come with numbers.

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

Is this PR regressing Guid performance in some cases then?

@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, SequenceEqual becomes unrolling friendly when JIT sees the length). I can kick off a EgorBot, but I don't see a scenario it might regress tbh

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@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

Copy link
Copy Markdown
Member

Do we know how to explain this?

CompletelyDifferentInt128PR #1306445.893 ns0.0294 ns1.00
CompletelyDifferentInt128main1.548 ns0.0166 ns0.26

@tannergooding

Copy link
Copy Markdown
Member

It does not sound particularly hard to me...

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

Copy link
Copy Markdown
MemberAuthor

Do we know how to explain this?

CompletelyDifferentInt128 PR #130644 5.893 ns 0.0294 ns 1.00
CompletelyDifferentInt128 main 1.548 ns 0.0166 ns 0.26

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,72ret

for 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.00

I'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

Copy link
Copy Markdown
MemberAuthor

@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]);}}

@EgorBo

EgorBo commented Jul 14, 2026

Copy link
Copy Markdown
MemberAuthor

@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 DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION we end up emitting

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,al

instead 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,eax

So 2 options:

  1. We merge as is and eventually this problem goes away as we remove the legacy struct promoter (that promotes based on metadata fields)
  2. We remove Int128/UInt128 from this PR. I do not think Guid can struggle from the same problem (the old promoter would not handle it I guess)

@EgorBo

Copy link
Copy Markdown
MemberAuthor

I think for most other benchmarks for Int128 PR still makes it faster judging by the results.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@EgorBo
EgorBo enabled auto-merge (squash) July 14, 2026 18:17
@EgorBo
EgorBo merged commit dab6af2 into dotnet:mainJul 14, 2026
138 of 140 checks passed
@EgorBo
EgorBo deleted the egorbo/guid-bitwise-equatable branch July 14, 2026 18:17
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jul 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 15, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@EgorBo@huoyaoyuan@jkotas@tannergooding@MichalStrehovsky@MichalPetryka