') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); S.IO.StringReader: Use ReadOnlySpan.IndexOfAny in ReadLine() for performance by nietras · Pull Request #60463 · dotnet/runtime · GitHub
Skip to content

S.IO.StringReader: Use ReadOnlySpan.IndexOfAny in ReadLine() for performance - #60463

Merged
adamsitnik merged 16 commits into
dotnet:mainfrom
nietras:stringreader-use-indexofany-in-readline
Dec 8, 2021
Merged

S.IO.StringReader: Use ReadOnlySpan.IndexOfAny in ReadLine() for performance#60463
adamsitnik merged 16 commits into
dotnet:mainfrom
nietras:stringreader-use-indexofany-in-readline

Conversation

@nietras

Copy link
Copy Markdown
Contributor

Have not done benchmarks of this yet, as I wanted to know first if this could be an acceptable change. The premise is IndexOfAny is highly optimized and uses vectorization if possible. Also untested.

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.IO labels Oct 15, 2021
@ghost

Copy link
Copy Markdown

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

Issue Details

Have not done benchmarks of this yet, as I wanted to know first if this could be an acceptable change. The premise is IndexOfAny is highly optimized and uses vectorization if possible. Also untested.

Author:nietras
Assignees:-
Labels:

area-System.IO, community-contribution

Milestone:-

@adamsitnikadamsitnik added the tenet-performance Performance related issue label Oct 15, 2021
@adamsitnik

Copy link
Copy Markdown
Member

It sounds reasonable as long as there is no regression for relatively short lines (20 characters?) that might be common.

@danmoseley

Copy link
Copy Markdown
Contributor

You might consider reviewing/augmenting the coverage for this in dotnet/performance first. Then using that to evaluate it.

@nietras

Copy link
Copy Markdown
ContributorAuthor

@adamsitnik code size will of course increase if you count IndexOfAny, but agree tests should show small line length perf differences.

@danmoseley unfortunately I cannot find any benchmarks of StringReader in https://github.com/dotnet/performance/

Can you guys confirm this? I guess I will add a benchmark for this under MicroBenchmarks in that case.

@nietras

Copy link
Copy Markdown
ContributorAuthor

Guessing location of test should be something like C:\git\oss\performance\src\benchmarks\micro\libraries\System.IO e.g. StringReaderReadLineTests.cs.

@danmoseley

Copy link
Copy Markdown
Contributor

@nietras yes, it looks like there are none (I didn't check earlier as I was on my phone). Yes, that would be the place to put some. Seems like they need not be that elaborate.

@nietras

Copy link
Copy Markdown
ContributorAuthor

Added benchmark in dotnet/performance#2083 and repeating results here.

Even for small line lengths [1, 8] there is a minor speedup (4%) and hence no regressions. For empty lines e.g. just new lines, there is a 40% regression, though.

For longer lines we see a nice 2-3x speed up.

It might be possible to do something about the empty line regression, but perhaps that is not so important?

BenchmarkDotNet=v0.13.1.1611-nightly, OS=Windows 10.0.19043.1266 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-BIEWJM : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-IFXICU : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Allocated
ReadLineJob-BIEWJMruntime-m[ 0, 0]7.208 ns0.0556 ns0.0434 ns7.194 ns7.165 ns7.288 ns1.000.00--
ReadLineJob-IFXICUruntime-pr[ 0, 0]10.112 ns0.1781 ns0.1666 ns10.105 ns9.875 ns10.438 ns1.410.02--
ReadLineJob-BIEWJMruntime-m[ 1, 8]17.751 ns0.2287 ns0.2028 ns17.686 ns17.552 ns18.192 ns1.000.000.002033 B
ReadLineJob-IFXICUruntime-pr[ 1, 8]16.998 ns0.1011 ns0.0946 ns16.992 ns16.872 ns17.212 ns0.960.010.002033 B
ReadLineJob-BIEWJMruntime-m[ 9, 32]26.336 ns0.1205 ns0.1127 ns26.273 ns26.200 ns26.510 ns1.000.000.003965 B
ReadLineJob-IFXICUruntime-pr[ 9, 32]19.896 ns0.0740 ns0.0618 ns19.879 ns19.801 ns20.046 ns0.760.000.003865 B
ReadLineJob-BIEWJMruntime-m[ 33, 128]62.594 ns0.2100 ns0.1861 ns62.584 ns62.222 ns62.928 ns1.000.000.0108185 B
ReadLineJob-IFXICUruntime-pr[ 33, 128]31.021 ns0.0902 ns0.0800 ns31.015 ns30.912 ns31.200 ns0.500.000.0110185 B
ReadLineJob-BIEWJMruntime-m[ 129,1024]319.244 ns0.6595 ns0.6169 ns319.302 ns318.088 ns320.391 ns1.000.000.06971,181 B
ReadLineJob-IFXICUruntime-pr[ 129,1024]98.174 ns0.2733 ns0.2282 ns98.123 ns97.705 ns98.567 ns0.310.000.07051,181 B

@nietras

Copy link
Copy Markdown
ContributorAuthor

ReadLine code has the following:

publicoverridestring?ReadLine(){if(_s==null){thrownewObjectDisposedException(null,SR.ObjectDisposed_ReaderClosed);}

would it be of any benefit to use ThrowHelper here? Or is that a mute point now?

@stephentoub

Copy link
Copy Markdown
Member

Even for small line lengths [1, 8] there is a minor speedup (4%) and hence no regressions. For empty lines e.g. just new lines, there is a 40% regression, though.

I'm not surprised by line length 0. I am surprised by the line length 1. Do we have an explanation for why that gets faster?

@nietras

Copy link
Copy Markdown
ContributorAuthor

I'm not surprised by line length 0. I am surprised by the line length 1. Do we have an explanation for why that gets faster?

Me neither on line length 0. Although I have improved that a bit. The 1 to 8 line length case still has som lines of 8 + 2 = 10 chars or 20 bytes < 16 bytes length, so we still hit 128-bit paths and SSE2 in some percentage right. Additionally, the IndexOfAny has optimized code for >= 4 chars, which is more than 50% of cases.

Probably, 1 character only would be slower too. I can run a test for that if you like?

@nietras

Copy link
Copy Markdown
ContributorAuthor

Improved regression a little bit by special casing line length 0 to avoid Substring overhead. Also added ThrowHelper.

Additionally, added line length 1 case. 6% regression. No doubt still dominated by Substring and new string here, so overhead of calling IndexOfAny not that big an issue, perhaps.

BenchmarkDotNet=v0.13.1.1611-nightly, OS=Windows 10.0.19043.1266 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-BZJQTB : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-PFAYGN : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioGen 0Allocated
ReadLineJob-BZJQTBm[ 0, 0]7.371 ns0.0943 ns0.0883 ns7.402 ns7.212 ns7.498 ns1.00--
ReadLineJob-PFAYGNpr[ 0, 0]9.472 ns0.1117 ns0.0990 ns9.471 ns9.350 ns9.608 ns1.28--
ReadLineJob-BZJQTBm[ 0, 1024]292.841 ns1.4690 ns1.3741 ns292.241 ns291.296 ns295.981 ns1.000.06171,049 B
ReadLineJob-PFAYGNpr[ 0, 1024]90.232 ns0.5357 ns0.5011 ns90.073 ns89.500 ns91.097 ns0.310.06251,049 B
ReadLineJob-BZJQTBm[ 1, 1]13.278 ns0.1174 ns0.1098 ns13.291 ns13.068 ns13.433 ns1.000.001424 B
ReadLineJob-PFAYGNpr[ 1, 1]14.013 ns0.0911 ns0.0853 ns13.998 ns13.897 ns14.180 ns1.060.001424 B
ReadLineJob-BZJQTBm[ 1, 8]18.093 ns0.1064 ns0.0996 ns18.079 ns17.968 ns18.277 ns1.000.001933 B
ReadLineJob-PFAYGNpr[ 1, 8]16.986 ns0.0912 ns0.0853 ns16.950 ns16.884 ns17.136 ns0.940.002033 B
ReadLineJob-BZJQTBm[ 9, 32]27.359 ns0.1525 ns0.1427 ns27.381 ns27.056 ns27.582 ns1.000.003965 B
ReadLineJob-PFAYGNpr[ 9, 32]20.399 ns0.1372 ns0.1216 ns20.407 ns20.208 ns20.590 ns0.750.003865 B
ReadLineJob-BZJQTBm[ 33, 128]64.000 ns0.3802 ns0.3557 ns64.061 ns63.476 ns64.639 ns1.000.0110185 B
ReadLineJob-PFAYGNpr[ 33, 128]31.459 ns0.2157 ns0.2018 ns31.485 ns31.102 ns31.758 ns0.490.0110185 B
ReadLineJob-BZJQTBm[ 129, 1024]326.246 ns1.2682 ns1.1863 ns325.828 ns324.743 ns328.814 ns1.000.07041,181 B
ReadLineJob-PFAYGNpr[ 129, 1024]98.230 ns0.7595 ns0.7104 ns97.891 ns97.538 ns99.812 ns0.300.07041,181 B

@nietras

Copy link
Copy Markdown
ContributorAuthor

Using Environment.NewLine. Line length 1 sees larger regression now inline with length 0, so my previous comment about new string might be incorrect.

BenchmarkDotNet=v0.13.1.1611-nightly, OS=Windows 10.0.19043.1266 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-LBAYRI : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-QZQWHF : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Allocated
ReadLineJob-LBAYRIm[ 0, 0]3.602 ns0.0187 ns0.0175 ns3.601 ns3.578 ns3.636 ns1.000.00--
ReadLineJob-QZQWHFpr[ 0, 0]4.538 ns0.0529 ns0.0469 ns4.538 ns4.399 ns4.593 ns1.260.01--
ReadLineJob-LBAYRIm[ 0, 1024]290.351 ns0.8590 ns0.8035 ns290.511 ns289.016 ns291.381 ns1.000.000.06211,045 B
ReadLineJob-QZQWHFpr[ 0, 1024]86.508 ns0.4516 ns0.3771 ns86.576 ns85.995 ns87.273 ns0.300.000.06241,045 B
ReadLineJob-LBAYRIm[ 1, 1]8.032 ns0.1276 ns0.1194 ns8.067 ns7.786 ns8.233 ns1.000.000.001424 B
ReadLineJob-QZQWHFpr[ 1, 1]10.258 ns0.0778 ns0.0689 ns10.254 ns10.153 ns10.393 ns1.280.020.001424 B
ReadLineJob-LBAYRIm[ 1, 8]16.362 ns0.1832 ns0.1714 ns16.300 ns16.151 ns16.691 ns1.000.000.001933 B
ReadLineJob-QZQWHFpr[ 1, 8]14.091 ns0.1842 ns0.1633 ns14.075 ns13.780 ns14.408 ns0.860.010.002033 B
ReadLineJob-LBAYRIm[ 9, 32]25.329 ns0.1646 ns0.1540 ns25.380 ns25.096 ns25.568 ns1.000.000.003865 B
ReadLineJob-QZQWHFpr[ 9, 32]17.268 ns0.0570 ns0.0533 ns17.280 ns17.174 ns17.356 ns0.680.000.003865 B
ReadLineJob-LBAYRIm[ 33, 128]61.464 ns0.1807 ns0.1602 ns61.445 ns61.241 ns61.771 ns1.000.000.0108185 B
ReadLineJob-QZQWHFpr[ 33, 128]28.177 ns0.1248 ns0.1167 ns28.221 ns27.878 ns28.306 ns0.460.000.0110185 B
ReadLineJob-LBAYRIm[ 129, 1024]321.899 ns0.9247 ns0.8650 ns321.595 ns320.682 ns323.301 ns1.000.000.07021,175 B
ReadLineJob-QZQWHFpr[ 129, 1024]96.303 ns0.5141 ns0.4557 ns96.215 ns95.688 ns97.331 ns0.300.000.06991,175 B

@stephentoub

stephentoub commented Oct 17, 2021

Copy link
Copy Markdown
Member

The 1 to 8 line length case still has som lines of 8 + 2 = 10 chars or 20 bytes < 16 bytes length, so we still hit 128-bit paths and SSE2 in some percentage right. Additionally, the IndexOfAny has optimized code for >= 4 chars, which is more than 50% of cases. Probably, 1 character only would be slower too. I can run a test for that if you like?

Yes, when I commented I hadn't realized your comment about length 1 was about random selections between 1 and 8.

I think this change is still worth taking: it simplifies the code, and improves what's expected to be the majority case. I just want to make sure we understand the regressions before doing so. (And I do think 0 is worth special-casing if it avoids regressing that case while not measurably impacting others; I haven't looked at the change yet, though)

@nietras

Copy link
Copy Markdown
ContributorAuthor

@stephentoub replacing Substring with new string(ReadOnlySpan<char>) does not appear to help on tiny lines, thought it might given Substring does a bunch of checks. I'd probably still use this, as I assume code size should be smaller.

Since we have context here creating the span for 1 char line could side step the checks of the Span.Slice...

BenchmarkDotNet=v0.13.1.1611-nightly, OS=Windows 10.0.19043.1266 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-VAAVCB : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-XGNHNV : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Allocated
ReadLineJob-VAAVCBm[ 0, 0]3.377 ns0.0260 ns0.0231 ns3.367 ns3.348 ns3.418 ns1.000.00--
ReadLineJob-XGNHNVpr[ 0, 0]4.503 ns0.0447 ns0.0418 ns4.503 ns4.419 ns4.568 ns1.330.02--
ReadLineJob-VAAVCBm[ 0, 1024]284.428 ns2.1638 ns1.8068 ns283.843 ns282.711 ns288.775 ns1.000.000.06191,045 B
ReadLineJob-XGNHNVpr[ 0, 1024]84.528 ns0.6573 ns0.6149 ns84.244 ns83.892 ns85.629 ns0.300.000.06241,045 B
ReadLineJob-VAAVCBm[ 1, 1]7.780 ns0.1162 ns0.1087 ns7.819 ns7.601 ns7.940 ns1.000.000.001424 B
ReadLineJob-XGNHNVpr[ 1, 1]9.962 ns0.0544 ns0.0424 ns9.972 ns9.835 ns9.996 ns1.270.010.001424 B
ReadLineJob-VAAVCBm[ 1, 8]16.026 ns0.1693 ns0.1501 ns16.019 ns15.709 ns16.328 ns1.000.000.001933 B
ReadLineJob-XGNHNVpr[ 1, 8]13.556 ns0.0395 ns0.0308 ns13.558 ns13.497 ns13.612 ns0.840.010.001933 B
ReadLineJob-VAAVCBm[ 9, 32]25.054 ns0.0973 ns0.0910 ns25.013 ns24.947 ns25.238 ns1.000.000.003965 B
ReadLineJob-XGNHNVpr[ 9, 32]16.762 ns0.1132 ns0.1059 ns16.761 ns16.624 ns16.922 ns0.670.000.003965 B
ReadLineJob-VAAVCBm[ 33, 128]60.175 ns0.2383 ns0.2112 ns60.153 ns59.796 ns60.608 ns1.000.000.0110185 B
ReadLineJob-XGNHNVpr[ 33, 128]27.256 ns0.2427 ns0.2027 ns27.213 ns26.944 ns27.614 ns0.450.000.0110185 B
ReadLineJob-VAAVCBm[ 129, 1024]326.478 ns3.8631 ns3.6135 ns327.742 ns318.213 ns330.983 ns1.000.000.07001,175 B
ReadLineJob-XGNHNVpr[ 129, 1024]92.774 ns0.5438 ns0.5087 ns92.504 ns92.264 ns93.762 ns0.280.000.07001,175 B

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
@nietras

Copy link
Copy Markdown
ContributorAuthor

Latest benchmark results. No significant changes. Line lengths 0 (+1.2ns) and 1 (+2ns) still have ~30% regressions, all others improvements, going up to 3x for [129, 1024] with -220ns.

BenchmarkDotNet=v0.13.1.1611-nightly, OS=Windows 10.0.19043.1266 (21H1/May2021Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-ZPUFRH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-ZTWPGG : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainLineLengthRangeMeanErrorStdDevMedianMinMaxRatioGen 0Allocated
ReadLineJob-ZPUFRHm[ 0, 0]3.667 ns0.0277 ns0.0245 ns3.665 ns3.637 ns3.723 ns1.00--
ReadLineJob-ZTWPGGpr[ 0, 0]4.862 ns0.0306 ns0.0271 ns4.852 ns4.822 ns4.912 ns1.33--
ReadLineJob-ZPUFRHm[ 0, 1024]279.798 ns1.6451 ns1.5388 ns279.602 ns276.440 ns282.489 ns1.000.06211,045 B
ReadLineJob-ZTWPGGpr[ 0, 1024]85.326 ns0.4640 ns0.4113 ns85.207 ns84.769 ns86.083 ns0.300.06221,045 B
ReadLineJob-ZPUFRHm[ 1, 1]7.437 ns0.0750 ns0.0665 ns7.435 ns7.271 ns7.572 ns1.000.001424 B
ReadLineJob-ZTWPGGpr[ 1, 1]9.588 ns0.0712 ns0.0666 ns9.597 ns9.451 ns9.710 ns1.290.001424 B
ReadLineJob-ZPUFRHm[ 1, 8]15.502 ns0.1362 ns0.1207 ns15.523 ns15.278 ns15.717 ns1.000.001933 B
ReadLineJob-ZTWPGGpr[ 1, 8]13.196 ns0.0994 ns0.0881 ns13.193 ns13.058 ns13.361 ns0.850.001933 B
ReadLineJob-ZPUFRHm[ 9, 32]23.798 ns0.1131 ns0.1002 ns23.804 ns23.660 ns23.962 ns1.000.003865 B
ReadLineJob-ZTWPGGpr[ 9, 32]16.729 ns0.1849 ns0.1639 ns16.760 ns16.508 ns17.046 ns0.700.003965 B
ReadLineJob-ZPUFRHm[ 33, 128]58.583 ns0.2611 ns0.2442 ns58.465 ns58.337 ns59.034 ns1.000.0110185 B
ReadLineJob-ZTWPGGpr[ 33, 128]27.529 ns0.0886 ns0.0829 ns27.545 ns27.332 ns27.625 ns0.470.0110185 B
ReadLineJob-ZPUFRHm[ 129, 1024]313.442 ns2.0088 ns1.8790 ns314.089 ns308.542 ns315.778 ns1.000.06911,175 B
ReadLineJob-ZTWPGGpr[ 129, 1024]94.201 ns0.8138 ns0.7612 ns93.789 ns93.577 ns95.612 ns0.300.07001,175 B

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated

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

Thanks so much!

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@nietras You and I had some discussion on this issue regarding potential improvements to string.Substring. If you want to try taking those on as a new PR, please feel free. Otherwise we can add it to our own backlog of future improvements.

Thanks again for this! It looks great. :)

@danmoseley

Copy link
Copy Markdown
Contributor

@stephentoub any remaining feedback or can we merge? the failure is in HTTP tests which I will investigate.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Comment threadsrc/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs Outdated
nietrasand others added 4 commits December 7, 2021 23:03
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
@nietras

Copy link
Copy Markdown
ContributorAuthor

@stephentoub hope comments have been resolved, PTAL.

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

Thanks!

@adamsitnikadamsitnik added this to the 7.0.0 milestone Dec 8, 2021
@adamsitnik
adamsitnik merged commit 836f2c5 into dotnet:mainDec 8, 2021
@nietras

Copy link
Copy Markdown
ContributorAuthor

@GrabYourPitchforks regarding Substring, there are only a limited number of things one can try to improve this, one of which would be to try and optimize the guard clauses. This would perhaps mean changing the exception messages (not types), since we would like to do multiple checks in one go for example, is that acceptable?

Other than that one might specialize for short substrings... if that would improve anything.

@adamsitnik the existing performance Benchmarks do not look that useful, at least in my runs they are highly volatile and seem to be based on JIT compiling away all checks... maybe... would adding other tests be acceptable? Perhaps you can take a look and tell me if I'm wrong, but use of Arguments seems problematic.

Is there any guidance anywhere on getting a good dev inner loop for looking at corelib asm? How to use BDN in that case? A bit rusty here.

@adamsitnik

Copy link
Copy Markdown
Member

the existing performance Benchmarks do not look that useful,

do you mean these benchmarks?

would adding other tests be acceptable?

yes, of course! The existing string benchmarks are far from perfect, please fell free to add more.

Is there any guidance anywhere on getting a good dev inner loop for looking at corelib asm? How to use BDN in that case?

For managed part of the corelib you should be able to use disassembly diagnoser with corerun: https://github.com/dotnet/performance/blob/797425cdc4dcbc407f4546fd83e00d79b431ea09/docs/benchmarkdotnet.md#disassembly

You can also use VTune: https://github.com/dotnet/performance/blob/5d6fc749333ce7d7e34e6d0ac311df67c5d2047c/docs/profiling-workflow-dotnet-runtime.md#vtune

or use one of tools provided by the Jit Team. @kunalspathak@EgorBo@AndyAyersMS what is your preferred way of getting the disassembly?

@kunalspathak

Copy link
Copy Markdown
Contributor

I would use vTune to profile and also see the instructions in the disassembly that are hot. If you want to use JitDisasm and JitDump flags, then you can set those environment variables with respective method names and use dotnet MicroBenchmarks.dll with --corerun path\to\checked\corerun.

@nietras
nietras deleted the stringreader-use-indexofany-in-readline branch December 9, 2021 13:17
@nietras

Copy link
Copy Markdown
ContributorAuthor

@kunalspathak@adamsitnik thanks both for the pointers, I'll try to see if I can figure this out! 😅

do you mean these benchmarks?

Yes, an example run below where m and pr are the exact same commit. Here up to 50% difference. I have observed up 80% divergences.

BDN notes one of these is multi-modal.

Additionally, Substring(0) is reported to take 1 ns which seems a bit suspicious to me, that's 5 cycles. Yes it only has to return the same string, but still. Is the call inlined and the JIT fully compiles things away since the start is "hard-coded"?

If you have any suggestions on how to make these more stable I am all ears. One thing I want is better coverage of short substrings e.g. sizes 0-8 or similar.

// * Warnings *
MultimodalDistribution
Perf_String.Substring_Int: PowerPlanMode=00000000-0000-0000-0000-000000000000, Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog, Toolchain=\runtime-pr\artifacts\bin\testhost\net7.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0\CoreRun.exe, IterationTime=250.0000 ms, MaxIterationCount=20, MinIterationCount=15, WarmupCount=1 -> It seems that the distribution can have several modes (mValue = 3)
BenchmarkDotNet=v0.13.1.1620-nightly, OS=Windows 10.0.19044.1348 (21H2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=7.0.100-alpha.1.21568.2
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
Job-ZIOPOT : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
Job-ESNSIZ : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT
PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog IterationTime=250.0000 ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1 
MethodJobToolchainsi1i2iMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Allocated
Substring_IntIntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz08?13.046 ns0.6042 ns0.6958 ns13.142 ns10.866 ns14.260 ns1.000.000.002440 B
Substring_IntIntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz08?12.091 ns0.7554 ns0.8700 ns12.326 ns9.890 ns13.647 ns0.930.090.002440 B
Substring_IntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz??01.092 ns0.0024 ns0.0023 ns1.091 ns1.088 ns1.096 ns1.000.00--
Substring_IntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz??01.094 ns0.0066 ns0.0061 ns1.094 ns1.085 ns1.105 ns1.000.01--
Substring_IntIntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz74?9.119 ns0.7003 ns0.8065 ns9.210 ns7.828 ns10.840 ns1.000.000.001932 B
Substring_IntIntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz74?8.439 ns0.5301 ns0.6104 ns8.404 ns7.452 ns9.690 ns0.930.090.001932 B
Substring_IntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz??78.947 ns0.5383 ns0.5983 ns8.964 ns7.750 ns10.128 ns1.000.000.002948 B
Substring_IntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz??79.384 ns0.7757 ns0.8934 ns9.538 ns7.507 ns10.890 ns1.040.090.002848 B
Substring_IntIntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz101?5.125 ns0.1145 ns0.1071 ns5.120 ns4.950 ns5.324 ns1.000.000.001424 B
Substring_IntIntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz101?7.813 ns0.5706 ns0.6105 ns7.760 ns6.692 ns9.071 ns1.530.140.001424 B
Substring_IntJob-ZIOPOTmdzsdzsDDZSDZSDZSddsz??1011.729 ns0.9097 ns1.0112 ns11.659 ns8.977 ns13.597 ns1.000.000.002848 B
Substring_IntJob-ESNSIZprdzsdzsDDZSDZSDZSddsz??109.645 ns0.6790 ns0.7819 ns9.837 ns8.141 ns10.912 ns0.830.120.002848 B

@ghostghost locked as resolved and limited conversation to collaborators Jan 8, 2022
@EgorBo

Copy link
Copy Markdown
Member

Improvement on ubuntu-x64 dotnet/perf-autofiling-issues#2670

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community membertenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@nietras@adamsitnik@danmoseley@stephentoub@GrabYourPitchforks@kunalspathak@EgorBo