Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations - #104365

Merged
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack
Jul 16, 2024
Merged

Reimplement LINQ ToList using SegmentedArrayBuilder to reduce allocations#104365
eiriktsarpalis merged 7 commits into
dotnet:mainfrom
andrewjsaid:linq-tolist-stack

Conversation

@andrewjsaid

@andrewjsaidandrewjsaid commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

After #96570, ToArray() in many places is faster and has fewer allocations that ToList(). As a List is just a wrapper over an array, we can re-use most of the logic in SegmentedArrayBuilder to build Lists, too.

There were some more complex cases using SegmentedArrayBuilder for example Concat where I left the code as-is. I can also experiment with performance numbers for those if it is requested.

Benchmarks

Enumerable_ToList changes have been reverted, they are not in current PR version

Int32Tests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain125.87 ns1.00112 B1.00
Enumerable_ToListpr128.49 ns1.10104 B0.93
IEnumerableSelectIterator_ToListmain146.05 ns1.00168 B1.00
IEnumerableSelectIterator_ToListpr148.76 ns1.06160 B0.95
IteratorSelectIterator_ToListmain155.84 ns1.00184 B1.00
IteratorSelectIterator_ToListpr160.04 ns1.08184 B1.00
OfTypeIterator_ToListmain161.88 ns1.00200 B1.00
OfTypeIterator_ToListpr165.08 ns1.05176 B0.88
SelectManySingleSelectorIterator_ToListmain162.16 ns1.00240 B1.00
SelectManySingleSelectorIterator_ToListpr167.54 ns1.08232 B0.97
IEnumerableSkipTakeIterator_ToLostmain141.68 ns1.00128 B1.00
IEnumerableSkipTakeIterator_ToLostpr143.83 ns1.05128 B1.00
IEnumerableWhereIterator_ToListmain142.11 ns1.00168 B1.00
IEnumerableWhereIterator_ToListpr142.63 ns1.01160 B0.95
ArrayWhereIterator_ToListmain138.62 ns1.00152 B1.00
ArrayWhereIterator_ToListpr145.68 ns1.18144 B0.95
IEnumerableWhereSelectIterator_ToListmain160.61 ns1.00232 B1.00
IEnumerableWhereSelectIterator_ToListpr165.19 ns1.10224 B0.97
ArrayWhereSelectIterator_ToListmain155.79 ns1.00208 B1.00
ArrayWhereSelectIterator_ToListpr160.01 ns1.07200 B0.96
Enumerable_ToListmain544.33 ns1.00168 B1.00
Enumerable_ToListpr532.99 ns0.74120 B0.71
IEnumerableSelectIterator_ToListmain565.96 ns1.00224 B1.00
IEnumerableSelectIterator_ToListpr556.28 ns0.85176 B0.79
IteratorSelectIterator_ToListmain573.91 ns1.00224 B1.00
IteratorSelectIterator_ToListpr577.94 ns1.05224 B1.00
OfTypeIterator_ToListmain5132.67 ns1.00384 B1.00
OfTypeIterator_ToListpr5115.63 ns0.87304 B0.79
SelectManySingleSelectorIterator_ToListmain5122.24 ns1.00296 B1.00
SelectManySingleSelectorIterator_ToListpr5106.91 ns0.88248 B0.84
IEnumerableSkipTakeIterator_ToLostmain556.19 ns1.00168 B1.00
IEnumerableSkipTakeIterator_ToLostpr558.61 ns1.04168 B1.00
IEnumerableWhereIterator_ToListmain562.12 ns1.00224 B1.00
IEnumerableWhereIterator_ToListpr550.66 ns0.81176 B0.79
ArrayWhereIterator_ToListmain556.66 ns1.00224 B1.00
ArrayWhereIterator_ToListpr549.61 ns0.88176 B0.79
IEnumerableWhereSelectIterator_ToListmain582.31 ns1.00288 B1.00
IEnumerableWhereSelectIterator_ToListpr572.20 ns0.88240 B0.83
ArrayWhereSelectIterator_ToListmain574.32 ns1.00280 B1.00
ArrayWhereSelectIterator_ToListpr562.94 ns0.85232 B0.83
Enumerable_ToListmain50167.20 ns1.00688 B1.00
Enumerable_ToListpr50118.14 ns0.71296 B0.43
IEnumerableSelectIterator_ToListmain50191.60 ns1.00744 B1.00
IEnumerableSelectIterator_ToListpr50171.93 ns0.90352 B0.47
IteratorSelectIterator_ToListmain50251.54 ns1.00800 B1.00
IteratorSelectIterator_ToListpr50231.18 ns0.92408 B0.51
OfTypeIterator_ToListmain50731.40 ns1.002432 B1.00
OfTypeIterator_ToListpr50734.97 ns1.011744 B0.72
SelectManySingleSelectorIterator_ToListmain50692.19 ns1.00816 B1.00
SelectManySingleSelectorIterator_ToListpr50656.03 ns0.95424 B0.52
IEnumerableSkipTakeIterator_ToLostmain50186.72 ns1.00744 B1.00
IEnumerableSkipTakeIterator_ToLostpr50161.33 ns0.86352 B0.47
IEnumerableWhereIterator_ToListmain50187.08 ns1.00744 B1.00
IEnumerableWhereIterator_ToListpr50164.58 ns0.88352 B0.47
ArrayWhereIterator_ToListmain50154.86 ns1.00920 B1.00
ArrayWhereIterator_ToListpr50141.83 ns0.92528 B0.57
IEnumerableWhereSelectIterator_ToListmain50209.25 ns1.00808 B1.00
IEnumerableWhereSelectIterator_ToListpr50186.49 ns0.89416 B0.51
ArrayWhereSelectIterator_ToListmain50171.83 ns1.00976 B1.00
ArrayWhereSelectIterator_ToListpr50145.71 ns0.85584 B0.60
ObjectTests

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3810/23H2/2023Update/SunValley3)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.302
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
Job-YPZLVD : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Job-OASBAG : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
LaunchCount=3 
MethodCountMeanRatioAllocatedAlloc Ratio
Enumerable_ToListmain141.15 ns1.00136 B1.00
Enumerable_ToListpr145.14 ns1.10112 B0.82
IEnumerableSelectIterator_ToListmain168.84 ns1.00192 B1.00
IEnumerableSelectIterator_ToListpr168.88 ns1.01168 B0.88
IteratorSelectIterator_ToListmain175.90 ns1.00192 B1.00
IteratorSelectIterator_ToListpr184.56 ns1.11192 B1.00
OfTypeIterator_ToListmain157.58 ns1.00184 B1.00
OfTypeIterator_ToListpr161.52 ns1.07160 B0.87
SelectManySingleSelectorIterator_ToListmain188.42 ns1.00264 B1.00
SelectManySingleSelectorIterator_ToListpr189.23 ns1.00240 B0.91
IEnumerableSkipTakeIterator_ToLostmain155.73 ns1.00136 B1.00
IEnumerableSkipTakeIterator_ToLostpr160.91 ns1.09136 B1.00
IEnumerableWhereIterator_ToListmain166.58 ns1.00192 B1.00
IEnumerableWhereIterator_ToListpr170.11 ns1.05168 B0.88
ArrayWhereIterator_ToListmain148.82 ns1.00168 B1.00
ArrayWhereIterator_ToListpr156.08 ns1.15144 B0.86
IEnumerableWhereSelectIterator_ToListmain187.87 ns1.00256 B1.00
IEnumerableWhereSelectIterator_ToListpr190.83 ns1.03232 B0.91
ArrayWhereSelectIterator_ToListmain170.01 ns1.00224 B1.00
ArrayWhereSelectIterator_ToListpr173.59 ns1.05200 B0.89
Enumerable_ToListmain584.91 ns1.00224 B1.00
Enumerable_ToListpr571.13 ns0.83144 B0.64
IEnumerableSelectIterator_ToListmain5168.50 ns1.00280 B1.00
IEnumerableSelectIterator_ToListpr5153.26 ns0.91200 B0.71
IteratorSelectIterator_ToListmain5121.77 ns1.00248 B1.00
IteratorSelectIterator_ToListpr5187.19 ns1.54248 B1.00
OfTypeIterator_ToListmain5101.74 ns1.00272 B1.00
OfTypeIterator_ToListpr588.26 ns0.87192 B0.71
SelectManySingleSelectorIterator_ToListmain5187.48 ns1.00352 B1.00
SelectManySingleSelectorIterator_ToListpr5153.98 ns0.82272 B0.77
IEnumerableSkipTakeIterator_ToLostmain591.36 ns1.00192 B1.00
IEnumerableSkipTakeIterator_ToLostpr594.57 ns1.04192 B1.00
IEnumerableWhereIterator_ToListmain5170.63 ns1.00280 B1.00
IEnumerableWhereIterator_ToListpr5154.21 ns0.90200 B0.71
ArrayWhereIterator_ToListmain5144.74 ns1.00288 B1.00
ArrayWhereIterator_ToListpr5135.21 ns0.94208 B0.72
IEnumerableWhereSelectIterator_ToListmain5191.12 ns1.00344 B1.00
IEnumerableWhereSelectIterator_ToListpr5176.70 ns0.93264 B0.77
ArrayWhereSelectIterator_ToListmain5166.66 ns1.00344 B1.00
ArrayWhereSelectIterator_ToListpr5149.40 ns0.90264 B0.77
Enumerable_ToListmain50452.81 ns1.001192 B1.00
Enumerable_ToListpr50429.92 ns0.94504 B0.42
IEnumerableSelectIterator_ToListmain50543.63 ns1.001248 B1.00
IEnumerableSelectIterator_ToListpr50552.12 ns1.02560 B0.45
IteratorSelectIterator_ToListmain50701.63 ns1.001304 B1.00
IteratorSelectIterator_ToListpr50707.15 ns1.01608 B0.47
OfTypeIterator_ToListmain50499.60 ns1.001240 B1.00
OfTypeIterator_ToListpr50501.70 ns1.00552 B0.45
SelectManySingleSelectorIterator_ToListmain501,267.24 ns1.001320 B1.00
SelectManySingleSelectorIterator_ToListpr501,033.94 ns0.82632 B0.48
IEnumerableSkipTakeIterator_ToLostmain50475.09 ns1.001248 B1.00
IEnumerableSkipTakeIterator_ToLostpr50483.19 ns1.02552 B0.44
IEnumerableWhereIterator_ToListmain50566.51 ns1.001248 B1.00
IEnumerableWhereIterator_ToListpr50559.30 ns0.99560 B0.45
ArrayWhereIterator_ToListmain50474.29 ns1.001616 B1.00
ArrayWhereIterator_ToListpr50544.54 ns1.15928 B0.57
IEnumerableWhereSelectIterator_ToListmain50581.70 ns1.001312 B1.00
IEnumerableWhereSelectIterator_ToListpr50599.43 ns1.03624 B0.48
ArrayWhereSelectIterator_ToListmain50494.47 ns1.001672 B1.00
ArrayWhereSelectIterator_ToListpr50499.22 ns1.01984 B0.59

Benchmark Code

Command Line
dotnet run -c Release `--corerun `
D:\Code\_forks\dotnet-runtime-base\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`
D:\Code\_forks\dotnet-runtime\artifacts\bin\testhost\net9.0-windows-Release-x64\shared\Microsoft.NETCore.App\9.0.0\corerun.exe`--launchCount 3
Program.cs
usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(Tests<>).Assembly).Run(args);publicpartialclassTests<T>whereT:new(){privateT_value=new();privateT[]_valueArr=[new()];privateIEnumerable<T>CreateEnumerable(){for(inti=0;i<Count;i++)yieldreturn_value;}privateIEnumerable<T>CreateArray(){varresult=newT[Count];for(inti=0;i<Count;i++){result[i]=_value;}returnresult;}[Params(1,5,50)]publicintCount{get;set;}[Benchmark]publicList<T>Enumerable_ToList()=>CreateEnumerable().ToList();[Benchmark]publicList<T>IEnumerableSelectIterator_ToList()=>AssertType(CreateEnumerable().Select(i =>i),"IEnumerableSelectIterator`2").ToList();[Benchmark]publicList<T>IteratorSelectIterator_ToList()=>AssertType(CreateEnumerable().Skip(1).Select(i =>i),"IteratorSelectIterator`2").ToList();[Benchmark]publicList<object>OfTypeIterator_ToList()=>AssertType(CreateEnumerable().OfType<object>(),"OfTypeIterator`1").ToList();[Benchmark]publicList<T>SelectManySingleSelectorIterator_ToList()=>AssertType(CreateEnumerable().SelectMany(i =>_valueArr),"SelectManySingleSelectorIterator`2").ToList();[Benchmark]publicList<T>IEnumerableSkipTakeIterator_ToLost()=>AssertType(CreateEnumerable().Skip(1),"IEnumerableSkipTakeIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true),"IEnumerableWhereIterator`1").ToList();[Benchmark]publicList<T>ArrayWhereIterator_ToList()=>AssertType(CreateArray().Where(_ =>true),"ArrayWhereIterator`1").ToList();[Benchmark]publicList<T>IEnumerableWhereSelectIterator_ToList()=>AssertType(CreateEnumerable().Where(_ =>true).Select(i =>i),"IEnumerableWhereSelectIterator`2").ToList();[Benchmark]publicList<T>ArrayWhereSelectIterator_ToList()=>AssertType(CreateArray().Where(_ =>true).Select(i =>i),"ArrayWhereSelectIterator`2").ToList();privatestaticTIteratorAssertType<TIterator>(TIteratorvalue,stringtype){if(value!.GetType().Nameis{}actual&&actual!=type){thrownewInvalidOperationException($"Expected {type} got {actual}");}returnvalue;}}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassInt32Tests:Tests<int>{}[HideColumns("Error","StdDev","Median","RatioSD","Job")][MemoryDiagnoser(false)]publicclassObjectTests:Tests<object>{}

Performance Analysis

In all cases allocations are down and sometimes significantly so. As the size of the collection increases so do the savings due to reduced number of "unnecessary" array copies as the list grows.

Speed is more of a trade-off. For lower counts the PR is actually slower whereas for higher counts it is faster, again due to less time copying data around.

However this must be seen in context; we are trading off nanoseconds. In situations where nanoseconds of individual LINQ operations matter, it would probably be recommended to write the loops by hand anyway. On this basis I would say that creating less garbage to collect would probably be better for the overall system performance than the slower code for the Count less than 5 case.

@ghostghost added the area-System.Linq label Jul 3, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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

Comment threadsrc/libraries/System.Linq/src/System/Linq/SegmentedArrayBuilder.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/ToCollection.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Select.SpeedOpt.cs Outdated
@eiriktsarpaliseiriktsarpalis added the tenet-performance Performance related issue label Jul 16, 2024

@eiriktsarpaliseiriktsarpalis 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!

@eiriktsarpalis
eiriktsarpalis merged commit 3922062 into dotnet:mainJul 16, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqcommunity-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.

3 participants

@andrewjsaid@stephentoub@eiriktsarpalis