[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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 > 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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@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

[WIP] Improve performance of Utf8Parser.TryParseInt32D - #32843

Closed
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2
Closed

[WIP] Improve performance of Utf8Parser.TryParseInt32D#32843
GrabYourPitchforks wants to merge 7 commits into
dotnet:masterfrom
GrabYourPitchforks:utf8tryparse_2

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

WIP because it's not plumbed through to other Utf8Parser APIs and because these same improvements are likely viable for other methods like int.Parse. I also need to clean up / comment the code a little bit.

There are a handful of patterns being used here that result in improved codegen. Some of these are things that really should be in the JIT's wheelhouse rather than worked around in this code base. Given that Utf8Parser.TryParse (and related APIs int.Parse) are such hot code paths, though, it may be worthwhile to take these code changes anyway while waiting for the JIT's changes to come through.

Overall perf results

MethodToolchainMeanErrorStdDevRatio
TryParseInt32master1,217.5 ns12.02 ns10.03 ns1.00
TryParseInt32thisbranch954.9 ns8.77 ns6.85 ns0.78

Fast-track 'default' formats

Since we expect standardFormat = default to be the common case, it's fast-tracked at the start of the method rather than entering the switch statement. Additionally, entries in the switch statement are normalized to lowercase to minimize the number of if conditions present.

; BEFOREmovzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352f5 (00007ff9`b841a555)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352cd (00007ff9`b841a52d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c352af (00007ff9`b841a50f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c35337 (00007ff9`b841a597)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9b84188c0 (00007ff9`b84188c0); AFTERmovzxeax,r9wtesteax,eaxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)oreax,20hcmpeax,67hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a18 (00007ff9`b843abb8)cmpeax,64hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a3c78a35 (00007ff9`b843abd5)learcx,[rsp+28h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9b8438388 (00007ff9`b8438388)

Repurpose some locals for more efficient register allocation

The answer local is repurposed to hold the running value from the very start of the method, no longer bouncing through num. The check for '+' and '-' is moved out of the common path because those characters should be uncommon.

Avoid expensive instructions

The span is indexed by native int rather than int. This avoids an expensive movsxd instruction in the hot path. Additionally, at the end of the method the imul instruction is removed in favor of cheaper xor and sub instructions.

Finally, the intermediate value resulting from the num - '0' operation is reused as the addend when building up the expression answer = answer * 10 + .... This avoids an expensive three-component lea instruction in the hot path.

; HOT PATH - BEFOREcmpr10d,ecxjae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)movsxdr11,r10dmovzxr11d,byte ptr [rax+r11]leaedi,[r11-30h]cmpedi,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`beef5172 (00007ff9`e303a882)incr10dleaesi,[rsi+rsi*4]leaesi,[r11+rsi*2-30h]; HOT PATH - AFTERcmpr11,rdijae System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)movzxeax,byte ptr [r9+r11]addeax,0FFFFFFD0hcmpeax,9ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParseInt32D(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef)+0xffffffff`befc8605 (00007ff9`e30eac25)incr11leaecx,[rsi+rsi*4]leaesi,[rax+rcx*2]

Benchmark application

publicclassUtf8ParserRunner{privatebyte[][]_intStrings;[GlobalSetup]publicvoidSetup(){// Randomly create 200 strings "19472903" and similar.// Vary the lengths to throw off the branch predictor.Randomrnd=newRandom(0x12345);byte[][]intStrings=newbyte[200][];for(inti=0;i<intStrings.Length;i++){byte[]thisIntString=newbyte[rnd.Next(1,10)];for(intj=0;j<thisIntString.Length;j++){thisIntString[j]=(byte)rnd.Next(0,10);}intStrings[i]=thisIntString;}_intStrings=intStrings;}[Benchmark]publicboolTryParseInt32(){boolretVal=false;byte[][]strs=_intStrings;for(inti=0;i<strs.Length;i++){byte[]str=strs[i];_=str.Length;// deref now to avoid implicit span null checkretVal=Utf8Parser.TryParse(str,outint_,outint_);}returnretVal;}}

Related issues: #28070, #12218, #12402

/cc @layomia

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

BTW I expect this to be a little contentious. And that's ok. Part of this PR is to start a discussion of just how far we should go in managed code to try to eke every last bit of perf out of these code paths. I'm still myself trying to learn what the right balance is. :)

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getting +20% for int parsing is a really great improvement!

Overall looks good to me, the only thing I don't like is replacing clear switch with complex if in TryParse method

return (uint)(i - '0') <= ('9' - '0');
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

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.

do we have to force the inlining of this method? I would assume that JIT is going to do this even without it

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.

BTW I've used https://godbolt.org/z/imxPHx to see if we could use some magic bit operation to get the same effect, but it looks like no ;)

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.

The codegen turns this into three instructions: add, cmp, jcc. I'd say that's fairly efficient. :)

}

value = default;
return TryParseUInt32X(source, out Unsafe.As<int, uint>(ref value), out bytesConsumed);

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.

after moving from the previous switch to this if statement it became much harder to read. Did we get any noticeable performance gain by doing it?

num = source[index];
}
int sign = 0;
IntPtr index = IntPtr.Zero;

@jkotasjkotasFeb 26, 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.

Suggested change
IntPtrindex=IntPtr.Zero;
nintindex=0;

case 'n':
case 'N':
return TryParseInt32N(source, out value, out bytesConsumed);
if (standardFormat == default

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.

This helps, but it would be even better to have overload of the API that does not have the standardFormat argument at all.

@jkotas

Copy link
Copy Markdown
Member

This avoids an expensive movsxd instruction in the hot path

@dotnet/jit-contrib What would it take for the JIT to optimize this out?

@AndyAyersMS

Copy link
Copy Markdown
Member

What would it take for the JIT to optimize this out?

The jit would need to retype index as nint. This is essentially induction variable widening (#7312).

Widening has two aspects, safety and profitability.

For safety the jit would have to prove the code was not relying on any int overflow. The loop handling leading zeros is the tricky part here, initially it appears that index might overflow; the analysis would have to show this is not possible because the span length must be non-negative.

For profitability, the jit would have to look at every expression involving index and cost the computation using both nint and int, then sum over these and see which representation looks better. Ideally this summation would use profile-weighted costs. Here it is probably simple to see nint is cheaper.

@GrabYourPitchforks

GrabYourPitchforks commented Feb 26, 2020

Copy link
Copy Markdown
MemberAuthor

@adamsitnik I extracted rearranging the switch statement out into its own commit. I also further optimized it + improved its readability compared to this PR. See GrabYourPitchforks@b5ea028. Is that a bit cleaner from a maintainability perspective?

From the switch improvements alone:

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1.233 us0.0220 us0.0206 us1.000.00
TryParseInt32utf8parser1.081 us0.0215 us0.0272 us0.890.03

This also further validates @jkotas's theory that introducing a separate overload that doesn't have standardFormat may be worthwhile. Unless we get some kind of mechanism where we can say "hey, JIT, always inline this method into the caller if the standardFormat parameter is a constant." :)

FWIW, in the 'g' and 'd' cases the switch statement jumps straight to the address which sets up parameters for the call site. It doesn't bother evaluating if (standardFormat == default) again because the JIT has already proven this statement to evaluate to true. See codegen below.

; SWITCH STATEMENT - BEFOREpushrdipushrsipushrbxsubrsp,30hxoreax,eaxmov qword ptr [rsp+20h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxebx,r9wcmpebx,4Ehja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65435 (00007ff9`bbb4a695)cmpebx,44hja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c6540d (00007ff9`bbb4a66d)testebx,ebxje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c653ef (00007ff9`bbb4a64f)cmpebx,44hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c65477 (00007ff9`bbb4a6d7)learcx,[rsp+20h]mov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+20h]call CLRStub[MethodDescPrestub]@7ff9bbb47fe8 (00007ff9`bbb47fe8); SWITCH STATEMENT - AFTERpushrdipushrsisubrsp,38hxoreax,eaxmov qword ptr [rsp+28h],raxmovrsi,qword ptr [rcx]movedi,dword ptr [rcx+8]movzxecx,r9wtestecx,ecx ; jne statement immediately after this predicted not taken (common case)jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98958 (00007ff9`bbb5a9b8)learcx,[rsp+28h] ; <-- instruction at 00007ff9`bbb5a99bmov qword ptr [rcx],rsimov dword ptr [rcx+8],edilearcx,[rsp+28h]call CLRStub[MethodDescPrestub]@7ff9bbb58388 (00007ff9`bbb58388)nopaddrsp,38hpoprsipoprdiretmovzxecx,r9worecx,20hcmpecx,67hjg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c98970 (00007ff9`bbb5a9d0)cmpecx,64hje System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)cmpecx,67hjne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c989b4 (00007ff9`bbb5aa14)jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a0c9893b (00007ff9`bbb5a99b)

}

private static bool TryParseInt32D(ReadOnlySpan<byte> source, out int value, out int bytesConsumed)
private static bool TryParseInt32D(in ReadOnlySpan<byte> refToSource, out int value, out int bytesConsumed)

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.

I suspect that the in is deoptimization everywhere else, except Windows x64.

@jkotasjkotasFeb 26, 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.

In other words, passing ReadOnlySpan as in does not sound like a good idea.

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.

I agree this is not a good optimization, and we should back it out if we take this PR. But it's hard to argue with the data that shows minimizing parameter shuffling (see assembly in #32843 (comment)) has a measurable impact on performance.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

And if you reeeeeally wanted to optimize the method with the switch statement, you could use the in keyword to avoid stack spilling the incoming ReadOnlySpan<byte> entirely. This nets another few percentage gain.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,260.8 ns25.00 ns32.50 ns1.000.00
TryParseInt32utf8parser849.4 ns17.01 ns33.18 ns0.690.03
00007ff9`bc68a980 4883ec28 subrsp,28h00007ff9`bc68a984 410fb7c1 movzxeax,r9w00007ff9`bc68a988 85c0 testeax,eax00007ff9`bc68a98a 750b jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8737 (00007ff9`bc68a997)00007ff9`bc68a98c e8f7d9ffff call CLRStub[MethodDescPrestub]@7ff9bc688388 (00007ff9`bc688388)00007ff9`bc68a991 90nop00007ff9`bc68a992 4883c428 addrsp,28h00007ff9`bc68a996 c3 ret00007ff9`bc68a997 410fb7c1 movzxeax,r9w00007ff9`bc68a99b 83c820 oreax,20h00007ff9`bc68a99e 83f867 cmpeax,67h00007ff9`bc68a9a1 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d874f (00007ff9`bc68a9af)00007ff9`bc68a9a3 83f864 cmpeax,64h00007ff9`bc68a9a6 74e4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9a8 83f867 cmpeax,67h00007ff9`bc68a9ab 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9ad ebdd jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d872c (00007ff9`bc68a98c)00007ff9`bc68a9af 83f86e cmpeax,6Eh00007ff9`bc68a9b2 7410je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d8764 (00007ff9`bc68a9c4)00007ff9`bc68a9b4 83f878 cmpeax,78h00007ff9`bc68a9b7 7516jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a12d876f (00007ff9`bc68a9cf)00007ff9`bc68a9b9 e86adaffff call CLRStub[MethodDescPrestub]@7ff9bc688428 (00007ff9`bc688428)00007ff9`bc68a9be 90nop00007ff9`bc68a9bf 4883c428 addrsp,28h00007ff9`bc68a9c3 c3 ret00007ff9`bc68a9c4 e8dfd9ffff call CLRStub[MethodDescPrestub]@7ff9bc6883a8 (00007ff9`bc6883a8)00007ff9`bc68a9c9 90nop00007ff9`bc68a9ca 4883c428 addrsp,28h00007ff9`bc68a9ce c3 ret00007ff9`bc68a9cf e834f5d8ff call CLRStub[MethodDescPrestub]@7ff9bc419f08 (00007ff9`bc419f08)00007ff9`bc68a9d4 cc int3

Of note in the assembly above: rsp is bumped but there's no stack-spillage at all. I don't know why the calls to helper functions aren't tail-calls. Seems like they'd be ideal candidates. The JIT also emits the instruction movzx eax,r9w twice instead of reusing the eax register. That's a minor opportunity for optimization.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@dotnet/jit-contrib I don't feel comfortable using in in this manner because I think it's taking advantage of a Windows-specific calling convention. Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

@BruceForstall

Copy link
Copy Markdown
Contributor

Is there any reason why the existing call sites in this method aren't candidates for tail-calling?

The fail-to-tailcall reason is output in the JitDump, in CLR logging, and in JIT ETW events.

Maybe someone else can eyeball why it's not happening in your case.

@AndyAyersMS

AndyAyersMS commented Feb 26, 2020

Copy link
Copy Markdown
Member

By IL semantics the jit has to copy the struct into a local struct before the call. This blocks tail calling as we're then passing a bit of the local frame to the callee.

There is a special optimization we can do for implicit byrefs when there's just one use of the struct -- we realize this must be the last use so nobody can tell if we pass the original struct or make a copy, so we suppress the copy and then can tail call (edit: we just suppress the copy, we don't do this for tail calls yet...).

But here you have 3 uses and so this simple optimization is defeated. We could probably fix it so we separate out uses from uses at tail call sites, the latter are always last use and so always safe to pass without copying.

This is probably something we could fix relatively quickly; I actually noticed this recently (see this comment) and was prototyping how to fix it. There's an additional wrinkle that makes it a bit trickier than just counting -- we also like to promote multiply used implicit byref params, and we make the promotion decisions before we think about what they might do to tail calls, and if we promote we're back passing local state in the call and so can't tail call.

@adamsitnik

Copy link
Copy Markdown
Member

Is that a bit cleaner from a maintainability perspective?

Yes, thank you @GrabYourPitchforks !

@GrabYourPitchforks

GrabYourPitchforks commented Mar 2, 2020

Copy link
Copy Markdown
MemberAuthor

I was experimenting a bit with @AndyAyersMS's change at #33004. Preliminary investigations show that his change has two positive effects: (a) it provides some good optimizations in its own right, and (b) it provides a solid foundation on which we can build additional improvements.

The numbers and codegen below are from three runs. The master run is the current master branch with no changes. The utf8parser_1 run is from Andy's PR only, with no changes to any of the Utf8Parser code paths. The utf8parser_2 run is from Andy's PR, plus changes to the Utf8Parser.TryParse(..., out int value, ...) method which contains the switch statement. Other methods like TryParseInt32D were left unchanged from their master implementation. Any changes we make to TryParseInt32D and related methods would just be goodness on top of this.

MethodToolchainMeanErrorStdDevRatioRatioSD
TryParseInt32master1,224.1 ns24.13 ns45.33 ns1.000.00
TryParseInt32utf8parser_11,022.3 ns20.25 ns17.95 ns0.840.03
TryParseInt32utf8parser_2733.7 ns13.82 ns11.54 ns0.600.02
; TryParse(..., out int value, ...) in current master branch00007ffe`4c6cb1e0 57pushrdi00007ffe`4c6cb1e1 56pushrsi00007ffe`4c6cb1e2 53pushrbx00007ffe`4c6cb1e3 4883ec30 subrsp,30h00007ffe`4c6cb1e7 33c0 xoreax,eax00007ffe`4c6cb1e9 4889442420mov qword ptr [rsp+20h],rax00007ffe`4c6cb1ee 488b31 movrsi,qword ptr [rcx]00007ffe`4c6cb1f1 8b7908 movedi,dword ptr [rcx+8]00007ffe`4c6cb1f4 410fb7d9 movzxebx,r9w00007ffe`4c6cb1f8 83fb4e cmpebx,4Eh00007ffe`4c6cb1fb 7758ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9355 (00007ffe`4c6cb255)00007ffe`4c6cb1fd 83fb44 cmpebx,44h00007ffe`4c6cb200 772b ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db932d (00007ffe`4c6cb22d)00007ffe`4c6cb202 85db testebx,ebx00007ffe`4c6cb204 7409je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb206 83fb44 cmpebx,44h00007ffe`4c6cb209 0f8588000000 jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb20f 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb214 488931mov qword ptr [rcx],rsi00007ffe`4c6cb217 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb21a 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb21f e89ce3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95c0 (00007ffe`4c6c95c0)00007ffe`4c6cb224 90nop00007ffe`4c6cb225 4883c430 addrsp,30h00007ffe`4c6cb229 5b poprbx00007ffe`4c6cb22a 5e poprsi00007ffe`4c6cb22b 5f poprdi00007ffe`4c6cb22c c3 ret00007ffe`4c6cb22d 83fb47 cmpebx,47h00007ffe`4c6cb230 74dd je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb232 83fb4e cmpebx,4Eh00007ffe`4c6cb235 7560jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb237 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb23c 488931mov qword ptr [rcx],rsi00007ffe`4c6cb23f 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb242 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb247 e894e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c95e0 (00007ffe`4c6c95e0)00007ffe`4c6cb24c 90nop00007ffe`4c6cb24d 4883c430 addrsp,30h00007ffe`4c6cb251 5b poprbx00007ffe`4c6cb252 5e poprsi00007ffe`4c6cb253 5f poprdi00007ffe`4c6cb254 c3 ret00007ffe`4c6cb255 83fb64 cmpebx,64h00007ffe`4c6cb258 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9366 (00007ffe`4c6cb266)00007ffe`4c6cb25a 83fb58 cmpebx,58h00007ffe`4c6cb25d 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9375 (00007ffe`4c6cb275)00007ffe`4c6cb25f 83fb64 cmpebx,64h00007ffe`4c6cb262 7533jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb264 eba9 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb266 83fb67 cmpebx,67h00007ffe`4c6cb269 74a4 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db930f (00007ffe`4c6cb20f)00007ffe`4c6cb26b 83fb6e cmpebx,6Eh00007ffe`4c6cb26e 74c7 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9337 (00007ffe`4c6cb237)00007ffe`4c6cb270 83fb78 cmpebx,78h00007ffe`4c6cb273 7522jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4db9397 (00007ffe`4c6cb297)00007ffe`4c6cb275 33c9 xorecx,ecx00007ffe`4c6cb277 890a mov dword ptr [rdx],ecx00007ffe`4c6cb279 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb27e 488931mov qword ptr [rcx],rsi00007ffe`4c6cb281 897908mov dword ptr [rcx+8],edi00007ffe`4c6cb284 488d4c2420 learcx,[rsp+20h]00007ffe`4c6cb289 e8d2e3ffff call CLRStub[MethodDescPrestub]@7ffe4c6c9660 (00007ffe`4c6c9660)00007ffe`4c6cb28e 90nop00007ffe`4c6cb28f 4883c430 addrsp,30h00007ffe`4c6cb293 5b poprbx00007ffe`4c6cb294 5e poprsi00007ffe`4c6cb295 5f poprdi00007ffe`4c6cb296 c3 ret00007ffe`4c6cb297 33c0 xoreax,eax00007ffe`4c6cb299 8902mov dword ptr [rdx],eax00007ffe`4c6cb29b 418900mov dword ptr [r8],eax00007ffe`4c6cb29e e88deed8ff call CLRStub[MethodDescPrestub]@7ffe4c45a130 (00007ffe`4c45a130)00007ffe`4c6cb2a3 cc int3; TryParse(..., out int value, ...) with Andy's pending PR00007ffe`4c69b320 56pushrsi00007ffe`4c69b321 4883ec20 subrsp,20h00007ffe`4c69b325 410fb7f1 movzxesi,r9w00007ffe`4c69b329 83fe4e cmpesi,4Eh00007ffe`4c69b32c 772c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6948a (00007ffe`4c69b35a)00007ffe`4c69b32e 83fe44 cmpesi,44h00007ffe`4c69b331 7713ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69476 (00007ffe`4c69b346)00007ffe`4c69b333 85f6 testesi,esi00007ffe`4c69b335 7405je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b337 83fe44 cmpesi,44h00007ffe`4c69b33a 754c jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b33c 4883c420 addrsp,20h00007ffe`4c69b340 5e poprsi00007ffe`4c69b341 e9badaffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e00 (00007ffe`4c698e00)00007ffe`4c69b346 83fe47 cmpesi,47h00007ffe`4c69b349 74f1 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b34b 83fe4e cmpesi,4Eh00007ffe`4c69b34e 7538jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b350 4883c420 addrsp,20h00007ffe`4c69b354 5e poprsi00007ffe`4c69b355 e9c6daffff jmp CLRStub[MethodDescPrestub]@7ffe4c698e20 (00007ffe`4c698e20)00007ffe`4c69b35a 83fe64 cmpesi,64h00007ffe`4c69b35d 770c ja System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6949b (00007ffe`4c69b36b)00007ffe`4c69b35f 83fe58 cmpesi,58h00007ffe`4c69b362 7416je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694aa (00007ffe`4c69b37a)00007ffe`4c69b364 83fe64 cmpesi,64h00007ffe`4c69b367 751f jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b369 ebd1 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b36b 83fe67 cmpesi,67h00007ffe`4c69b36e 74cc je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d6946c (00007ffe`4c69b33c)00007ffe`4c69b370 83fe6e cmpesi,6Eh00007ffe`4c69b373 74db je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d69480 (00007ffe`4c69b350)00007ffe`4c69b375 83fe78 cmpesi,78h00007ffe`4c69b378 750e jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4d694b8 (00007ffe`4c69b388)00007ffe`4c69b37a 33c0 xoreax,eax00007ffe`4c69b37c 8902mov dword ptr [rdx],eax00007ffe`4c69b37e 4883c420 addrsp,20h00007ffe`4c69b382 5e poprsi00007ffe`4c69b383 e918dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c698ea0 (00007ffe`4c698ea0)00007ffe`4c69b388 33c0 xoreax,eax00007ffe`4c69b38a 8902mov dword ptr [rdx],eax00007ffe`4c69b38c 418900mov dword ptr [r8],eax00007ffe`4c69b38f e89cedd8ff call CLRStub[MethodDescPrestub]@7ffe4c42a130 (00007ffe`4c42a130)00007ffe`4c69b394 cc int3; TryParse(..., out int value, ...) with Andy's PR and some refactoring of the switch statement00007ffe`4c6db320 410fb7c1 movzxeax,r9w00007ffe`4c6db324 85c0 testeax,eax00007ffe`4c6db326 7505jne System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93cd (00007ffe`4c6db32d)00007ffe`4c6db328 e9d3daffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e00 (00007ffe`4c6d8e00)00007ffe`4c6db32d 410fb7c1 movzxeax,r9w00007ffe`4c6db331 83c820 oreax,20h00007ffe`4c6db334 83f867 cmpeax,67h00007ffe`4c6db337 7f0c jg System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93e5 (00007ffe`4c6db345)00007ffe`4c6db339 83f864 cmpeax,64h00007ffe`4c6db33c 74ea je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db33e 83f867 cmpeax,67h00007ffe`4c6db341 74e5 je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93c8 (00007ffe`4c6db328)00007ffe`4c6db343 eb16 jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db345 83f86e cmpeax,6Eh00007ffe`4c6db348 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f1 (00007ffe`4c6db351)00007ffe`4c6db34a 83f878 cmpeax,78h00007ffe`4c6db34d 7407je System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93f6 (00007ffe`4c6db356)00007ffe`4c6db34f eb0a jmp System_Private_CoreLib!System.Buffers.Text.Utf8Parser.TryParse(System.ReadOnlySpan`1<Byte>, Int32 ByRef, Int32 ByRef, Char)+0xffffffff`a4da93fb (00007ffe`4c6db35b)00007ffe`4c6db351 e9cadaffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8e20 (00007ffe`4c6d8e20)00007ffe`4c6db356 e945dbffff jmp CLRStub[MethodDescPrestub]@7ffe4c6d8ea0 (00007ffe`4c6d8ea0)00007ffe`4c6db35b e980f3ffff jmp CLRStub[MethodDescPrestub]@7ffe4c6da6e0 (00007ffe`4c6da6e0)

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

With latest changes, Utf8Parser.TryParse(..., out int) runs at around 10% faster than baseline (master branch). int.Parse runs at around 5% faster than baseline.

I'm still investigating whether we can eke more performance out of int.Parse to try to get it into double-digits percentage gain. At only 5% I question as to whether the added complexity of the current PR is worth it.

/// Otherwise returns <see langword="false"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe bool TryGetElementAt(nuint index, [MaybeNullWhen(false)] out T value)

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.

I am not fan of improving performance by internal helpers like this one that are not available in public surface. This should be either turned into public API, or we should live without it.

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.

@jkotas I understand the concern. How about I experiment in a separate branch with plumbing it through other call sites and see what the perf impact is? That should give us evidence that this API is (or isn't) generally worthwhile.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Closing this PR since it's no longer actively being worked on. Opened GrabYourPitchforks#9 so that I don't lose track of it.

@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.Memorytenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@GrabYourPitchforks@jkotas@AndyAyersMS@BruceForstall@adamsitnik@ycrumeyrolle@Dotnet-GitSync-Bot