Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e 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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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 \u003e 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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot
, '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

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R - #32371

Merged
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual
Apr 27, 2020
Merged

Use intrinsics for SequenceEqual<byte> vectorization to emit at R2R#32371
GrabYourPitchforks merged 4 commits into
dotnet:masterfrom
benaadams:SequenceEqual

Conversation

@benaadams

@benaadamsbenaadams commented Feb 15, 2020

Copy link
Copy Markdown
Member

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

gist Benchamark+Results

Resolves#32363

/cc @ahsonkhan

@jkotasjkotas added the tenet-performance Performance related issue label Feb 15, 2020
@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte>Use intrinsics for SequenceEqual<byte> and improve short lengthsFeb 15, 2020
@benaadams

Copy link
Copy Markdown
MemberAuthor

Redoing this on top of @ahsonkhan's change #32364 as that outperformed this in various areas

@ahsonkhan

ahsonkhan commented Feb 15, 2020

Copy link
Copy Markdown
Contributor

@benaadams, can you run the following benchmark with what's in master (with my recent change) vs. what's in this PR to measure/validate the small buffer perf?

I am asking because I noticed not using the actually built SequenceEqual method was giving different results (compared to having your own local implementation in the benchmark). The RuntimeHelpers.IsBitwiseEquatable<T> call with multiple return points should be part of the benchmark (it ends up changing the results noticably).

[BenchmarkCategory(Categories.CoreFX,Categories.JSON)][DisassemblyDiagnoser(printPrologAndEpilog:true,recursiveDepth:5)]publicclassSequenceEqualThreshold{privatebyte[]_input;privatebyte[]_expected;[Params(0,1,2,3,4)]publicintLength;[GlobalSetup]publicvoidSetup(){varbuilder=newStringBuilder();for(inti=0;i<Length;i++){builder.Append("a");}stringinput=builder.ToString();_input=Encoding.UTF8.GetBytes(input);_expected=_input;_expected=Encoding.UTF8.GetBytes(input);Console.WriteLine(typeof(Span<byte>).AssemblyQualifiedName);Console.WriteLine(typeof(Span<byte>).Assembly.Location);}[Benchmark]publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}}

This is what I did here: #32363

Using the dotnet/performance repo (see https://github.com/dotnet/performance/blob/ca80d8e2886b583d0a69635740b188248d3d6fdd/src/benchmarks/micro/README.md#private-runtime-builds):

  1. Build dotnet/runtime master: build.cmd -subsetCategory coreclr -c Release && build.cmd -subsetCategory libraries /p:CoreCLRConfiguration=Release
  2. Create a copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new directory called 5.0.0_Before.
  3. Copy the recently built relevant files from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release (S.P.Corelib.dll, CoreRun.exe, etc.) into it (i.e. into 5.0.0_Before).
  4. Create another copy of "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0" into a new into a new directory called 5.0.0_After.
  5. Make changes to the implementation in System.Private.Corelib (the optimization you are testing from this PR) and just rebuild the coreclr dlls:
    build.cmd -subsetCategory coreclr -c Release
  6. Copy the newly built dlls (including S.P.Corelib) from E:\GitHub\Fork\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release into 5.0.0_After.
  7. In dotnet/performance repo: cd src\benchmarks\micro
  8. dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_Before\CoreRun.exe" --artifacts "E:\results\before" && dotnet.exe run -c Release -f netcoreapp5.0 --filter SequenceEqualThreshold --corerun "E:\GitHub\Fork\runtime\artifacts\bin\testhost\netcoreapp5.0-Windows_NT-Release-x64\shared\Microsoft.NETCore.App\5.0.0_After\CoreRun.exe" --artifacts "E:\results\after"
  9. cd ..\..\tools\ResultsComparer
  10. dotnet run --base "E:\results\before" --diff "E:\results\after" --threshold 2%

Here are the dlls I copy/override:
image

If you have another, easier way to do it, please do that (and share) :) I probably made things more complicated than needed, so there gotta be a better way to do the perf measurements to speed up inner dev loop.

Btw, @adamsitnik - the workflow instructions need to be updated. The testhost\corerun folder doesn't contain the latest built System.Private.Corelib.dll which is why I ended up having to manually copy the new dlls to that folder.

Also, we may want to see whether removing multiple return statements in the main public method helps (also apparently, the if-branch is the special case, so putting the common code in the else branch or outside the if might be better for perf too, so inverted the condition). Maybe you can find ways to optimize that as well in different ways :)

[MethodImpl(MethodImplOptions.AggressiveInlining)]publicstaticboolSequenceEqual<T>(thisSpan<T>span,ReadOnlySpan<T>other)whereT:IEquatable<T>{intlength=span.Length;boolresult=length==other.Length;if(!RuntimeHelpers.IsBitwiseEquatable<T>()){result=result&&SpanHelpers.SequenceEqual(refMemoryMarshal.GetReference(span),refMemoryMarshal.GetReference(other),length);}else{nuintsize=(nuint)Unsafe.SizeOf<T>();result=result&&SpanHelpers.SequenceEqual(refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(span)),refUnsafe.As<T,byte>(refMemoryMarshal.GetReference(other)),((nuint)length)*size);// If this multiplication overflows, the Span we got overflows the entire address range. There's no happy outcome for this api in such a case so we choose not to take the overhead of checking.}returnresult;}

@ahsonkhanahsonkhan added this to the 5.0 milestone Feb 15, 2020

@jkotasjkotasFeb 16, 2020

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.

nint -> IntPtr casts are a performance trap. I believe that it will go to 64-bit long first on 32-platforms, and the 64-bit long then gets down-casted using checked cast to 32-bit again.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Uses the explicit operator IntPtr(int value) -> IntPtr(int value) so should be ok? (rather than nuint which would go via long)

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.

FYI what Jan said is the reason the UTF-8 transcoding logic uses void* as an intermediary when converting between IntPtr and (whatever integral type).

uintremainingInputBytes=(uint)(void*)Unsafe.ByteOffset(ref*pInputBuffer,ref*pFinalPosWhereCanReadDWordFromInputBuffer)+4;

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.

You are right. This should be fine. I thought there is unsigned/signed conversion too.

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.

But as @GrabYourPitchforks noted it is very easy to miss the cases where it is not fine. We had number of 32-bit specific perf bugs because of that.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yeah, I previously had an issue because pointers are unsigned so my less than zero tests always went the wrong way, which I hadn't expected :(

@benaadams

benaadams commented Feb 16, 2020

Copy link
Copy Markdown
MemberAuthor

@ahsonkhan I was using local copies of SequenceEquals for previous vs master vs PR vs loop method; where master outperforms the PR on short lengths; though am combining the two which looks like it improves on both.

Doing it this way for a couple reasons.

  1. Creating and passing the spans in below code takes significantly longer than the SequenceEqual(ref, ref, nuint) method in its entirety (perhaps we need a .SequenceEqual extension on array so it can bypass span creation if you just have arrays, as string.Equals does?); whereas I wanted a fair comparison against a byte-wise loop (calling overheads something to look at separately?)
publicboolSequenceEqual(){return_input.AsSpan().SequenceEqual(_expected);}
  1. Is quite a pain to have 4 coreclrs for the 4 comparisons I'm making (as you detail above); and I'm not currently very efficient with the new runtime repo so have to keep looking up build steps and hunting for files, which is slower for iterations.

  2. Dasming the tiered/vectorized coreclr is more tricky... whereas its quite easy to right click on method in exe using @EgorBo's Disamso see the asm, make some changes, hit refresh get new asm, etc.

Also, we may want to see whether removing multiple return statements in the main public method helps:

Should branch eliminate to only 1 return?

@benaadams

Copy link
Copy Markdown
MemberAuthor

Probably workflow-wise 3. (iterating on the asm) is the highest factor as you set the bar quite high with the last PR 😄

image

@ahsonkhan

ahsonkhan commented Feb 16, 2020

Copy link
Copy Markdown
Contributor

Should branch eliminate to only 1 return?

I assume so since the check is an intrinsic. How can we test/verify that is indeed the case? Is there a way to observe that in the disassembly?

I will re-run the benchmark tomorrow to verify that it has no perf impact.

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 06a3478 to 99ca277CompareFebruary 16, 2020 03:43
@benaadams

Copy link
Copy Markdown
MemberAuthor

Span passing costs seem very high?

e.g. passing 2 Spans from one method to another is more expensive than comparing the whole 4096 byte spans for equality? (Windows)

| Method | Length | Mean | Error | StdDev |
|------------------------- |------- |----------:|----------:|----------:|
| UseSequenceEqualPR | 4096 | 14.618 ns | 0.0257 ns | 0.0215 ns |
| UseSequenceEqualPRDirect | 4096 | 6.812 ns | 0.0125 ns | 0.0105 ns |
// Passing as params[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPR(){returnSequenceEqualPR(_input.Span,_expected.Span);}[MethodImpl(MethodImplOptions.NoInlining)]privatestaticboolSequenceEqualPR(Span<byte>input,Span<byte>expected){returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}// Using direct[Benchmark][MethodImpl(MethodImplOptions.NoInlining)]publicboolUseSequenceEqualPRDirect(){returnSequenceEqualPRDirect();}[MethodImpl(MethodImplOptions.NoInlining)]privateboolSequenceEqualPRDirect(){Span<byte>input=_input.Span;Span<byte>expected=_expected.Span;returninput.Length==expected.Length&&SequenceEqualPR(refMemoryMarshal.GetReference(input),refMemoryMarshal.GetReference(expected),(nuint)input.Length);}

@benaadams

Copy link
Copy Markdown
MemberAuthor

Updated the benchmark to show the Span passing cost https://gist.github.com/benaadams/bf85405a5eae4c750cf6470a5506fd8d can make the SequenceEqual(ref, ref, nuint) method faster, but its already less than 50% of the invocation cost of SequenceEqual<T>(this Span<T> span, Span<T> other) even up to 4096 bytes 🤔

@benaadams

Copy link
Copy Markdown
MemberAuthor

Raised issue for the Span<byte> costs when used as parameters #32396

Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

As it now has Sse2 intrinisics, removed the AggressiveOptimization which prevents them from being emitted at R2R.

Pure Vector<T> methods are blocked from R2R so need AggressiveOptimization to bypass the inline restrictions at Tier0.

Note this is a regression on Arm as it will run Tier0 code; however I couldn't find a #if to put it behind; and it should get picked up by #33308

@benaadams

benaadams commented Mar 14, 2020

Copy link
Copy Markdown
MemberAuthor

R2R version

; Assembly listing for method SpanHelpers:SequenceEqual(byref,byref,long):bool; Emitting BLENDED_CODE for X64 CPU with SSE2 - Windows; ReadyToRun compilation; optimized code; rsp based frame; fully interruptible; Final local variable assignments;; V00 arg0 [V00,T01] ( 11, 10 ) byref -> rcx ; ...;* V47 tmp27 [V47 ] ( 0, 0 ) byref -> zero-ref "Inlining Arg";; Lcl frame size = 0G_M37173_IG01: ;; bbWeight=1 PerfScore 0.00G_M37173_IG02:cmpr8,8jae SHORT G_M37173_IG07 ;; bbWeight=1 PerfScore 1.25G_M37173_IG03:cmpr8,4jae SHORT G_M37173_IG06xoreax,eaxmovr9,r8andr9,2testr9,r9je SHORT G_M37173_IG04movzxrax, word ptr [rcx]movzxr10, word ptr [rdx]subeax,r10d ;; bbWeight=0.50 PerfScore 3.75G_M37173_IG04:testr8b,1je SHORT G_M37173_IG05movzxrcx, byte ptr [rcx+r9]movzxrdx, byte ptr [rdx+r9]subecx,edxorecx,eaxmoveax,ecx ;; bbWeight=0.50 PerfScore 3.00G_M37173_IG05:testeax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 1.75G_M37173_IG06:addr8,-4moveax, dword ptr [rcx]subeax, dword ptr [rdx]movecx, dword ptr [rcx+r8]subecx, dword ptr [rdx+r8]oreax,ecxtesteax,eax sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.00G_M37173_IG07:cmprcx,rdxje SHORT G_M37173_IG09jmp SHORT G_M37173_IG11 ;; bbWeight=0.50 PerfScore 1.63G_M37173_IG08:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG09:moveax,1 ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG10:ret ;; bbWeight=0.50 PerfScore 0.50G_M37173_IG11:cmpr8,16jb SHORT G_M37173_IG14xorrax,raxaddr8,-16testr8,r8je SHORT G_M37173_IG13 ;; bbWeight=0.50 PerfScore 1.50G_M37173_IG12:movupsxmm0, xmmword ptr [rcx+rax]movupsxmm1, xmmword ptr [rdx+rax]pcmpeqbxmm0,xmm1pmovmskbr9d,xmm0cmpr9d,0xFFFFjne SHORT G_M37173_IG15addrax,16cmpr8,raxja SHORT G_M37173_IG12 ;; bbWeight=4 PerfScore 49.00G_M37173_IG13:movupsxmm0, xmmword ptr [rcx+r8]movupsxmm1, xmmword ptr [rdx+r8]pcmpeqbxmm0,xmm1pmovmskbecx,xmm0cmpecx,0xFFFFjne SHORT G_M37173_IG15jmp SHORT G_M37173_IG09 ;; bbWeight=0.50 PerfScore 6.38G_M37173_IG14:learax,[r8-8]movr8, qword ptr [rcx]subr8, qword ptr [rdx]movrcx, qword ptr [rcx+rax]subrcx, qword ptr [rdx+rax]orr8,rcxtestr8,r8 sete almovzxrax,aljmp SHORT G_M37173_IG08 ;; bbWeight=0.50 PerfScore 6.13G_M37173_IG15:xoreax,eax ;; bbWeight=0.50 PerfScore 0.13G_M37173_IG16:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code 225, prolog size 0, PerfScore 104.63, (MethodHash=63986eca) for method SpanHelpers:SequenceEqual(byref,byref,long):bool; ============================================================

@benaadams
benaadamsforce-pushed the SequenceEqual branch 2 times, most recently from 858ef64 to 8111936CompareMarch 16, 2020 00:38
@adamsitnik

Copy link
Copy Markdown
Member

the workflow instructions need to be updated.

Please excuse me for the late response. Both the benchmarking and profiling docs have been updated some time ago and now they are up to date:

https://github.com/dotnet/performance/blob/master/docs/benchmarking-workflow-dotnet-runtime.md
https://github.com/dotnet/performance/blob/master/docs/profiling-workflow-dotnet-runtime.md

Please let me know if something does not work as expected.

@benaadams

Copy link
Copy Markdown
MemberAuthor

Bit more stable across sizes; but mostly similar. The biggest win I'd highlight is that it's now emitted at R2R rather than always requiring JIT.

image

image

@benaadamsbenaadams changed the title Use intrinsics for SequenceEqual<byte> and improve short lengthsUse intrinsics for SequenceEqual<byte> vectorization to emit at R2RMar 16, 2020
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs Outdated
@benaadams

Copy link
Copy Markdown
MemberAuthor

/cc @GrabYourPitchforks any more to do here?

@GrabYourPitchforks
GrabYourPitchforks merged commit 535b998 into dotnet:masterApr 27, 2020
@GrabYourPitchforksGrabYourPitchforks added the enhancement Product code improvement that does NOT require public API changes/additions label Apr 27, 2020
@danmoseley

Copy link
Copy Markdown
Contributor

@tannergooding do you have time to help get this reviewed? I know @GrabYourPitchforks is fully occupied with something critical. At least one other PR is blocked on this one.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@danmosemsft did you mean to comment on a different PR? This one is merged.

@benaadams
benaadams deleted the SequenceEqual branch May 2, 2020 16:33
@danmoseley

Copy link
Copy Markdown
Contributor

Doh. My goal was to unblock @benadams
#25023 (comment)

@benaadams

Copy link
Copy Markdown
MemberAuthor

Need to minimise code churn/merge clashes between the PRs, have done a cleanup PR to make it easier #35765

@ghostghost locked as resolved and limited conversation to collaborators Dec 10, 2020
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.MemoryenhancementProduct code improvement that does NOT require public API changes/additionstenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can Span<T>.SequenceEqual be optimized further to be faster for small buffers (buffer.Length < 5)?

7 participants

@benaadams@ahsonkhan@adamsitnik@danmoseley@GrabYourPitchforks@jkotas@Dotnet-GitSync-Bot