Uh oh!
There was an error while loading. Please reload this page.
StreamReader sync/async optimizations: ArrayPool, IndexOfAny, ValueStringBuilder - #62552
StreamReader sync/async optimizations: ArrayPool, IndexOfAny, ValueStringBuilder#62552Trapov wants to merge 26 commits into
Conversation
ghost
commented
Dec 8, 2021
Tagging subscribers to this area: @dotnet/area-system-io Issue Detailsnull
|
Uh oh!
There was an error while loading. Please reload this page.
Trapov
commented
Dec 8, 2021
First commit:
|
Trapov
commented
Dec 8, 2021
Second commit.
|
Trapov
commented
Dec 8, 2021
Third commit.
|
Trapov
commented
Dec 8, 2021
@stephentoub@adamsitnik moved your suggestions to here. |
stephentoub
commented
Dec 8, 2021
Thanks. My use of StringBuilder in the other PR comments was mostly for a quick hack to exemplify the wins. I think we should look here at instead using char arrays from ArrayPool. |
Wouldn't be there a problem when there's only one line and we return and rent new char arrays other and other until we hit EOF? |
8c20efb to
fb6ccb3Comparefb6ccb3 to
e2e318fCompare
The first and the second cases do need reallocating/resizing pooled array. |
stephentoub
commented
Dec 9, 2021
Why is that a problem? |
Trapov
commented
Dec 9, 2021
I meant reallocating and resizing the polled arrays wouldn't the cost be higher? |
stephentoub
commented
Dec 9, 2021
There would be some cost, but it should also be rare. We can afford to rent a larger char[] initially than we can proactively allocate a StringBuilder, and by renting/returning, we save the cost of creating a new StringBuilder for each StreamReader, plus avoid increasing the size of the StreamReader itself. We also avoid holding on to a potentially large object for the lifetime of the StreamReader and instead allow other code in the process to use that same memory, or potentially for it to be reclaimed. |
Trapov
commented
Dec 9, 2021
@stephentoub Added array pools as a draft and they do look better. I've picked 80 as default increase factor, probably gonna change to something different. I assume we could redo passing the arrays and copying them as spans, and change Array.Copy to Buffer.Copy but I'm not sure. Also gonna run unit-tests locally. Feel like I broke something.
|
Trapov
commented
Dec 9, 2021
Can't really run the tests I hope the CI would spot an error if there is one (I hope no errors) :( #62586 (created an issue) Anyway the benchmarks are here so feel free to comment. I'm gonna play with the Spans until any other comments/suggestions. |
Fixed the tests. And figured out how to run them locally (wrote about it in the issue and closed it). |
Trapov
commented
Dec 9, 2021
Benchmarks after fixing the tests.
|
Trapov
commented
May 11, 2022
@danmoseley removed readtoend changes. |
Uh oh!
There was an error while loading. Please reload this page.
Can you update the benchmark now -- I don't see ReadLine results. We care about ReadLine and ReadLineAsync benchmarks, I would expect the others to be unchanged (of course we should verify) Some nice improvements in the ReadLineAsync numbers though. I'm guessing the short line cases are slower because IndexOfAny(..) is slower than a trivial loop when the hit is very early in the buffer. Something interesting I noticed is that the perf characteristics will be slightly different on Windows vs Linux/Mac (that you have). That's because the perf tests use the platform line ending: You might want to locally edit that line to use @stephentoub any concerns about the changes here? The local methods seem ugly, but as discussed, we seem to be missing a reusable type here for this case. |
Trapov
commented
May 11, 2022
@danmoseley added NewLine to the params ( BenchmarkDotNet=v0.13.1.1786-nightly, OS=macOS Monterey 12.3.1 (21E258) [Darwin 21.4.0]
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-preview.3.22179.4
[Host] : .NET 7.0.0 (7.0.22.17504), X64 RyuJIT
Job-HKXHTD : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-RJNYWV : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
|
Trapov
commented
May 11, 2022
We could fall-back to initial while loop if the line is short. We need to determine which is short tho. Something around 5-16 chars. |
danmoseley
commented
May 12, 2022
How do you know how long the line is without searching in the first place? It’s just an assumption we have to make that files are mostly content not newline characters. |
Trapov
commented
May 12, 2022
Sorry, I meant running the search loop for 10 chars and then switching back to index of any |
Trapov
commented
May 12, 2022
@danmoseley that's what I meant. My naive solution (didn't push it) but what I don't understand why it's allocating 74 kb like that. publicoverridestring?ReadLine(){ThrowIfDisposed();CheckAsyncTaskInProgress();if(_charPos==_charLen){if(ReadBuffer()==0){returnnull;}}// fast path(intindexStart,intlength)=(_charPos,_charLen-_charPos);intindexOfNewLine=-1;intfastPathMaxLength=length%9;Span<char>span=_charBuffer.AsSpan(indexStart,fastPathMaxLength);intindex=fastPathMaxLength-1;while(index>-1){char@char=span[index];if(@charis'\r' or '\n'){indexOfNewLine=index;break;}index--;}if(indexOfNewLine!=-1){strings=newstring(_charBuffer,indexStart,indexOfNewLine);charch=_charBuffer[_charPos+indexOfNewLine];_charPos+=indexOfNewLine+1;// if it is two chars for the new-line character then inc the _charPosif(ch=='\r'&&(_charPos<_charLen||ReadBuffer()>0)){if(_charBuffer[_charPos]=='\n'){_charPos++;}}returns;}usingValueStringBuildersb=new(stackallocchar[256]);do{// Note the following common line feed chars:// \n - UNIX/Mac \r\n - Windows(indexStart,length)=(_charPos,_charLen-_charPos);indexOfNewLine=_charBuffer.AsSpan(indexStart,length).IndexOfAny('\r','\n');if(indexOfNewLine>=0){strings;if(sb.Length>0){sb.Append(_charBuffer.AsSpan(indexStart,indexOfNewLine));s=sb.ToString();}else{s=newstring(_charBuffer,indexStart,indexOfNewLine);}charch=_charBuffer[_charPos+indexOfNewLine];_charPos+=indexOfNewLine+1;// if it is two chars for the new-line character then inc the _charPosif(ch=='\r'&&(_charPos<_charLen||ReadBuffer()>0)){if(_charBuffer[_charPos]=='\n'){_charPos++;}}returns;}inttotalRead=_charLen-_charPos;sb.Append(_charBuffer.AsSpan(_charPos,totalRead));}while(ReadBuffer()>0);returnsb.ToString();}BenchmarkDotNet=v0.13.1.1786-nightly, OS=macOS Monterey 12.3.1 (21E258) [Darwin 21.4.0]
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-preview.3.22179.4
[Host] : .NET 7.0.0 (7.0.22.17504), X64 RyuJIT
Job-QWLAYE : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-PVIZJQ : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
|
danmoseley
commented
May 13, 2022
I don't think you can have a faster path here without making shaky assumptions. I think just IndexOfAny is good. |
@danmoseley Do we need something else in this PR or we still waiting on something? (I don't know what to change) |
Trapov
commented
Jun 11, 2022
@danmoseley@adamsitnik any update on the PR? |
danmoseley
commented
Jun 11, 2022
oops, overlooked this one. @stephentoub do you have any remaining concerns about the approach (I do think we need to stop returning to pool on array) |
stephentoub
commented
Jun 11, 2022
This is going to conflict with #69888. @GrabYourPitchforks' change is fixing some functional issues in addition to perf optimizations. I know this PR has been open longer, but I'd like to land @GrabYourPitchforks' change first and then revisit this. |
Trapov
commented
Jun 12, 2022
I need to look into #69888 to merge it correctly then, I guess. Tag me, please, when it's going to be merged? |
Trapov
commented
Aug 30, 2022
Any updates? @danmoseley |
danmoseley
commented
Aug 30, 2022
@Trapov unfortunately #69888 was paused for a while as @GrabYourPitchforks was working on something else. That's been picked up again, although I see there's test failures. So unfortunately, this remains waiting.. |
Trapov
commented
Aug 30, 2022
No problem, just tag me please when it's going to be merged. @danmoseley |
stephentoub
commented
Nov 7, 2022
@Trapov, thanks for your patience and for working on this. #69888 is about to be merged, but at this point I think it also covers most or all of what you were trying to accomplish in this PR. If there's anything additional to be done, once that's merged you can rebase and filter this down to just the incremental portions. Otherwise, we can probably close it. Again, thank you. |
@stephentoub it covers mostly everything as you said. so probably better to close this PR. |
stephentoub
commented
Nov 7, 2022
Ok. Thanks very much, @Trapov. |
No description provided.