Skip to content

try to port ASCIIUtility.WidenAsciiToUtf16 to x-plat intrinsics - #73055

Merged
adamsitnik merged 16 commits into
dotnet:mainfrom
adamsitnik:WidenAsciiToUtf16
Sep 9, 2022
Merged

try to port ASCIIUtility.WidenAsciiToUtf16 to x-plat intrinsics#73055
adamsitnik merged 16 commits into
dotnet:mainfrom
adamsitnik:WidenAsciiToUtf16

Conversation

@adamsitnik

@adamsitnikadamsitnik commented Jul 29, 2022

Copy link
Copy Markdown
Member

EDIT: for updated perf numbers please go to #73055 (comment)

x64

Initially, there was a major regression, but I was able to solve it by enforcing the inlining of Vector128.Widen(Vector128<byte>) . After porting everything the new implementation was on par:

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=Windows 11 (10.0.22000.795/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-preview.6.22352.1
[Host] : .NET 7.0.0 (7.0.22.32404), X64 RyuJIT AVX2
Job-IVMYPN : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-BJNCCX : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT AVX2
TypeMethodJobsizeencNameInputMeanRatio
Perf_EncodingGetStringPR16ascii?22.09 ns0.97
Perf_EncodingGetStringbase16ascii?22.72 ns1.00
Perf_EncodingGetStringPR16utf-8?20.37 ns0.99
Perf_EncodingGetStringbase16utf-8?20.55 ns1.00
Perf_EncodingGetStringPR512ascii?70.05 ns0.93
Perf_EncodingGetStringbase512ascii?75.12 ns1.00
Perf_EncodingGetStringPR512utf-8?77.35 ns1.00
Perf_EncodingGetStringbase512utf-8?78.87 ns1.00
Perf_Utf8EncodingGetStringPR??EnglishAllAscii20,990.29 ns0.99
Perf_Utf8EncodingGetStringbase??EnglishAllAscii21,203.47 ns1.00
Perf_Utf8EncodingGetStringPR??EnglishMostlyAscii125,617.25 ns0.99
Perf_Utf8EncodingGetStringbase??EnglishMostlyAscii126,595.44 ns1.00
Perf_Utf8EncodingGetStringPR??Chinese156,988.65 ns1.00
Perf_Utf8EncodingGetStringbase??Chinese156,257.20 ns1.00
Perf_Utf8EncodingGetStringPR??Cyrillic155,448.25 ns1.00
Perf_Utf8EncodingGetStringbase??Cyrillic155,961.57 ns1.00
Perf_Utf8EncodingGetStringPR??Greek244,318.02 ns1.00
Perf_Utf8EncodingGetStringbase??Greek244,570.33 ns1.00

With some additional optimizations and adding Vector256 code path it's now on par or up to 20% faster, depending on input (the more characters are ascii, the better).

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=Windows 11 (10.0.22000.795/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-preview.6.22352.1
[Host] : .NET 7.0.0 (7.0.22.32404), X64 RyuJIT AVX2
Method=GetString
TypeJobsizeencNameInputMeanRatio
Perf_EncodingPR16ascii?22.22 ns0.99
Perf_Encodingmain16ascii?22.56 ns1.00
Perf_EncodingPR16utf-8?20.19 ns0.94
Perf_Encodingmain16utf-8?21.42 ns1.00
Perf_EncodingPR512ascii?57.58 ns0.77
Perf_Encodingmain512ascii?74.38 ns1.00
Perf_EncodingPR512utf-8?67.43 ns0.79
Perf_Encodingmain512utf-8?84.91 ns1.00
Perf_Utf8EncodingPR??EnglishAllAscii20,610.46 ns0.98
Perf_Utf8Encodingmain??EnglishAllAscii21,046.16 ns1.00
Perf_Utf8EncodingPR??EnglishMostlyAscii129,464.76 ns1.00
Perf_Utf8Encodingmain??EnglishMostlyAscii128,755.26 ns1.00
Perf_Utf8EncodingPR??Chinese157,597.33 ns1.03
Perf_Utf8Encodingmain??Chinese153,599.10 ns1.00
Perf_Utf8EncodingPR??Cyrillic151,221.52 ns0.98
Perf_Utf8Encodingmain??Cyrillic153,741.42 ns1.00
Perf_Utf8EncodingPR??Greek245,325.88 ns1.01
Perf_Utf8Encodingmain??Greek243,378.47 ns1.00

ARM64

Initially, after just mapping the code there was a major regression of 40-50%:

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=ubuntu 20.04
Unknown processor
.NET SDK=7.0.100-rc.1.22378.8
[Host] : .NET 7.0.0 (7.0.22.37802), Arm64 RyuJIT AdvSIMD
Job-VUTCOY : .NET 7.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
Job-LKXRPH : .NET 7.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
TypeMethodToolchainsizeencNameInputMeanRatio
Perf_EncodingGetString/7.0.0/corerun16ascii?90.41 ns1.02
Perf_EncodingGetString/main/corerun16ascii?88.37 ns1.00
Perf_EncodingGetString/7.0.0/corerun16utf-8?81.14 ns1.00
Perf_EncodingGetString/main/corerun16utf-8?81.43 ns1.00
Perf_EncodingGetString/7.0.0/corerun512ascii?376.65 ns1.55
Perf_EncodingGetString/main/corerun512ascii?242.93 ns1.00
Perf_EncodingGetString/7.0.0/corerun512utf-8?435.39 ns1.43
Perf_EncodingGetString/main/corerun512utf-8?305.42 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??EnglishAllAscii142,873.52 ns1.39
Perf_Utf8EncodingGetString/main/corerun??EnglishAllAscii102,949.86 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??EnglishMostlyAscii267,682.01 ns1.04
Perf_Utf8EncodingGetString/main/corerun??EnglishMostlyAscii256,662.77 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Chinese372,408.90 ns0.99
Perf_Utf8EncodingGetString/main/corerun??Chinese376,872.54 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Cyrillic277,097.47 ns1.00
Perf_Utf8EncodingGetString/main/corerun??Cyrillic275,724.07 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Greek418,494.63 ns1.01
Perf_Utf8EncodingGetString/main/corerun??Greek416,134.26 ns1.00

After I replaced Vector128.Widen with Vector128.WidenLower and Vector128.WidenUpper, I was able to lower the regression to 10-16%

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=ubuntu 20.04
Unknown processor
.NET SDK=7.0.100-rc.1.22379.1
[Host] : .NET 7.0.0 (7.0.22.37802), Arm64 RyuJIT AdvSIMD
Method=GetString
TypeJobsizeencNameInputMeanRatio
Perf_EncodingPR16ascii?91.36 ns1.02
Perf_Encodingmain16ascii?89.58 ns1.00
Perf_EncodingPR16utf-8?81.10 ns0.97
Perf_Encodingmain16utf-8?83.81 ns1.00
Perf_EncodingPR512ascii?282.59 ns1.16
Perf_Encodingmain512ascii?244.48 ns1.00
Perf_EncodingPR512utf-8?337.99 ns1.10
Perf_Encodingmain512utf-8?308.56 ns1.00
Perf_Utf8EncodingPR??EnglishAllAscii114,550.43 ns1.13
Perf_Utf8Encodingmain??EnglishAllAscii101,123.74 ns1.00
Perf_Utf8EncodingPR??EnglishMostlyAscii270,465.07 ns1.06
Perf_Utf8Encodingmain??EnglishMostlyAscii254,863.02 ns1.00
Perf_Utf8EncodingPR??Chinese370,954.69 ns0.97
Perf_Utf8Encodingmain??Chinese382,811.25 ns1.00
Perf_Utf8EncodingPR??Cyrillic278,273.22 ns1.01
Perf_Utf8Encodingmain??Cyrillic275,921.61 ns1.00
Perf_Utf8EncodingPR??Greek418,066.91 ns1.01
Perf_Utf8Encodingmain??Greek415,772.69 ns1.00

I am trying to update my Surface Pro X to Win 11, which will allow me to install VS 2022 (ARM64), build dotnet/runtime and try the new internal MS profiler. But I can't promise anything.

contributes to #64451

cc @tannergooding@GrabYourPitchforks

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-text-encoding
See info in area-owners.md if you want to be subscribed.

Issue Details

x64

There was a major regression, but I was able to solve it by enforcing the inlining of Vector128.Widen(Vector128<byte>) . The new implementation is now on par.

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=Windows 11 (10.0.22000.795/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-preview.6.22352.1
[Host] : .NET 7.0.0 (7.0.22.32404), X64 RyuJIT AVX2
Job-IVMYPN : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-BJNCCX : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT AVX2
TypeMethodJobsizeencNameInputMeanRatio
Perf_EncodingGetStringPR16ascii?22.09 ns0.97
Perf_EncodingGetStringbase16ascii?22.72 ns1.00
Perf_EncodingGetStringPR16utf-8?20.37 ns0.99
Perf_EncodingGetStringbase16utf-8?20.55 ns1.00
Perf_EncodingGetStringPR512ascii?70.05 ns0.93
Perf_EncodingGetStringbase512ascii?75.12 ns1.00
Perf_EncodingGetStringPR512utf-8?77.35 ns1.00
Perf_EncodingGetStringbase512utf-8?78.87 ns1.00
Perf_Utf8EncodingGetStringPR??EnglishAllAscii20,990.29 ns0.99
Perf_Utf8EncodingGetStringbase??EnglishAllAscii21,203.47 ns1.00
Perf_Utf8EncodingGetStringPR??EnglishMostlyAscii125,617.25 ns0.99
Perf_Utf8EncodingGetStringbase??EnglishMostlyAscii126,595.44 ns1.00
Perf_Utf8EncodingGetStringPR??Chinese156,988.65 ns1.00
Perf_Utf8EncodingGetStringbase??Chinese156,257.20 ns1.00
Perf_Utf8EncodingGetStringPR??Cyrillic155,448.25 ns1.00
Perf_Utf8EncodingGetStringbase??Cyrillic155,961.57 ns1.00
Perf_Utf8EncodingGetStringPR??Greek244,318.02 ns1.00
Perf_Utf8EncodingGetStringbase??Greek244,570.33 ns1.00

ARM64

There is a major perf regression: 40-50%. I know that it's not caused by the ContainsNonAsciiByte changes, I suspect that ARMs Vector128.Widen* implementations are just suboptimal. I am just guessing because I currently can't get the ARM64 disassembly with profile information.

I am trying to update my Surface Pro X to Win 11, which will allow me to install VS 2022 (ARM64), build dotnet/runtime and try the new internal MS profiler. But I can't promise anything.

Details
BenchmarkDotNet=v0.13.1.1828-nightly, OS=ubuntu 20.04
Unknown processor
.NET SDK=7.0.100-rc.1.22378.8
[Host] : .NET 7.0.0 (7.0.22.37802), Arm64 RyuJIT AdvSIMD
Job-VUTCOY : .NET 7.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
Job-LKXRPH : .NET 7.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
TypeMethodToolchainsizeencNameInputMeanRatio
Perf_EncodingGetString/7.0.0/corerun16ascii?90.41 ns1.02
Perf_EncodingGetString/main/corerun16ascii?88.37 ns1.00
Perf_EncodingGetString/7.0.0/corerun16utf-8?81.14 ns1.00
Perf_EncodingGetString/main/corerun16utf-8?81.43 ns1.00
Perf_EncodingGetString/7.0.0/corerun512ascii?376.65 ns1.55
Perf_EncodingGetString/main/corerun512ascii?242.93 ns1.00
Perf_EncodingGetString/7.0.0/corerun512utf-8?435.39 ns1.43
Perf_EncodingGetString/main/corerun512utf-8?305.42 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??EnglishAllAscii142,873.52 ns1.39
Perf_Utf8EncodingGetString/main/corerun??EnglishAllAscii102,949.86 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??EnglishMostlyAscii267,682.01 ns1.04
Perf_Utf8EncodingGetString/main/corerun??EnglishMostlyAscii256,662.77 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Chinese372,408.90 ns0.99
Perf_Utf8EncodingGetString/main/corerun??Chinese376,872.54 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Cyrillic277,097.47 ns1.00
Perf_Utf8EncodingGetString/main/corerun??Cyrillic275,724.07 ns1.00
Perf_Utf8EncodingGetString/7.0.0/corerun??Greek418,494.63 ns1.01
Perf_Utf8EncodingGetString/main/corerun??Greek416,134.26 ns1.00

contributes to #64451

cc @tannergooding@GrabYourPitchforks

Author:adamsitnik
Assignees:-
Labels:

area-System.Text.Encoding

Milestone:-

/// <param name="source">The vector whose elements are to be widened.</param>
/// <returns>A pair of vectors that contain the widened lower and upper halves of <paramref name="source" />.</returns>
[CLSCompliant(false)]
[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.

We should add this to all the Widen APIs and to the same on Vector64.

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.

Can the JIT use named intrinsic and treat all methods in Vector###<T> as candidates for aggressive inlining?

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.

@tannergooding I've synced my fork with upstream and verified that it's not needed anymore. So we don't need to backport anything to 7.0

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Text/ASCIIUtility.cs Outdated
@adamsitnik

Copy link
Copy Markdown
MemberAuthor

I added Vector256 code path and got it up to 20% faster on x64.

I replaced Vector128.Widen with Vector128.WidenLower and Vector128.WidenUpper and got the ARM64 regression down to 10-16% (from 40-50%).

I updated the results posted above.


// Can we at least widen the first part of the vector?

if (!containsNonAsciiBytes)

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've removed this code, as it was impossible to satisfy this requirement as the jump was performed only when the flag was set to true:

if(containsNonAsciiBytes)
{
// non-ASCII byte somewhere
goto NonAsciiDataSeenInInnerLoop;
}

pCurrentWriteAddress += (nuint)Vector128<byte>.Count;
} while (currentOffset <= finalOffsetWhereCanRunLoop);

// Calculate how many elements we wrote in order to get pOutputBuffer to its next alignment

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.

based on the benchmarking that I've done this was not improving perf in noticeable way, but increasing the code complexity. I've removed it and added Vector256 code path (less code = less code to duplicate)

@adamsitnik

adamsitnik commented Aug 19, 2022

Copy link
Copy Markdown
MemberAuthor

Edit: it turned out to be a R2R bug: #74253

OK, I have no idea what is going on and I need an extra pair of eyes.

The tests are failing with Encountered infinite recursion while looking up resource 'Word_At' in System.Private.CoreLib.:

The callstack is quite long, but it shows that failure starts from WidenAsciiToUtf16_Vector256 method which I've just added:

 at System.Diagnostics.Debug.Fail(System.String, System.String)
at System.Text.ASCIIUtility.WidenAsciiToUtf16_Vector256(Byte*, Char*, UIntPtr)
at System.Text.ASCIIUtility.WidenAsciiToUtf16(Byte*, Char*, UIntPtr)

This method has 3 debug asserts:

private static unsafe nuint WidenAsciiToUtf16_Vector256(byte*pAsciiBuffer,char*pUtf16Buffer,nuintelementCount)
{
Debug.Assert(Vector256.IsHardwareAccelerated);
Debug.Assert(BitConverter.IsLittleEndian);
Debug.Assert(elementCount>=2*(uint)Vector256<byte>.Count);

And the only place where it's called from has exactly the same guards:

if(BitConverter.IsLittleEndian&&Vector256.IsHardwareAccelerated&&elementCount>=2*(uint)Vector256<byte>.Count)
{
currentOffset = WidenAsciiToUtf16_Vector256(pAsciiBuffer,pUtf16Buffer,elementCount);
}

So the asserts mentioned above should definitely not fail.

I was able to reproduce the failure locally. It's gone when I remove those 3 asserts! What am I missing?

@danmoseley

Copy link
Copy Markdown
Contributor

Http test crash. Worth cracking the dump to check it's not related to this change?

@davidwrighton

Copy link
Copy Markdown
Member

@adamsitnik please, don't just comment that assert out. The generated code will not reliably behave correctly, you need to follow the instructions I put in #74253

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@adamsitnik please, don't just comment that assert out. The generated code will not reliably behave correctly, you need to follow the instructions I put in #74253

@davidwrighton thank you!

btw based on the perf results I decided to simply inline these two helpers

Details
MethodToolchainsizeencNameMeanRatio
GetString\helpers\corerun.exe8ascii18.53 ns0.88
GetString\inlined\corerun.exe8ascii17.47 ns0.83
GetString\main\corerun.exe8ascii21.16 ns1.00
GetString\helpers\corerun.exe8utf-816.84 ns0.80
GetString\inlined\corerun.exe8utf-815.96 ns0.76
GetString\main\corerun.exe8utf-820.96 ns1.00
GetString\helpers\corerun.exe16ascii24.87 ns0.83
GetString\inlined\corerun.exe16ascii18.32 ns0.61
GetString\main\corerun.exe16ascii30.11 ns1.00
GetString\helpers\corerun.exe16utf-818.20 ns0.77
GetString\inlined\corerun.exe16utf-816.27 ns0.69
GetString\main\corerun.exe16utf-823.71 ns1.00
GetString\helpers\corerun.exe32ascii18.50 ns0.61
GetString\inlined\corerun.exe32ascii17.07 ns0.56
GetString\main\corerun.exe32ascii30.56 ns1.00
GetString\helpers\corerun.exe32utf-820.95 ns0.84
GetString\inlined\corerun.exe32utf-818.00 ns0.72
GetString\main\corerun.exe32utf-824.88 ns1.00
GetString\helpers\corerun.exe64ascii21.41 ns0.60
GetString\inlined\corerun.exe64ascii20.59 ns0.58
GetString\main\corerun.exe64ascii35.69 ns1.00
GetString\helpers\corerun.exe64utf-830.83 ns1.03
GetString\inlined\corerun.exe64utf-828.59 ns0.96
GetString\main\corerun.exe64utf-829.89 ns1.00
GetString\helpers\corerun.exe512ascii55.39 ns0.72
GetString\inlined\corerun.exe512ascii53.79 ns0.70
GetString\main\corerun.exe512ascii77.35 ns1.00
GetString\helpers\corerun.exe512utf-871.63 ns0.89
GetString\inlined\corerun.exe512utf-869.81 ns0.87
GetString\main\corerun.exe512utf-880.38 ns1.00

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

Updated perf numbers:

For size==16 we can observe gain for both x64 and arm64. It's caused by executing the vectorized code path now (previously the buffer needed to be at least double of Vector128<byte>.Count (32)).

x64 AVX2

There is a 10-30% boost for large inputs. It's caused by adding the Vector256 code path.

Small inputs also work faster, partially because WidenFourAsciiBytesToUtf16AndWriteToBuffer is producing better code gen now. I am afraid that some of these gains are caused by code alignment changes (the benchmarks themselves were run with memory randomization enabled, so we can exclude memory alignment from the list).

Details
BenchmarkDotNet=v0.13.1.20220823-develop, OS=Windows 11 (10.0.22000.856/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-preview.7.22377.5
[Host] : .NET 7.0.0 (7.0.22.37506), X64 RyuJIT AVX2
Job-SJSFRM : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-QAHUTQ : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=9 MemoryRandomization=True
MethodToolchainsizeencNameMeanRatio
GetString\main\corerun.exe8ascii24.70 ns1.00
GetString\pr\corerun.exe8ascii16.86 ns0.69
GetString\main\corerun.exe8utf-819.65 ns1.00
GetString\pr\corerun.exe8utf-816.48 ns0.84
GetString\main\corerun.exe16ascii27.84 ns1.00
GetString\pr\corerun.exe16ascii18.36 ns0.66
GetString\main\corerun.exe16utf-821.78 ns1.00
GetString\pr\corerun.exe16utf-817.26 ns0.79
GetString\main\corerun.exe32ascii30.05 ns1.00
GetString\pr\corerun.exe32ascii17.09 ns0.57
GetString\main\corerun.exe32utf-823.42 ns1.00
GetString\pr\corerun.exe32utf-817.59 ns0.75
GetString\main\corerun.exe64ascii33.73 ns1.00
GetString\pr\corerun.exe64ascii20.59 ns0.61
GetString\main\corerun.exe64utf-826.96 ns1.00
GetString\pr\corerun.exe64utf-827.51 ns1.02
GetString\main\corerun.exe512ascii75.27 ns1.00
GetString\pr\corerun.exe512ascii52.61 ns0.70
GetString\main\corerun.exe512utf-876.35 ns1.00
GetString\pr\corerun.exe512utf-867.73 ns0.89

ARM64 AdvSIMD

There is a small (4-8%) gain for all test cases.

Details
BenchmarkDotNet=v0.13.1.1845-nightly, OS=Windows 11 (10.0.22622.575)
Microsoft SQ1 3.0 GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK=7.0.100-rc.2.22422.7
[Host] : .NET 7.0.0 (7.0.22.41112), Arm64 RyuJIT AdvSIMD
Job-AHYKGY : .NET 8.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
Job-THHHUV : .NET 8.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
LaunchCount=9 MemoryRandomization=True
MethodToolchainsizeencNameMeanRatio
GetString\main\corerun.exe8ascii29.61 ns1.00
GetString\pr\corerun.exe8ascii28.39 ns0.96
GetString\main\corerun.exe8utf-829.42 ns1.00
GetString\pr\corerun.exe8utf-828.39 ns0.96
GetString\main\corerun.exe16ascii33.19 ns1.00
GetString\pr\corerun.exe16ascii30.45 ns0.92
GetString\main\corerun.exe16utf-833.15 ns1.00
GetString\pr\corerun.exe16utf-829.42 ns0.89
GetString\main\corerun.exe32ascii39.20 ns1.00
GetString\pr\corerun.exe32ascii34.30 ns0.88
GetString\main\corerun.exe32utf-837.09 ns1.00
GetString\pr\corerun.exe32utf-833.67 ns0.91
GetString\main\corerun.exe64ascii47.49 ns1.00
GetString\pr\corerun.exe64ascii41.39 ns0.87
GetString\main\corerun.exe64utf-850.62 ns1.00
GetString\pr\corerun.exe64utf-844.74 ns0.89
GetString\main\corerun.exe512ascii162.25 ns1.00
GetString\pr\corerun.exe512ascii148.39 ns0.92
GetString\main\corerun.exe512utf-8199.57 ns1.00
GetString\pr\corerun.exe512utf-8182.53 ns0.92

{
Vector256<byte> asciiVector = Vector256.Load(pAsciiBuffer + currentOffset);

if (asciiVector.ExtractMostSignificantBits() != 0)

@stephentoubstephentoubSep 8, 2022

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.

https://github.com/dotnet/runtime/pull/73055/files?diff=split&w=1#diff-66bbe89271f826c9232bd146abb678844754515dc027f70ad0ce36f751da46ebR1378 suggests that Sse41.TestZ is faster than ExtractMostSignificantBits for 128 bits. Does the same not hold for Avx.TestZ for 256 bits?

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.

It does not:

Details
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem.Reflection.Metadata;usingSystem.Runtime.InteropServices;usingSystem.Runtime.Intrinsics;usingSystem.Runtime.Intrinsics.X86;namespaceVectorBenchmarks{internalclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);}publicunsafeclassContainsNonAscii{privateconstintSize=1024;privatebyte*_bytes;[GlobalSetup]publicvoidSetup(){_bytes=(byte*)NativeMemory.AlignedAlloc(newUIntPtr(Size),newUIntPtr(32));newSpan<byte>(_bytes,Size).Clear();}[GlobalCleanup]publicvoidFree()=>NativeMemory.AlignedFree(_bytes);[Benchmark]publicboolExtractMostSignificantBits(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector256<byte>.Count;do{Vector256<byte>asciiVector=Vector256.LoadUnsafe(refsearchSpace,currentOffset);if(asciiVector.ExtractMostSignificantBits()!=0){returntrue;}currentOffset+=(nuint)Vector256<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}[Benchmark]publicboolTestZ(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector256<byte>.Count;do{Vector256<byte>asciiVector=Vector256.LoadUnsafe(refsearchSpace,currentOffset);if(!Avx.TestZ(asciiVector,Vector256.Create((byte)0x80))){returntrue;}currentOffset+=(nuint)Vector256<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}}}
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.856/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-rc.1.22423.16
[Host] : .NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2
MethodMeanErrorStdDevCode Size
ExtractMostSignificantBits11.78 ns0.042 ns0.040 ns57 B
TestZ14.76 ns0.320 ns0.416 ns68 B

.NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2

; VectorBenchmarks.ContainsNonAscii.ExtractMostSignificantBits()vzerouppermovrax,[rcx+8]xoredx,edxnop dword ptr [rax]M00_L00: vmovdqu ymm0,ymmword ptr [rax+rdx] vpmovmskb ecx,ymm0testecx,ecxjne short M00_L01addrdx,20cmprdx,3E0jbe short M00_L00xoreax,eaxvzeroupperretM00_L01:moveax,1vzeroupperret; Total bytes of code 57

.NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2

; VectorBenchmarks.ContainsNonAscii.TestZ()vzerouppermovrax,[rcx+8]xoredx,edxvmovupdymm0,[7FF9E3D94D60]nop dword ptr [rax]nop dword ptr [rax]M00_L00:vptestymm0,ymmword ptr [rax+rdx]jne short M00_L01addrdx,20cmprdx,3E0jbe short M00_L00xoreax,eaxvzeroupperretM00_L01:moveax,1vzeroupperret; Total bytes of code 68

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.

It does not:

That's not what I see on my machine.

MethodMean
ExtractMostSignificantBits_12831.77 ns
TestZ_12825.58 ns
ExtractMostSignificantBits_25615.58 ns
TestZ_25611.66 ns
Details
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Runtime.InteropServices;usingSystem.Runtime.Intrinsics.X86;usingSystem.Runtime.Intrinsics;[HideColumns("Error","StdDev","Median","RatioSD")]publicunsafepartialclassProgram{staticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);privateconstintSize=1024;privatebyte*_bytes;[GlobalSetup]publicvoidSetup(){_bytes=(byte*)NativeMemory.AlignedAlloc(newUIntPtr(Size),newUIntPtr(32));newSpan<byte>(_bytes,Size).Clear();}[GlobalCleanup]publicvoidFree()=>NativeMemory.AlignedFree(_bytes);[Benchmark]publicboolExtractMostSignificantBits_128(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector128<byte>.Count;do{Vector128<byte>asciiVector=Vector128.LoadUnsafe(refsearchSpace,currentOffset);if(asciiVector.ExtractMostSignificantBits()!=0){returntrue;}currentOffset+=(nuint)Vector128<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}[Benchmark]publicboolTestZ_128(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector128<byte>.Count;do{Vector128<byte>asciiVector=Vector128.LoadUnsafe(refsearchSpace,currentOffset);if(!Sse41.TestZ(asciiVector,Vector128.Create((byte)0x80))){returntrue;}currentOffset+=(nuint)Vector128<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}[Benchmark]publicboolExtractMostSignificantBits_256(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector256<byte>.Count;do{Vector256<byte>asciiVector=Vector256.LoadUnsafe(refsearchSpace,currentOffset);if(asciiVector.ExtractMostSignificantBits()!=0){returntrue;}currentOffset+=(nuint)Vector256<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}[Benchmark]publicboolTestZ_256(){refbytesearchSpace=ref*_bytes;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector256<byte>.Count;do{Vector256<byte>asciiVector=Vector256.LoadUnsafe(refsearchSpace,currentOffset);if(!Avx.TestZ(asciiVector,Vector256.Create((byte)0x80))){returntrue;}currentOffset+=(nuint)Vector256<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);returnfalse;}}

@EgorBoEgorBoSep 9, 2022

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.

@stephentoub I think it depends on CPU, I even had to revert TestZ from Vector.Equals because it produced regressions #67902

@stephentoubstephentoubSep 9, 2022

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 think it depends on CPU, I even had to revert TestZ from Vector.Equals because it produced regressions #67902

That change reverted it from both the 256-bit and 128-bit code paths. This PR uses TestZ for 128-bit. Why is that ok?

I'm questioning the non-symmetrical usage.

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.

@stephentoubstephentoubSep 9, 2022

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.

Thanks, but I'm not seeing the answer there to my question.

I'll restate:
This PR is adding additional code to prefer using TestZ with Vector128:
https://github.com/dotnet/runtime/pull/73055/files#diff-66bbe89271f826c9232bd146abb678844754515dc027f70ad0ce36f751da46ebR1379-R1391
Your #67902 reverted other changes that preferred using TestZ, not just on 256-bit but also on 128-bit vectors.
Does it still make sense for this PR to be adding additional code to use TestZ with Vector128?

(Part of why I'm pushing on this is with a goal of avoiding needing to drop down to direct instrinsics as much as possible. I'd hope we can get to a point where the obvious code to write is the best code to write in as many situations as possible.)

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.

Right, I am not saying the current non-symmetrical usage is correct, I'd probably change both to use ExtractMostSignificantBits

C++ compilers also do different things here, e.g. LLVM folds even direct MoveMask usage to testz: https://godbolt.org/z/MobvxvzGK

@tannergoodingtannergoodingSep 9, 2022

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.

What is "better" is going to depend on a few factors.

On x86/x64, ExtractMostSignificantBits is likely faster because it can always emit exactly as movmsk and there are some CPUs where TestZ can be slower, particularly for "small inputs" where the match is sooner. When the match is later, TestZ typically wins out regardless.

On Arm64, doing the early comparison against == Zero is likely better because it is a single instruction vs the multi-instruction sequence required to emulate x64's movmsk.

I think the best choice here is to use == Zero (and therefore TestZ) as I believe it will, on average, produce the best/most consistent code. The cases where it might be a bit slower will typically be for smaller inputs where we're already returning quickly and the extra couple nanoseconds won't really matter.


currentOffset += (nuint)Vector256<byte>.Count;
pCurrentWriteAddress += (nuint)Vector256<byte>.Count;
} while (currentOffset <= finalOffsetWhereCanRunLoop);

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.

On every iteration of the loop we're bumping currentOffset and then also adding that to pAsciiBuffer. Would it be faster to instead compute the upper bound as an address, just bump the current pointer in the loop, and then after the loop compute the currentOffset if needed based on the ending/starting difference?

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.

It produces better codegen, but the reported time difference is within the range of error (0-0.3ns gain or loss)

Details
publicunsafeclassWiden{privateconstintSize=1024;privatebyte*_bytes;privatechar*_chars;[GlobalSetup]publicvoidSetup(){_bytes=(byte*)NativeMemory.AlignedAlloc(newUIntPtr(Size),newUIntPtr(32));newSpan<byte>(_bytes,Size).Fill((byte)'a');_chars=(char*)NativeMemory.AlignedAlloc(newUIntPtr(Size*sizeof(char)),newUIntPtr(32));}[GlobalCleanup]publicvoidFree(){NativeMemory.AlignedFree(_bytes);NativeMemory.AlignedFree(_chars);}[Benchmark]publicvoidCurrent(){refbytesearchSpace=ref*_bytes;ushort*pCurrentWriteAddress=(ushort*)_chars;nuintcurrentOffset=0;nuintfinalOffsetWhereCanRunLoop=Size-(uint)Vector256<byte>.Count;do{Vector256<byte>asciiVector=Vector256.Load(_bytes+currentOffset);if(asciiVector.ExtractMostSignificantBits()!=0){break;}(Vector256<ushort>low,Vector256<ushort>upper)=Vector256.Widen(asciiVector);low.Store(pCurrentWriteAddress);upper.Store(pCurrentWriteAddress+Vector256<ushort>.Count);currentOffset+=(nuint)Vector256<byte>.Count;pCurrentWriteAddress+=(nuint)Vector256<byte>.Count;}while(currentOffset<=finalOffsetWhereCanRunLoop);}[Benchmark]publicvoidSuggested(){refbytecurrentSearchSpace=ref*_bytes;refushortcurrentWriteAddress=refUnsafe.As<char,ushort>(ref*_chars);refbyteoneVectorAwayFromEnd=refUnsafe.Add(refcurrentSearchSpace,Size-Vector256<byte>.Count);do{Vector256<byte>asciiVector=Vector256.LoadUnsafe(refcurrentSearchSpace);if(asciiVector.ExtractMostSignificantBits()!=0){break;}(Vector256<ushort>low,Vector256<ushort>upper)=Vector256.Widen(asciiVector);low.StoreUnsafe(refcurrentWriteAddress);upper.StoreUnsafe(refcurrentWriteAddress,(nuint)Vector256<ushort>.Count);currentSearchSpace=refUnsafe.Add(refcurrentSearchSpace,Vector256<byte>.Count);currentWriteAddress=refUnsafe.Add(refcurrentWriteAddress,Vector256<byte>.Count);}while(!Unsafe.IsAddressGreaterThan(refcurrentSearchSpace,refoneVectorAwayFromEnd));}}
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.856/21H2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK=7.0.100-rc.1.22423.16
[Host] : .NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2
MethodMeanErrorStdDevCode Size
Current44.29 ns0.171 ns0.152 ns81 B
Suggested44.54 ns0.042 ns0.032 ns77 B

.NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2

; VectorBenchmarks.Widen.Current()vzerouppermoveax,[rcx+8]movrax,[rcx+10]xoredx,edxM00_L00:movr8,[rcx+8] vmovdqu ymm0,ymmword ptr [r8+rdx] vpmovmskb r8d,ymm0testr8d,r8djne short M00_L01 vmovaps ymm1,ymm0 vpmovzxbw ymm1,xmm1vextractf128xmm0,ymm0,1 vpmovzxbw ymm0,xmm0 vmovdqu ymmword ptr [rax],ymm1 vmovdqu ymmword ptr [rax+20],ymm0addrdx,20addrax,40cmprdx,3E0jbe short M00_L00M00_L01:vzeroupperret; Total bytes of code 81

.NET 7.0.0 (7.0.22.42223), X64 RyuJIT AVX2

; VectorBenchmarks.Widen.Suggested()vzerouppermovrax,[rcx+8]movrdx,[rcx+10]learcx,[rax+3E0]M00_L00: vmovdqu ymm0,ymmword ptr [rax] vpmovmskb r8d,ymm0testr8d,r8djne short M00_L01 vmovaps ymm1,ymm0 vpmovzxbw ymm1,xmm1vextractf128xmm0,ymm0,1 vpmovzxbw ymm0,xmm0 vmovdqu ymmword ptr [rdx],ymm1 vmovdqu ymmword ptr [rdx+20],ymm0addrax,20addrdx,40cmprax,rcxjbe short M00_L00M00_L01:vzeroupperret; Total bytes of code 77

If you don't mind I am going to merge it as it is and apply your suggestion in my next PR.

break;
}

// Vector128.Widen is not used here as it less performant on ARM64

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 know why? Naively I'd expect the JIT to be able to produce the same code for both.

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 am sorry, but I don't. It's not that I did not try to find out, it's the arm64 tooling that makes it hard for me.

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.

@tannergooding?

If this is by design, ok. But if it's something we can/should be fixing in the JIT, I want to make sure we're not sweeping such issues under the rug. Ideally the obvious code is also the best performing code.

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.

Codegen looks good to me:
image
(add could be contained but it's unrelated here)

@tannergoodingtannergoodingSep 9, 2022

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'd need a comparison between the two code paths to see where the difference is.

I would expect these to be identical except for a case where the original code was making some assumption (based on knowing the inputs were restricted to a subset of all possible values) and therefore skipping an otherwise "required" step that would be necessary to ensure deterministic results for "any input".

} while (currentOffset <= finalOffsetWhereCanRunLoop);
}
}
else if (Vector.IsHardwareAccelerated)

@stephentoubstephentoubSep 8, 2022

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.

Why is the Vector<T> path still needed?

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.

Why is the Vector path still needed?

Some Mono variants don't support Vector128 for all configs yet

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.

Some Mono variants don't support Vector128 for all configs yet

Which support Vector and not Vector128?

Just the presence of these paths are keeping the methods from being R2R'd it seems.

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.

Mono-LLVM supports both, Mono without LLVM (e.g. default Mono jit mode or AOT) supports only Vector<>

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.

Mono-LLVM supports both, Mono without LLVM (e.g. default Mono jit mode or AOT) supports only Vector<>

Is that getting fixed?

We now have multiple vectorized implementations that don't have a Vector<T> code path. Why is this one special that it still needs one?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@adamsitnik@danmoseley@davidwrighton@AndyAyersMS@EgorBo@stephentoub@am11@tannergooding