Skip to content

Improve vectorization of String.Split - #64899

Merged
danmoseley merged 9 commits into
dotnet:mainfrom
yesmey:main
Mar 24, 2022
Merged

Improve vectorization of String.Split#64899
danmoseley merged 9 commits into
dotnet:mainfrom
yesmey:main

Conversation

@yesmey

@yesmeyyesmey commented Feb 7, 2022

Copy link
Copy Markdown
Contributor

This pull request aims to simplify and improve upon the current vectorized fast path of string.Split.

Changes include:

  • Replace specialized SSE4.1 instructions with the new cross-platform intrinsic API
  • Add 265 bit instructions for longer strings
  • Improve the common path of Append in ValueListBuilder
    • Haven't made any explicit benchmark for this, but you can compare assembly output here: beforeafter

For benchmark testing I tried to use both the csv parsing in #38001 and the benchmark referenced in #51259
The benchmarks include both 256 bit and 128 bit versions (sse/avx). Unfortunately I have not been able to benchmark any other platforms than x86_64

Benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1466 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-alpha.1.21568.2
[Host] : .NET 7.0.0 (7.0.21.56701), X64 RyuJIT
Job-OHGYOD : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Toolchain=CoreRun 
MethodCorpusUriMeanErrorStdDev
SplitCsv mainhttp(...).csv [107]11.71 μs0.224 μs0.210 μs
SplitCsv Vector128http(...).csv [107]9.915 μs0.0225 μs0.0175 μs
SplitCsv Vector256http(...).csv [107]9.768 μs0.1745 μs0.2612 μs
SplitCsv mainhttps(...)e.csv [50]69.22 μs0.182 μs0.170 μs
SplitCsv Vector128https(...)e.csv [50]65.784 μs0.0870 μs0.0772 μs
SplitCsv Vector256https(...)e.csv [50]58.383 μs0.3115 μs0.2914 μs
SplitCsv mainhttps(...)A.csv [77]354.65 μs2.050 μs1.712 μs
SplitCsv Vector128https(...)A.csv [77]311.968 μs0.8166 μs0.6376 μs
SplitCsv Vector256https(...)A.csv [77]319.919 μs2.0044 μs1.8749 μs
MethodschrMeanErrorStdDev
SplitArray mainA B C(...)X Y Z [51]' '291.20 ns5.879 ns12.655 ns
SplitArray Vector128A B C(...)X Y Z [51]' '270.11 ns2.206 ns2.063 ns
SplitArray Vector256A B C(...)X Y Z [51]' '271.53 ns3.810 ns3.377 ns
SplitArray mainABCDE(...)VWXYZ [26]' '19.57 ns0.180 ns0.151 ns
SplitArray Vector128ABCDE(...)VWXYZ [26]' '18.82 ns0.082 ns0.077 ns
SplitArray Vector256ABCDE(...)VWXYZ [26]' '19.35 ns0.059 ns0.052 ns
Benchmark code
[DisassemblyDiagnoser]publicclassCsvBenchmarks{privatestring[]_strings;publicIEnumerable<string>CorpusList(){yieldreturn"https://www.census.gov/econ/bfs/csv/date_table.csv";yieldreturn"https://www.sba.gov/sites/default/files/aboutsbaarticle/FY16_SBA_RAW_DATA.csv";yieldreturn"https://wfmi.nifc.gov/fire_reporting/annual_dataset_archive/1972-2010/_WFMI_Big_Files/BOR_1972-2010_Gis.csv";}[ParamsSource("CorpusList")]publicstringCorpusUri{get;set;}[GlobalSetup]publicvoidSetup(){_strings=GetStringsFromCorpus().GetAwaiter().GetResult();}privateasyncTask<string[]>GetStringsFromCorpus(){usingvarclient=newHttpClient();usingvarresponse=awaitclient.GetAsync(CorpusUri);response.EnsureSuccessStatusCode();varbody=awaitresponse.Content.ReadAsStringAsync();List<string>lines=new();StringReaderreader=newStringReader(body);string?line;while((line=reader.ReadLine())!=null){lines.Add(line);}returnlines.ToArray();}[Benchmark]publicstring[]?SplitCsv(){string[]?split=null;string[]lines=_strings;for(inti=0;i<lines.Length;i++){split=lines[i].Split(',');}returnsplit;}}[DisassemblyDiagnoser]publicclassRegressionBenchmark{[Benchmark][Arguments("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",' ')][Arguments("ABCDEFGHIJKLMNOPQRSTUVWXYZ",' ')]publicstring[]SplitArray(strings,charchr)=>s.Split(chr);}publicclassProgram{publicstaticvoidMain(string[]args){BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);}}

Related to #51259

- Implement Vector265 for longer strings
- Simplify the Vector code and use new cross-platform intrinsic API
- Use ref _firstChar instead of ref MemoryMarshal.GetReference(this.AsSpan());
- Use unsigned check for separators.Length so that two redundant range checks are optimized away
@ghostghost added the community-contribution Indicates that the PR has been added by a community member label Feb 7, 2022
@ghost

ghost commented Feb 7, 2022

Copy link
Copy Markdown

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.


// Special-case the common cases of 1, 2, and 3 separators, with manual comparisons against each separator.
else if (separators.Length <= 3)
else if (separators.Length <= 3u)

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.

does it affect codegen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes, it got rid of redundant range checks for separators, doing (uint)separators.Length <= (uint)3 is one movsxd less, but I personally thought this was cleaner. However, I can see it being too obscure with it's intent.

Vector256<ushort> vector = Vector256.LoadUnsafe(ref source, (uint)i);
Vector256<ushort> cmp = Vector256.Equals(vector, v1) | Vector256.Equals(vector, v2) | Vector256.Equals(vector, v3);

uint mask = cmp.AsByte().ExtractMostSignificantBits() & 0b0101010101010101;

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 might be a good idea to also use TestZ for faster out, e.g.

if(cmp==Vector256<ushort>.Zero)continue;

it's faster than movmsk

while (mask != 0)
{
sepListBuilder.Append(idx);
sepListBuilder.Append(i + BitOperations.TrailingZeroCount(mask) / 2);

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.

{
if ((lowBits & 0xF) != 0)
Vector256<ushort> vector = Vector256.LoadUnsafe(ref source, (uint)i);
Vector256<ushort> cmp = Vector256.Equals(vector, v1) | Vector256.Equals(vector, v2) | Vector256.Equals(vector, v3);

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.

consider splitting this to temps for better pipelining so all compare instructions will be next to each other and so are ORs


for (int idx = i; lowBits != 0; idx++)
int vector256ShortCount = Vector256<ushort>.Count;
for (; (i + vector256ShortCount) <= Length; i += vector256ShortCount)

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.

Consider processing trailing elements via overlapping instead of scalar fallback

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There's a risk though that the code will start getting a bit complicated, I wanted to keep the code easy to follow since it's only used for a specific scenario. If you still think it's worth it, I can definitely look into it

@EgorBoEgorBoFeb 7, 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.

handling trailing elements in the same loop (or via a spilled iteration) shows nice improvements for small-medium sized inputs, in theory it only adds an additional check inside the loop, feel free to keep it as is, we can then follow up


for (int idx = i; lowBits != 0; idx++)
int vector256ShortCount = Vector256<ushort>.Count;
for (; (i + vector256ShortCount) <= Length; i += vector256ShortCount)

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 + vector256ShortCount) <= Length might overflow, it should be
i <= Length - vector256ShortCount

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.

Besides that the i <= len - count version can keep the len - count in a register, whilst i + count needs a repeated addition.

Also local vector256ShortCount isn't needed, as JIT will treat Vector256<ushort>.Count as constant.


ref char c0 = ref MemoryMarshal.GetReference(this.AsSpan());
int cond = Length & -Vector128<ushort>.Count;
int i = 0;

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.

int -> nint, it will help to avoid redundant sign extensions

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The same variable is used as index to the scalar/non vectorized version at the bottom. I'll see if I can find a middle-way

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can always cast it to signed just once before the scalar version


for (int idx = i; lowBits != 0; idx++)
int vector256ShortCount = Vector256<ushort>.Count;
for (; (i + vector256ShortCount) <= Length; i += vector256ShortCount)

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.

Besides that the i <= len - count version can keep the len - count in a register, whilst i + count needs a repeated addition.

Also local vector256ShortCount isn't needed, as JIT will treat Vector256<ushort>.Count as constant.

Comment on lines +1706 to +1707
int vector128ShortCount = Vector128<ushort>.Count;
for (; (i + vector128ShortCount) <= Length; i += vector128ShortCount)

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
intvector128ShortCount=Vector128<ushort>.Count;
for(;(i+vector128ShortCount)<=Length;i+=vector128ShortCount)
for(;i<=Length-Vector128<ushort>.Count;i+=Vector128<ushort>.Count)

When i is of type nint just check if the comparison doesn't introduce any sign extensions -- please double check to be on the safe side.

@yesmey

yesmey commented Feb 7, 2022

Copy link
Copy Markdown
ContributorAuthor

@EgorBo@gfoidl Thanks for the good tips and feedback, I updated the pull request accordingly.
Unfortunately the 256 bit code had a bug - I was masking the movmskb result with every other bit, but accidentally had copied the mask from the 128 bit code where the result is only 16 bit. There wasn't any string in the test suite to cover it.

The benchmark numbers for 256 bit is much more realistic now. It looks to be much closer to the 128 bit version now. Please let me know your opinion, and sorry for the mistake

@gfoidl

Copy link
Copy Markdown
Member

benchmark numbers for 256 bit is much more realistic now. It looks to be much closer to the 128 bit version now

It's the current numbers in the PR's description?
For Vector256 there's only little gain, so is it worth to have a dedicated code-path for it? ARM won't support it anyway.

@yesmey

Copy link
Copy Markdown
ContributorAuthor

@gfoidl Yes those are the latest numbers. I can remove the 256bit path it if you want

@stephentoub

Copy link
Copy Markdown
Member

Yes those are the latest numbers. I can remove the 256bit path it if you want

Are any of these tests for really long inputs containing very few separators?

Vector256<byte> cmp = (vector1 | vector2 | vector3).AsByte();
Vector256<ushort> v1 = Vector256.Create((ushort)c);
Vector256<ushort> v2 = Vector256.Create((ushort)c2);
Vector256<ushort> v3 = Vector256.Create((ushort)c3);

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.

Unrelated to this PR, am just curios if our guidelines allow to use var here, the type of vector should be pretty obvious from the expression on the right.

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.

The guidelines say var should only be used for a ctor or explicit cast. While it's arguable that Create is equivalent to a ctor, there's nothing that requires it to return the same type it's declared on, and in fact there are cases where Create methods don't, e.g. File.Create.

@yesmey

yesmey commented Feb 9, 2022

Copy link
Copy Markdown
ContributorAuthor

Sorry for my delay, I decided to rewrite the 256 bit spilling and made some improvements for the scalar loop.

Here's a gist of a much bigger bechmark suite: https://gist.github.com/yesmey/2e7a7868bb10043553b78d77cbc3f2b8
(note: the bold text is baseline)

benchmark code for gist
publicclassBenchmarks{privatestaticstring_testStr;privatestaticSystem.Text.StringBuilderst;privatestaticchar[][]_testChar=newchar[3][];staticBenchmarks(){st=newSystem.Text.StringBuilder(5_000_000);_testChar[0]=newchar[1]{' '};_testChar[1]=newchar[2]{' ','t'};_testChar[2]=newchar[3]{' ','t','f'};}privatestaticstringBuildStr(charc,intstringLength,intsepFreq,charsep){for(inti=0;i<stringLength;i++){if(i%sepFreq==0){st.Append(sep);}else{st.Append(c);}}stringt=st.ToString();st.Clear();returnt;}[GlobalSetup]publicvoidInit(){_testStr=BuildStr('a',Size,SepFreq,_testChar[2][SplitCount-1]);}[Params(16,64,200,1000,10000)]publicintSize{get;set;}[Params(1,2,5,200)]publicintSepFreq{get;set;}[Params(1,2,3)]publicintSplitCount{get;set;}[Benchmark]publicstring[]Split(){return_testStr.Split(_testChar[SplitCount-1]);}}

Updated numbers from previous benchmarks:

csv + dotnet/performance
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1526 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22108.4
[Host] : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
Job-AJDBJE : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-XTZHCY : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
MethodJobToolchainCorpusUriMeanErrorStdDevRatioRatioSD
SplitCsvJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttp(...).csv [107]11.493 μs0.1911 μs0.1787 μs1.190.02
SplitCsvJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttp(...).csv [107]9.631 μs0.1624 μs0.1519 μs1.000.00
SplitCsvJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)e.csv [50]60.477 μs0.2351 μs0.2200 μs0.960.01
SplitCsvJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)e.csv [50]62.927 μs0.9685 μs1.5078 μs1.000.00
SplitCsvJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)A.csv [77]343.339 μs3.5451 μs3.3161 μs1.190.01
SplitCsvJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)A.csv [77]287.859 μs2.7981 μs2.6174 μs1.000.00
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1526 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22108.4
[Host] : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
Job-AJDBJE : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-XTZHCY : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
MethodJobToolchainschrarroptionsMeanErrorStdDevRatioRatioSD
SplitCharJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]** **??312.40 ns3.786 ns3.541 ns1.080.11
SplitCharJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]??284.75 ns8.865 ns26.139 ns1.000.00
SplitJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]None292.72 ns2.842 ns2.519 ns1.170.02
SplitJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]None251.01 ns4.483 ns3.974 ns1.000.00
SplitJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]RemoveEmptyEntries368.19 ns5.179 ns4.591 ns1.090.02
SplitJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]RemoveEmptyEntries337.88 ns0.900 ns0.841 ns1.000.00
SplitCharJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]** **??19.20 ns0.035 ns0.032 ns0.760.00
SplitCharJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]??25.16 ns0.044 ns0.041 ns1.000.00
SplitJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]None18.71 ns0.028 ns0.025 ns0.630.00
SplitJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]None29.67 ns0.052 ns0.046 ns1.000.00
SplitJob-AJDBJE\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]RemoveEmptyEntries17.71 ns0.026 ns0.024 ns0.550.00
SplitJob-XTZHCY\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]RemoveEmptyEntries32.04 ns0.050 ns0.045 ns1.000.00

There seems to be regressions on the strings with no split chars in them

@ghost

Copy link
Copy Markdown

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

Issue Details

This pull request aims to simplify and improve upon the current vectorized fast path of string.Split.

Changes include:

  • Replace specialized SSE4.1 instructions with the new cross-platform intrinsic API
  • Add 265 bit instructions for longer strings
  • Improve the common path of Append in ValueListBuilder
    • Haven't made any explicit benchmark for this, but you can compare assembly output here: beforeafter

For benchmark testing I tried to use both the csv parsing in #38001 and the benchmark referenced in #51259
The benchmarks include both 256 bit and 128 bit versions (sse/avx). Unfortunately I have not been able to benchmark any other platforms than x86_64

Benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1466 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-alpha.1.21568.2
[Host] : .NET 7.0.0 (7.0.21.56701), X64 RyuJIT
Job-OHGYOD : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Toolchain=CoreRun 
MethodCorpusUriMeanErrorStdDev
SplitCsv mainhttp(...).csv [107]11.71 μs0.224 μs0.210 μs
SplitCsv Vector128http(...).csv [107]9.915 μs0.0225 μs0.0175 μs
SplitCsv Vector256http(...).csv [107]9.768 μs0.1745 μs0.2612 μs
SplitCsv mainhttps(...)e.csv [50]69.22 μs0.182 μs0.170 μs
SplitCsv Vector128https(...)e.csv [50]65.784 μs0.0870 μs0.0772 μs
SplitCsv Vector256https(...)e.csv [50]58.383 μs0.3115 μs0.2914 μs
SplitCsv mainhttps(...)A.csv [77]354.65 μs2.050 μs1.712 μs
SplitCsv Vector128https(...)A.csv [77]311.968 μs0.8166 μs0.6376 μs
SplitCsv Vector256https(...)A.csv [77]319.919 μs2.0044 μs1.8749 μs
MethodschrMeanErrorStdDev
SplitArray mainA B C(...)X Y Z [51]' '291.20 ns5.879 ns12.655 ns
SplitArray Vector128A B C(...)X Y Z [51]' '270.11 ns2.206 ns2.063 ns
SplitArray Vector256A B C(...)X Y Z [51]' '271.53 ns3.810 ns3.377 ns
SplitArray mainABCDE(...)VWXYZ [26]' '19.57 ns0.180 ns0.151 ns
SplitArray Vector128ABCDE(...)VWXYZ [26]' '18.82 ns0.082 ns0.077 ns
SplitArray Vector256ABCDE(...)VWXYZ [26]' '19.35 ns0.059 ns0.052 ns
Benchmark code
[DisassemblyDiagnoser]publicclassCsvBenchmarks{privatestring[]_strings;publicIEnumerable<string>CorpusList(){yieldreturn"https://www.census.gov/econ/bfs/csv/date_table.csv";yieldreturn"https://www.sba.gov/sites/default/files/aboutsbaarticle/FY16_SBA_RAW_DATA.csv";yieldreturn"https://wfmi.nifc.gov/fire_reporting/annual_dataset_archive/1972-2010/_WFMI_Big_Files/BOR_1972-2010_Gis.csv";}[ParamsSource("CorpusList")]publicstringCorpusUri{get;set;}[GlobalSetup]publicvoidSetup(){_strings=GetStringsFromCorpus().GetAwaiter().GetResult();}privateasyncTask<string[]>GetStringsFromCorpus(){usingvarclient=newHttpClient();usingvarresponse=awaitclient.GetAsync(CorpusUri);response.EnsureSuccessStatusCode();varbody=awaitresponse.Content.ReadAsStringAsync();List<string>lines=new();StringReaderreader=newStringReader(body);string?line;while((line=reader.ReadLine())!=null){lines.Add(line);}returnlines.ToArray();}[Benchmark]publicstring[]?SplitCsv(){string[]?split=null;string[]lines=_strings;for(inti=0;i<lines.Length;i++){split=lines[i].Split(',');}returnsplit;}}[DisassemblyDiagnoser]publicclassRegressionBenchmark{[Benchmark][Arguments("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",' ')][Arguments("ABCDEFGHIJKLMNOPQRSTUVWXYZ",' ')]publicstring[]SplitArray(strings,charchr)=>s.Split(chr);}publicclassProgram{publicstaticvoidMain(string[]args){BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);}}

Related to #51259

Author:yesmey
Assignees:-
Labels:

area-System.Runtime, community-contribution

Milestone:-

@yesmey

Copy link
Copy Markdown
ContributorAuthor

Status update: I can't get the 256 bit version to perform well on lower-mid ranges because of the saving/restore overhead of registers due to the nested calls inside ValueListBuilder.Append. The 256 bit assembly currently looks like this: https://gist.github.com/yesmey/7786c102927cf8e9abf966cf44a35484, and as you can tell there's a lot of initial vmovaps just for the potential call of Grow in AddWithResize. Just to prove my point, I commented out Grow inside AddWithResize for comparison here.

So since I'm not getting any further there, I'm thinking maybe giving up on the 256 bit and keep the 128 bit version, which is on par in performance, just to have an implementation for arm

@EgorBo

Copy link
Copy Markdown
Member

So since I'm not getting any further there, I'm thinking maybe giving up on the 256 bit and keep the 128 bit version

that's ok, we try to use AVX only where it's definitely profitable.

@yesmey

yesmey commented Feb 13, 2022

Copy link
Copy Markdown
ContributorAuthor

benchmarks for commit dcadf05

CSV parsing benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1526 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22108.4
[Host] : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
Job-EPAGWH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-DBDUQW : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
MethodJobToolchainCorpusUriMeanErrorStdDevRatioRatioSD
SplitCsvJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttp(...).csv [107]11.25 μs0.104 μs0.098 μs1.000.00
SplitCsvJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttp(...).csv [107]10.04 μs0.011 μs0.009 μs0.890.01
SplitCsvJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)e.csv [50]57.39 μs1.093 μs1.074 μs1.000.00
SplitCsvJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)e.csv [50]53.80 μs0.305 μs0.285 μs0.940.02
SplitCsvJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)A.csv [77]326.79 μs1.039 μs0.921 μs1.000.00
SplitCsvJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exehttps(...)A.csv [77]297.05 μs5.757 μs5.912 μs0.910.02
dotnet/performance benchmarks
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1526 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22108.4
[Host] : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
Job-EPAGWH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-DBDUQW : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
MethodJobToolchainschrarroptionsMeanErrorStdDevRatioRatioSD
SplitCharJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]** **??297.36 ns5.652 ns6.282 ns1.000.00
SplitCharJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]??291.99 ns5.821 ns12.022 ns0.990.04
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]None314.12 ns9.964 ns29.379 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]None272.73 ns3.769 ns5.160 ns0.880.08
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]RemoveEmptyEntries374.66 ns4.502 ns3.759 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeA B C(...)X Y Z [51]?Char[1]RemoveEmptyEntries366.11 ns0.924 ns0.819 ns0.980.01
SplitCharJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]** **??25.87 ns0.030 ns0.027 ns1.000.00
SplitCharJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]??18.23 ns0.016 ns0.013 ns0.700.00
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]None18.19 ns0.146 ns0.122 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]None18.39 ns0.034 ns0.030 ns1.010.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]RemoveEmptyEntries17.82 ns0.017 ns0.013 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exeABCDE(...)VWXYZ [26]?Char[1]RemoveEmptyEntries18.37 ns0.066 ns0.059 ns1.030.00
partial 38001 issue benchmark suite
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1526 (20H2/October2020Update)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22108.4
[Host] : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
Job-EPAGWH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-DBDUQW : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
MethodJobToolchainSizeSepFreqSplitCountMeanErrorStdDevMedianRatioRatioSD
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe161190.66 ns0.146 ns0.129 ns90.66 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe161198.80 ns0.351 ns0.329 ns98.83 ns1.090.00
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1612101.48 ns1.119 ns0.992 ns101.72 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1612110.29 ns2.051 ns1.919 ns110.52 ns1.080.02
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe165162.59 ns1.013 ns0.846 ns62.98 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe165154.82 ns0.617 ns0.516 ns54.83 ns0.880.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe165266.75 ns1.380 ns1.842 ns65.78 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe165254.66 ns1.106 ns0.924 ns54.41 ns0.820.03
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe16200133.08 ns0.107 ns0.095 ns33.10 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe16200130.76 ns0.220 ns0.205 ns30.80 ns0.930.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe16200235.91 ns0.745 ns1.362 ns35.77 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe16200231.82 ns0.462 ns0.432 ns31.69 ns0.890.06
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200111,043.18 ns20.611 ns36.099 ns1,045.06 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200111,060.80 ns20.757 ns32.922 ns1,052.50 ns1.020.05
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe20012928.95 ns18.445 ns35.538 ns937.08 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200121,040.02 ns12.802 ns9.995 ns1,044.64 ns1.090.03
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe20051546.27 ns10.817 ns18.073 ns544.85 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe20051455.87 ns9.008 ns10.373 ns453.83 ns0.840.04
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe20052514.16 ns5.528 ns4.900 ns513.00 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe20052481.95 ns9.573 ns18.444 ns485.77 ns0.920.03
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200200165.77 ns0.118 ns0.098 ns65.77 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200200166.08 ns0.448 ns0.420 ns66.05 ns1.000.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200200265.91 ns0.365 ns0.342 ns65.75 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe200200267.15 ns0.508 ns0.424 ns67.10 ns1.020.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000114,139.27 ns15.050 ns11.750 ns4,139.32 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000114,358.61 ns9.762 ns8.654 ns4,354.70 ns1.050.00
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000124,162.92 ns52.087 ns77.961 ns4,133.73 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000124,639.01 ns92.551 ns197.234 ns4,646.57 ns1.110.05
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000512,348.05 ns47.024 ns112.666 ns2,355.87 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000512,201.39 ns53.919 ns158.983 ns2,145.12 ns0.940.08
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000522,408.45 ns49.325 ns145.436 ns2,465.65 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000522,160.78 ns42.667 ns85.210 ns2,151.04 ns0.890.07
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe10002001248.99 ns0.974 ns0.911 ns249.01 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe10002001246.70 ns1.574 ns1.314 ns247.05 ns0.990.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe10002002245.95 ns1.117 ns1.045 ns245.52 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe10002002244.58 ns1.302 ns1.218 ns244.91 ns0.990.00
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100001140,257.63 ns395.206 ns330.015 ns40,114.94 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100001143,103.42 ns797.633 ns622.740 ns43,088.24 ns1.070.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100001240,372.55 ns764.782 ns715.378 ns39,915.27 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100001243,351.75 ns857.048 ns1,916.910 ns42,166.56 ns1.100.05
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100005123,065.97 ns419.810 ns372.151 ns22,920.42 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100005121,142.54 ns471.224 ns1,389.414 ns20,593.73 ns0.940.08
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100005225,646.14 ns506.991 ns1,204.917 ns25,906.24 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe100005223,400.37 ns466.163 ns1,344.987 ns23,348.94 ns0.920.08
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000020012,161.35 ns3.897 ns3.254 ns2,159.56 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000020012,182.22 ns16.648 ns15.573 ns2,181.21 ns1.010.01
SplitJob-EPAGWH\runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000020022,174.07 ns3.713 ns3.291 ns2,173.86 ns1.000.00
SplitJob-DBDUQW\yesmey_runtime\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe1000020022,130.82 ns8.968 ns7.949 ns2,131.29 ns0.980.00
benchmark source
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;publicclassCsvBenchmarks{privatestring[]_strings;publicIEnumerable<string>CorpusList(){// only these three urls still return any resultyieldreturn"https://www.census.gov/econ/bfs/csv/date_table.csv";yieldreturn"https://www.sba.gov/sites/default/files/aboutsbaarticle/FY16_SBA_RAW_DATA.csv";yieldreturn"https://wfmi.nifc.gov/fire_reporting/annual_dataset_archive/1972-2010/_WFMI_Big_Files/BOR_1972-2010_Gis.csv";}[ParamsSource("CorpusList")]publicstringCorpusUri{get;set;}[GlobalSetup]publicvoidSetup(){_strings=GetStringsFromCorpus().GetAwaiter().GetResult();}privateasyncTask<string[]>GetStringsFromCorpus(){usingvarclient=newHttpClient();usingvarresponse=awaitclient.GetAsync(CorpusUri);response.EnsureSuccessStatusCode();varbody=awaitresponse.Content.ReadAsStringAsync();List<string>lines=new();StringReaderreader=newStringReader(body);string?line;while((line=reader.ReadLine())!=null){lines.Add(line);}returnlines.ToArray();}[Benchmark]publicstring[]?SplitCsv(){string[]?split=null;string[]lines=_strings;for(inti=0;i<lines.Length;i++){split=lines[i].Split(',');}returnsplit;}}publicclassRegressionBenchmark{[Benchmark][Arguments("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",' ')][Arguments("ABCDEFGHIJKLMNOPQRSTUVWXYZ",' ')]publicstring[]SplitChar(strings,charchr)=>s.Split(chr);[Benchmark][Arguments("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",newchar[]{' '},StringSplitOptions.None)][Arguments("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z",newchar[]{' '},StringSplitOptions.RemoveEmptyEntries)][Arguments("ABCDEFGHIJKLMNOPQRSTUVWXYZ",newchar[]{' '},StringSplitOptions.None)][Arguments("ABCDEFGHIJKLMNOPQRSTUVWXYZ",newchar[]{' '},StringSplitOptions.RemoveEmptyEntries)]publicstring[]Split(strings,char[]arr,StringSplitOptionsoptions)=>s.Split(arr,options);}publicclassBenchmarks{privatestaticstring_testStr;privatestaticSystem.Text.StringBuilderst;privatestaticchar[][]_testChar=newchar[3][];staticBenchmarks(){st=newSystem.Text.StringBuilder(5_000_000);_testChar[0]=newchar[1]{' '};_testChar[1]=newchar[3]{' ','t','f'};}privatestaticstringBuildStr(charc,intstringLength,intsepFreq,charsep){for(inti=0;i<stringLength;i++){if(i%sepFreq==0){st.Append(sep);}else{st.Append(c);}}stringt=st.ToString();st.Clear();returnt;}[GlobalSetup]publicvoidInit(){_testStr=BuildStr('a',Size,SepFreq,_testChar[1][SplitCount-1]);}[Params(16,200,1000,10000)]publicintSize{get;set;}[Params(1,5,200)]publicintSepFreq{get;set;}[Params(1,2)]publicintSplitCount{get;set;}[Benchmark]publicstring[]Split(){return_testStr.Split(_testChar[SplitCount-1]);}}publicclassProgram{publicstaticvoidMain(string[]args){BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);}}

@danmoseley

Copy link
Copy Markdown
Contributor

@EgorBo@stephentoub@gfoidl is your feedback addressed ?

@gfoidlgfoidl 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.

@danmoseley I had another look, when these points are addressed I'm happy with the PR 😄.


// Special-case the common cases of 1, 2, and 3 separators, with manual comparisons against each separator.
else if (separators.Length <= 3)
else if ((uint)separators.Length <= (uint)3)

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.

Is this cast still needed?
AFAIK JIT recognizes this now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gfoidl do you mean we no longer need the pattern if ((uint)index > (uint)array.Length) that we have everywhere in the tree? If so we should have an issue to remove 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.

#62864 is the PR for that change (got merged 26 days ago).

If so we should have an issue to remove it.

Filed #67044 for it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That's great! Glad it got fixed, I'll get rid of it

// Redundant test so we won't prejit remainder of this method
// on platforms without SSE.
if (!Sse41.IsSupported)
if (!Vector128.IsHardwareAccelerated)

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.

Please use the comment from the previous version (left side of comparison) to make it clear that this check is needed to avoid prejit.
Otherwise a Debug.Assert(Vector128.IsHardwareAccelerated) could do it too.

@yesmeyyesmeyMar 23, 2022

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'll reintroduce the comment with a small text change since it's not limited to only SSE anymore

int i = 0;

for (; i < cond; i += Vector128<ushort>.Count)
while (offset <= lengthToExamine - (nuint)Vector128<ushort>.Count)

@gfoidlgfoidlMar 23, 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.

Above is L1618 we guard by Vector128<ushort>.Count * 2, so when reaching this point, we know that there are for sure enough elements available. This this check isn't need at this point. So you could change the loop to da do-while loop. Thus the first iteration is without any (further) pre-condition, and after the iteration the check for more available elements is done.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks. I'll add a Debug.Assert on entry to make it a little more obvious to the reader that its a precondition

while (offset < lengthToExamine)
{
char curr = Unsafe.Add(ref c0, (IntPtr)(uint)i);
char curr = (char)Unsafe.Add(ref source, (nint)offset);

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
charcurr=(char)Unsafe.Add(refsource,(nint)offset);
charcurr=(char)Unsafe.Add(refsource,offset);

Not needed, there's an overload for nuint.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I must've missed that it got added, thanks

@gfoidlgfoidl 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.

Just one question -- otherwise LGTM.

sep2 = separators.Length > 2 ? separators[2] : sep1;

if (Length >= 16 && Sse41.IsSupported)
if (Vector128.IsHardwareAccelerated && Length >= Vector128<ushort>.Count * 2)

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.

Just to double-check: the * 2 is intentional as perf-numbers showed that?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes exactly, smaller strings doesn't perform as well

@danmoseley

Copy link
Copy Markdown
Contributor

methodtable assert is #64544
FSW crash is #67071
JSON assert is #60962

@danmoseley
danmoseley merged commit b4e258a into dotnet:mainMar 24, 2022
radekdoulik pushed a commit to radekdoulik/runtime that referenced this pull request Mar 30, 2022
@EgorBo

Copy link
Copy Markdown
Member

Improvement on win-x64 dotnet/perf-autofiling-issues#4291

@danmoseley

Copy link
Copy Markdown
Contributor

Nice drop in that graph @yesmey . Do you plan to do more of this kind of work?

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

Labels

area-System.Runtimecommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@yesmey@gfoidl@stephentoub@EgorBo@danmoseley@tannergooding@marek-safar