Uh oh!
There was an error while loading. Please reload this page.
StringBuilder: use Span.Fill in Append repeating char - #86287
Conversation
ghost
commented
May 16, 2023
Tagging subscribers to this area: @dotnet/area-system-runtime Issue Details
Note: Benchmark code BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=8.0.100-preview.5.23255.2
[Host] : .NET 8.0.0 (8.0.23.25213), X64 RyuJIT AVX2
Job-FJHCLO : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-DXTBEB : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| int firstLength = chunkChars.Length - chunkLength; | ||
| if (firstLength > 0) | ||
| { | ||
| chunkChars.AsSpan(chunkLength, firstLength).Fill(value); |
There was a problem hiding this comment.
Insert
Debug.Assert(firstLength<repeatCount,"We shouldn't be called if there was enough space for the entire run.");Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
adamsitnik
left a comment
There was a problem hiding this comment.
LGTM, thank you @yesmey not just for the contribution but also for providing benchmark numbers and their source code!
Would you be interested in contributing a benchmark for Append(char, int) to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.Text/Perf.StringBuilder.cs? Currently we have no benchmarks for it and our reporting system won't report any improvements once I merge your PR.
Uh oh!
There was an error while loading. Please reload this page.
@adamsitnik The regression in #88673 comes from marking the allocating path of Append(char) with MethodImplOptions.NoInlining. That benchmark in particular is allocating fairly often, and it makes sense the slow path is faster when it previously was being inlined more aggressively. This pattern is fairly common across the runtime, but if you prefer I can remove it and see if its better? |
adamsitnik
commented
Jul 17, 2023
@yesmey could you please give it a try? I was also thinking about doing sth like this: publicStringBuilderAppend(charvalue){intnextCharIndex=m_ChunkLength;char[]chars=m_ChunkChars;if((uint)chars.Length==(uint)nextCharIndex){ExpandByABlock(1);nextCharIndex=0;chars=m_ChunkChars;}chars[nextCharIndex]=value;m_ChunkLength++;returnthis;}but I am not sure if array boundaries check would got removed. |
MichalPetryka
commented
Jul 17, 2023
You can avoid them like this: Unsafe.Add(refMemoryMarshal.GetArrayDataReference(chars),(uint)nextCharIndex)= value; |
Sorry about the delay. I'm gonna need some help on this one. Having trouble reproducing this locally. I think its because most of the benchmark is spent allocating and the Error column is sometimes very high. I tried to turn off as much as possible on my machine while running the benchmarks.
I doubt avoiding an array boundaries check would even be noticeable here since the allocation size doubles every time, so the times that code is executed is gonna be rare, and sacrificing bounds safety for that is likely not worth it My hypothesis is that the slowdown is coming from the benchmark calling this very frequently in a loop, and the jit is able to inline so the read/write of m_ChunkLength better in the previous version. Here's how I build and run the benchmark locally .\build.cmd Clr+Clr.Aot+Libs -c Release -rc Releaseand python3 .\scripts\benchmarks_ci.py -f net8.0 --filter System.Text.Tests.Perf_StringBuilder.Append_Char --corerun "D:\dotnet\runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe""D:\dotnet\yesmey_runtime\artifacts\bin\testhost\net8.0-windows-Release-x64\shared\Microsoft.NETCore.App\8.0.0\CoreRun.exe" --bdn-artifacts C:\results\after_test\yesmey_runtime...CoreRun.exe built from ff57624 BenchmarkDotNet=v0.13.2.2052-nightly, OS=Windows 11 (10.0.22621.1992)
AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores
.NET SDK=8.0.100-preview.7.23364.32
[Host] : .NET 8.0.0 (8.0.23.36403), X64 RyuJIT AVX2
Job-DWSUBF : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-YNMWPS : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
Here's the asm output: Before; System.Text.Tests.Perf_StringBuilder.Append_Char(Int32)pushrdipushrsipushrbppushrbxsubrsp,28movebx,edxmovrcx,offset MT_System.Text.StringBuildercall CORINFO_HELP_NEWSFASTmovrsi,raxmov dword ptr [rsi+20],7FFFFFFFmovrcx,offset MT_System.Char[]movedx,10call CORINFO_HELP_NEWARR_1_VClearcx,[rsi+8]movrdx,raxcall CORINFO_HELP_ASSIGN_REFxoredi,editestebx,ebxjle short M00_L02M00_L00:movecx,[rsi+18]movedx,ecxmovrax,[rsi+8]movr8d,[rax+8]cmpr8d,edxjbe short M00_L03movedx,edxmov word ptr [rax+rdx*2+10],61incecxmov[rsi+18],ecxM00_L01:incedicmpedi,ebxjl short M00_L00M00_L02:movrax,rsiaddrsp,28poprbxpoprbppoprsipoprdiretM00_L03:movebp,1movedx,[rsi+1C]leaedx,[rdx+rcx+1]cmpedx,[rsi+20]jg short M00_L07testedx,edxjle short M00_L07M00_L04:movrdx,[rsi+8]cmp[rdx+8],ecxjle short M00_L05leaeax,[rcx+1]cmpecx,[rdx+8]jae short M00_L08movecx,ecxmov word ptr [rdx+rcx*2+10],61decebpjmp short M00_L06M00_L05:mov[rsi+18],ecxmovrcx,rsimovedx,ebpcall qword ptr [7FFE1853D518]; System.Text.StringBuilder.ExpandByABlock(Int32)xorecx,ecxmoveax,ecxM00_L06:testebp,ebpmovecx,eaxjg short M00_L04mov[rsi+18],ecxjmp short M00_L01M00_L07:movrcx,offset MT_System.ArgumentOutOfRangeExceptioncall CORINFO_HELP_NEWSFASTmovrbx,raxmovecx,18331movrdx,7FFE180A4000call CORINFO_HELP_STRCNSmovrsi,raxcall qword ptr [7FFE18786790]movr8,raxmovrdx,rsimovrcx,rbxcall qword ptr [7FFE182B6F28]movrcx,rbxcall CORINFO_HELP_THROWint3M00_L08:call CORINFO_HELP_RNGCHKFAILint3; Total bytes of code 280After; System.Text.Tests.Perf_StringBuilder.Append_Char(Int32)pushrdipushrsipushrbxsubrsp,20movebx,edxmovrcx,offset MT_System.Text.StringBuildercall CORINFO_HELP_NEWSFASTmovrsi,raxmov dword ptr [rsi+20],7FFFFFFFmovrcx,offset MT_System.Char[]movedx,10call CORINFO_HELP_NEWARR_1_VClearcx,[rsi+8]movrdx,raxcall CORINFO_HELP_ASSIGN_REFxoredi,editestebx,ebxjle short M00_L02M00_L00:movecx,[rsi+18]movedx,ecxmovrax,[rsi+8]movr8d,[rax+8]cmpr8d,edxjbe short M00_L03movedx,edxmov word ptr [rax+rdx*2+10],61incecxmov[rsi+18],ecxM00_L01:incedicmpedi,ebxjl short M00_L00M00_L02:movrax,rsiaddrsp,20poprbxpoprsipoprdiretM00_L03:movrcx,rsimovedx,61call qword ptr [7FFE1853C3F0]; System.Text.StringBuilder.AppendWithExpansion(Char)jmp short M00_L01; Total bytes of code 137; System.Text.StringBuilder.AppendWithExpansion(Char)pushrsipushrbxsubrsp,28movrbx,rcxmovesi,edxmovrcx,rbxmovedx,1call qword ptr [7FFE1853CB58]; System.Text.StringBuilder.ExpandByABlock(Int32)movrax,[rbx+8]cmp dword ptr [rax+8],0jbe short M01_L00mov[rax+10],siinc dword ptr [rbx+18]addrsp,28poprbxpoprsiretM01_L00:call CORINFO_HELP_RNGCHKFAILint3; Total bytes of code 55 |
StringBuilder.Append(char value, int repeatCount)currently use a loop to add each character in sequence.We can instead use
Span<T>.Fillthat is better optimized for this.Note:
Append(char value)was calling this method as a fallback to when it needed to allocate. But with the new changes,Append(char value, int repeatCount)might be inlined, therefor I added a specificAppendWithExpansion(char)withMethodImplOptions.NoInlining.Benchmark code
Benchmark result: