Uh oh!
There was an error while loading. Please reload this page.
Optimize System.HexConverter.IsHexChar on 64 bits - #52470
Conversation
Add a branchless fast path on 64 bit systems that doesn't do memory accesses either
ghost
commented
May 7, 2021
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
| public static bool IsHexChar(int c) | ||
| public static unsafe bool IsHexChar(int c) | ||
| { | ||
| if (sizeof(IntPtr) == 8) |
There was a problem hiding this comment.
since corelib is always arch specific I guess it's better (e.g. for JIT) to use #if TARGET_64BIT here
There was a problem hiding this comment.
This is in the Common folder and is pulled in by a bunch of different libs, some of which only build AnyCPU. If we wanted an ifdef for the target platform, we'd also need to ifdef CORELIB. (See ValueStringBuilder.cs for some examples.)
| shift = unchecked((int)((35465847073801215UL >> i) & 1)), | ||
| and = shift & mask; | ||
| byte result = unchecked((byte)and); | ||
| bool valid = *(bool*)&result; |
There was a problem hiding this comment.
We considered similar tricks to this a while back and those tricks were rejected. See dotnet/coreclr#16138 for one example.
The tl;dr was "somebody's probably branching on the result of this method, so it's better if the final statement is an actual comparison that can be folded into the caller's branch condition."
The implication would be that these lines become return (and & 1) != 0;, or return (and & 1) != 0 ? true : false; if we're trying to work around #4207.
There was a problem hiding this comment.
That makes sense, I was thinking the last return expression could be changed to that due to callers branching 🙂
Question though: given the input is guaranteed to be either 1 or 0 here due to the previous logic, we can just test and != 0 here, right? As in, we can skip that & 1 since that wouldn't affect the actual semantics in this case, no?
So at the end of the day:
returnand!=0?true:false;Should work?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Nit: should read ['0', '0' + 64).
There was a problem hiding this comment.
I'm confused, aren't we checking that c is also >= 0 here?
From your original snippet on Discord:
ulongmask=i-64;// c is negative IFF '0' <= c < '0' + 64; else c is non-negativeAm I mixing things up here? 🤔
There was a problem hiding this comment.
If c is within [0, '0'), bit 31 will be set, but bit 63 (the bit we care about for masking purposes) will not be set.
Bit 63 only gets set if c is within ['0', '0' + 64).
There was a problem hiding this comment.
Oh right, yeah. Fixed, thanks! 😄
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Remove unsafe keyword; no longer needed.
There was a problem hiding this comment.
See comment below, done in 78bc559532402d2d4dd1a2e9a85b9e9efd4240a5.
There was a problem hiding this comment.
Prefer IntPtr.Size rather than sizeof(IntPtr) here. illink can see this as a proper method call to IntPtr.get_Size and will perform substitution. See, e.g., https://github.com/dotnet/runtime/blob/74cafe71de68fac46cd4956da98387bce9e1043c/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.64bit.xml.
There was a problem hiding this comment.
Oh, TIL. Done in 78bc559532402d2d4dd1a2e9a85b9e9efd4240a5 🙂
GrabYourPitchforks
left a comment
There was a problem hiding this comment.
Thanks! Appreciate the perf numbers in the issue as well.
ghost
commented
May 7, 2021
Tagging subscribers to this area: @tannergooding Issue DetailsOverviewThis PR adds a fast path to
The change is a specialized version of what I used in Codegen diffBefore (click to expand):; Method IsHexCharFast.HexConverter2:IsHexChar_OG(int):boolG_M3768_IG01:subrsp,40 ;; bbWeight=1 PerfScore 0.25G_M3768_IG02:cmpecx,256jge SHORT G_M3768_IG04 ;; bbWeight=1 PerfScore 1.25G_M3768_IG03:cmpecx,256jae SHORT G_M3768_IG07movsxdrax,ecxmovrdx,0xD1FFAB1Emovzxrax, byte ptr [rax+rdx]jmp SHORT G_M3768_IG05 ;; bbWeight=0.50 PerfScore 2.88G_M3768_IG04:moveax,255 ;; bbWeight=0.50 PerfScore 0.12G_M3768_IG05:cmpeax,255 setne almovzxrax,al ;; bbWeight=1 PerfScore 1.50G_M3768_IG06:addrsp,40ret ;; bbWeight=1 PerfScore 1.25G_M3768_IG07:call CORINFO_HELP_RNGCHKFAILint3 ;; bbWeight=0 PerfScore 0.00; Total bytes of code: 66After (click to expand):; Method IsHexCharFast.HexConverter2:IsHexChar(int):boolG_M2063_IG01: ;; bbWeight=1 PerfScore 0.00G_M2063_IG02:addecx,-48learax,[rcx-64]movrdx,0xD1FFAB1Eshlrdx,clandrax,rdxjl SHORT G_M2063_IG05 ;; bbWeight=1 PerfScore 4.25G_M2063_IG03:xoreax,eax ;; bbWeight=0.50 PerfScore 0.12G_M2063_IG04:ret ;; bbWeight=0.50 PerfScore 0.50G_M2063_IG05:moveax,1 ;; bbWeight=0.50 PerfScore 0.12G_M2063_IG06:ret ;; bbWeight=0.50 PerfScore 0.50; Total bytes of code: 34Additionally, the new version can also be JITted to just a constant, if the input is a constant. Before (click to expand):; Method IsHexCharFast.HexConverter2:Check_Constant_OG():boolG_M36347_IG01: ;; bbWeight=1 PerfScore 0.00G_M36347_IG02:movrax,0xD1FFAB1Emovzxrax, byte ptr [rax]cmpeax,255 setne almovzxrax,al ;; bbWeight=1 PerfScore 3.75G_M36347_IG03:ret ;; bbWeight=1 PerfScore 1.00; Total bytes of code: 25After (click to expand):; Method IsHexCharFast.HexConverter2:Check_Constant_NEW():boolG_M50831_IG01: ;; bbWeight=1 PerfScore 0.00G_M50831_IG02:moveax,1 ;; bbWeight=1 PerfScore 0.25G_M50831_IG03:ret ;; bbWeight=1 PerfScore 1.00; Total bytes of code: 6BenchmarkI've put together a small test benchmark which you can find here.
The new version is about 2x faster over random data in this test. When the input is always valid and the branch predictor can be more effective in the original implementation, this test shows the new version is still on par, but a couple notes:
JIT diffCurrently work in progress, I'm not having luck with the runtime tooling today... 😄
|
GrabYourPitchforks
commented
May 9, 2021
@EgorBo@tannergooding any other thoughts on this? Thinking of merging it in Tuesday, which gives a little while longer for comments. |
danmoseley
commented
May 11, 2021
Nice contribution @Sergio0694 thank you. |
Overview
This PR adds a fast path to
System.HexConverter.IsHexChar(int)on 64 bit systems, which has:The change is a specialized version of what I used in
BitHelper.HasLookupFlagin theMicrosoft.Toolkit.HighPerformancepackage, and just uses bit trickery to make the code branchless and read the lookup value from a constant value and not from memory.Codegen diff
Before (click to expand):
After (click to expand):
Additionally, the new version can also be JITted to just a constant, if the input is a constant.
That is, if you call
IsHexCharwith an input constant like'A', you get this JIT diff:Before (click to expand):
After (click to expand):
Benchmark
I've put together a small test benchmark which you can find here.
The new version is about 2x faster over random data in this test. When the input is always valid and the branch predictor can be more effective in the original implementation, this test shows the new version is still on par, but a couple notes:
JIT diff
Currently work in progress, I'm not having luck with the runtime tooling today... 😄
Opened the PR in the meantime to have the CI run on it at least.