Uh oh!
There was an error while loading. Please reload this page.
JIT: Unroll Equals and StartsWith for constant strings and spans. [0..32] chars - #65288
Conversation
ghost
commented
Feb 14, 2022
Tagging subscribers to this area: @JulieLeeMSFT Issue DetailsThis PR unrolls This PR handles the following APIs: String.Equals(strings1,strings2)
String.Equals(strings1,strings2,StringComparison.Ordinal)
strObj.Equals(strings1)
strObj.Equals(strings1,StringComparison.Ordinal)
strObj.StartsWith(strings1,StringComparison.Ordinal)
MemoryExtensions.SequenceEqual<char>(ROS<char>s1,ROS<char>s2)
MemoryExtensions.Equals(ROS<char>s1,ROS<char>s2,StringComparison.Ordinal)
MemoryExtensions.StartsWith(ROS<char>s1,ROS<char>s2,StringComparison.Ordinal)It unrolls & vectorizes them when either s1 or s2 are constants for [0..32] utf16 chars range using SWAR, SSE, AVX or AdvSimd (arm64). The existing code can be re-used to handle:
Motivation:I've seen quite a few Also, #65222 relies on this PR. Example:boolTest1(strings)=>s.StartsWith("https://",StringComparison.Ordinal);boolTest2(strings)=>s=="Access-Control-Allow-Credentials";New codegen: ; Method Tests:Test1(System.String):boolvzerouppercmp dword ptr [rcx+8],8 ;; <-- Length >= 8jne SHORT G_M46665_IG04 vmovdqu xmm0, xmmword ptr [rcx+12]vpxorxmm0,xmm0, xmmword ptr [reloc @RWD00]vptestxmm0,xmm0 sete almovzxrax,aljmp SHORT G_M46665_IG05 ;; we end up doing 'movzx rax, al' twice :(G_M46665_IG04:xoreax,eaxG_M46665_IG05:movzxrax,alretRWD00 dq 0070007400740068h,002F002F003A0073h; Total bytes of code: 41; Method Tests:Test2(System.String):boolvzerouppertestrcx,rcx ;; <-- is null?je SHORT G_M48330_IG06cmp dword ptr [rcx+8],32 ;; <-- Length == 32jne SHORT G_M48330_IG05 vmovdqu ymm0, ymmword ptr[rcx+12]vpxorymm0,ymm0, ymmword ptr[reloc @RWD00] vmovdqu ymm1, ymmword ptr[rcx+44] ;; <- btw, the "pipeline" issuevpxorymm1,ymm1, ymmword ptr[reloc @RWD32]vporymm0,ymm0,ymm1vptestymm0,ymm0 sete almovzxrax,aljmp SHORT G_M48330_IG07G_M48330_IG05:xoreax,eaxjmp SHORT G_M48330_IG07G_M48330_IG06:xoreax,eaxG_M48330_IG07:movzxrax,alvzeroupperretRWD00 dq 0065006300630041h,0043002D00730073h,00720074006E006Fh,0041002D006C006FhRWD32 dq 0077006F006C006Ch,006500720043002Dh,0074006E00650064h,0073006C00610069h; Total bytes of code: 70There are minor block layout issues but they're unrelated. The algorithm has a pretty simple heuristic (budget) to avoid doing too many unrolls in a single method. Simple benchmark[Benchmark][Arguments("https://google.com")]publicboolStartsWith(strings)=>s.StartsWith("https://",StringComparison.Ordinal);
NOTE: PS: Spans aren't enabled in the current commit - there is a small issue with them I am trying to resolve.
|
| public static int Main() | ||
| { | ||
| int testCount = 0; | ||
| foreach (var method in typeof(Tests).GetMethods()) |
There was a problem hiding this comment.
Another way (that avoids reflection and repeating Equals_# methods) could be by marking ValidateEquals method public and dup the string instances:
inttestCount=0;foreach(stringtestStrinTests.s_TestData){testCount++;stringnewStr=newstring(testStr.ToCharArray());Tests.ValidateEquals(testStr==newStr,testStr,newStr);}There was a problem hiding this comment.
Do you mean to use SG/T4 generation? Because I do need explicit literals in methods to test
There was a problem hiding this comment.
explicit literals
Ah, ok. 👍
(SG/T4 would pull in more infra code here than necessary. I was thinking if intention is to test other instance of same string, then we can clone it via char[] overload and reduce LOC)
stephentoub
commented
Feb 14, 2022
I assume this is the heuristic? |
Remember that the "take advantage of the fact that strings are null-terminated" trick won't work with spans. Spans aren't guaranteed null-terminated. Edit: there are a few other patterns that can be optimized as well. For example, |
There are methods like this in which Roslyn ends up emitting hundreds of str == "...:" and it's not clear what to do with them. Anyway, maybe the limit is not worth it at all, I'll check the diffs.
Yeah I don't rely on it at all here - it's left up-for-grabs 🙂 Even Length = 3 case is handled via two Int32 loads.
That one is already optimized in Main via |
Uh oh!
There was an error while loading. Please reload this page.
GrabYourPitchforks
commented
Feb 14, 2022
Neat! That'll teach me to look at main sources rather than 6.0 sources. 😁 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I think I've added everything I wanted to add, it now handles Span too, full list of APIs: // 1) String.Equals(string, string) // which means `str == "..."` too// 2) String.Equals(string, string, StringComparison.Ordinal)// 3) str.Equals(string)// 4) str.Equals(String, StringComparison.Ordinal)// 5) str.StartsWith(string, StringComparison.Ordinal)// 6) MemoryExtensions.SequenceEqual<char>(ROS<char>, ROS<char>)// 7) MemoryExtensions.Equals(ROS<char>, ROS<char>, StringComparison.Ordinal)// 8) MemoryExtensions.StartsWith<char>(ROS<char>, ROS<char>)// 9) MemoryExtensions.StartsWith(ROS<char>, ROS<char>, StringComparison.Ordinal)all of them work when one of the arguments (left or right) is a string literal or I do plan to add This PR needs more tests for all kinds of cases and an additional test for cross-page boundary (just in case) and I need to investigate the diffs (SPMI ones are empty because of lack of needed context - |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com>
Co-authored-by: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com>
EgorBo
commented
Mar 2, 2022
/azp run runtime-coreclr outerloop, runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 2 pipeline(s). |
EgorBo
commented
Mar 17, 2022
Improvement on alpine-x64 dotnet/perf-autofiling-issues#3978 |

This PR unrolls
Equals/StartsWithfor "half" constant strings and spans in [0..32] chars length.It does pretty much the same job as #64821 but in JIT this time to avoid problems listed there (e.g. low inliner's budget and throughput penalty). I decided to create a new PR to keep that as a reference ("what can be done in pure C#").
This PR handles the following APIs:
It unrolls & vectorizes them when either s1 or s2 are constants for [0..32] utf16 chars range using SWAR, SSE, AVX or AdvSimd (arm64).
The existing code can be re-used to handle:
Span.CopyToOrdinalIgnoreCasefor any case this PR already handles for just OrdinalEndsWithMotivation:
I've seen quite a few
SequenceEqualagainst constant data in high-perf scenarios e.g. "is input one of the well-known headers?" or e.g. TechEmpower: https://github.com/TechEmpower/FrameworkBenchmarks/blob/master/frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs#L98 - mostly on UTF8 inputs (I plan to support those eventually), but UTF16 are also not rare.Also, #65222 relies on this PR (it introduced a couple of regressions in order to make code cleaner and BE-friendly and expects this PR to fix them)
Example:
New codegen:
There are minor block layout issues but they're unrelated. The algorithm has a pretty simple heuristic (budget) to avoid doing too many unrolls in a single method.
Simple benchmark