Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh
, '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

Enumerable.ToArray performance improvement using InlineArray - #90459

Closed
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray
Closed

Enumerable.ToArray performance improvement using InlineArray#90459
neuecc wants to merge 1 commit into
dotnet:mainfrom
neuecc:improve-enumerable-toarray

Conversation

@neuecc

Copy link
Copy Markdown

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

use InlineArray byte[] sequence instead of LargeArrayBuilder
@ghostghost added area-System.Collections community-contribution Indicates that the PR has been added by a community member labels Aug 12, 2023
@ghost

Copy link
Copy Markdown

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

Issue Details

I've optimized the Enumerable.ToArray method for the case when the source is a pure IEnumerable<T>.
Typically, when the buffer overflows, an array with double the previous capacity is created, and the copying process repeats.
In this PR, although we still create an array of double the size, instead of copying immediately, we add the array to a list and perform all the copying at the end.
By reducing the number of copy operations, we see a significant performance improvement.

Benchmark

BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3324/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100-preview.7.23376.3
[Host] : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
Job-NGJHUQ : .NET 8.0.0 (8.0.23.37506), X64 RyuJIT AVX2
IterationCount=1 WarmupCount=1 
MethodLengthMeanErrorGen0Gen1Gen2Allocated
ToList1053.12 nsNA0.0153--256 B
ToArray1071.83 nsNA0.0153--256 B
PR_ToArray1038.40 nsNA0.0119--200 B
ToList100220.14 nsNA0.0730--1224 B
ToArray100326.60 nsNA0.0710--1192 B
PR_ToArray100180.50 nsNA0.0644--1080 B
ToList10001,679.32 nsNA0.50540.0057-8464 B
ToArray10002,081.20 nsNA0.5074--8536 B
PR_ToArray10001,384.58 nsNA0.4978--8336 B
ToList1000017,214.66 nsNA7.84301.2817-131440 B
ToArray1000021,014.18 nsNA6.3171--106224 B
PR_ToArray1000013,196.72 nsNA6.3171--105872 B
ToList100000338,986.18 nsNA285.6445285.6445285.64451049112 B
ToArray100000382,722.36 nsNA249.5117249.5117249.5117925132 B
PR_ToArray100000358,964.21 nsNA249.5117249.5117249.5117924780 B
ToList10000004,533,538.28 nsNA1984.37501984.37501984.37508389748 B
ToArray10000003,067,601.56 nsNA796.8750796.8750796.87508195588 B
PR_ToArray10000002,418,546.88 nsNA800.7813800.7813800.78138195040 B
ToList10000000197,990,525.00 nsNA3875.00003875.00003875.0000134219664 B
ToArray10000000229,693,275.00 nsNA1875.00001875.00001875.0000107110743 B
PR_ToArray1000000029,184,353.12 nsNA1968.75001968.75001968.7500107110478 B
ToList100000000583,235,200.00 nsNA6000.00006000.00006000.00001073744896 B
ToArray100000000501,066,800.00 nsNA3000.00003000.00003000.0000936873600 B
PR_ToArray100000000270,801,200.00 nsNA2000.00002000.00002000.0000936872432 B
ToList10000000004,677,241,700.00 nsNA9000.00009000.00009000.00008589938744 B
ToArray10000000005,229,577,600.00 nsNA4000.00004000.00004000.00008294970392 B
PR_ToArray10000000003,136,817,900.00 nsNA4000.00004000.00004000.00008294969760 B
publicclassToArrayBenchmark{[Params(10,100,1000,10000,100000,1000000,10000000,100000000,1000000000)]publicintLength{get;set;}[Benchmark]publicList<int>ToList(){returnGenerateNumber(Length).ToList();}[Benchmark]publicint[]ToArray(){returnGenerateNumber(Length).ToArray();}[Benchmark]publicint[]PR_ToArray(){returnEnumerableHelpers.ToArray2(GenerateNumber(Length));}publicstaticIEnumerable<int>GenerateNumber(intcount){for(inti=0;i<count;i++){yieldreturni;}}}

Implementation detail

In this scenario, there's a fixed maximum length for the list of arrays that should be allocated.
Starting with 4 and doubling the array size each time, we reach the maximum length after 29 arrays.
Therefore, using [InlineArray(29)], I've reserved a fixed-length space for the T[].

Option

It's also possible to use ArrayPool<T>.Shared.Rent instead of new T[].
In that case, by returning the array when calling ToArray, both copying and returning can be efficiently handled.
I wasn't sure if using ArrayPool was appropriate for this scenario, so in this PR, I've opted for creating new arrays.

Author:neuecc
Assignees:-
Labels:

area-System.Collections

Milestone:-

return result;
}

LargeArrayBuilder<T> builder = new();

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. What would it take to augment your PR to the point where we could delete LargeArrayBuilder? That'd make me much more interested in this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LargeArrayBuilder is used in many places, and given that it includes numerous methods specific to certain scenarios, the amount of rewrites, including from the user's side, would likely be extensive. Ideally, we'd like to replace it entirely, but we want to exclude that from the initial implementation.

Using the ArrayPool as suggested in the option might not be advisable if we replace the LargeArrayBuilder.

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.

My concern is that these code paths are used in a bunch of places on common paths, and adding more generic types will increase code size. It'd be nice to get rid of the LargeArrayBuilder so that it's a net win for both throughput and size, rather than improving throughput at the expense of size.

There's no rush here as it's not going to make .NET 8, anyway, so there's plenty of time to make and evaluate the larger change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you.
If this implementation is acceptable as a basic policy,
I'll take on the challenge of replacing the LargeArrayBuilder.
(Indeed, I was hoping it might make it into .NET 8! That would be great!)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this implementation is acceptable as a basic policy,

We'd support it in principle. Would you like to build on top of this PR or would you prefer to open a new one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am willing to work on this PR.
NET 8 release seems to be coming soon, so I will do it from the branch there when it is released. ......

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.

main is the correct branch. Any such change will be for .NET 9.

@neuecc

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Windows10CE

Windows10CE commented Aug 12, 2023

Copy link
Copy Markdown
Contributor

Your benchmarks show this being faster than ToList (almost) across the board, could it use the same (or similar) optimization?


public T[] ToArray(int lastBlockCount)
{
T[] array = GC.AllocateUninitializedArray<T>(_count + lastBlockCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AllocateUninitializedArray will introduce additional overhead for small arrays here.

@reflectronicreflectronicAug 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The code for AllocateUninitializedArray already has a special case for small-ish arrays:

// small arrays are allocated using `new[]` as that is generally faster.
#pragma warning disable 8500// sizeof of managed types
if(length<2048/sizeof(T))
#pragma warning restore 8500
{
returnnewT[length];
}

Given that AllocateUninitializedArray is marked as AggressiveInlining, there's likely no discernable perf impact here (modulo tuning on the threshold).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

when length(_count + lastBlockCount) == 0, return Array.Empty<byte>().

Comment on lines +181 to +185
for (int i = 0; i < _index; i++)
{
_blocks[i].CopyTo(dest);
dest = dest.Slice(_blocks[i].Length);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for(inti=0;i<_index;i++)
{
_blocks[i].CopyTo(dest);
dest=dest.Slice(_blocks[i].Length);
}
ReadOnlySpan<T[]>blocks=_blocks;
foreach(T[]blockinblocks.Slice(0,_index))
{
block.CopyTo(dest);
dest=dest.Slice(block.Length);
}

This should remove the bounds checks here too. Not sure whether this is the best syntax possible here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

use ref var item = ref _blocks[i].

@reflectronic

reflectronic commented Aug 13, 2023

Copy link
Copy Markdown
Contributor

Something that'd be neat to add here (though the feasibility is questionable) is a special case for small arrays (say, fewer than 32 elements) using an InlineArray of T. Something like:

structArrayBuilder<T>{[InlineArray(32)]structElementsBlock{T_field;}[InlineArray(32)]structArraysBlock{T[]_field;}ElementsBlock_elements;ArraysBlock_arrays;// ...}

Inside the implementation of ArrayBuilder, you'd first fill up ElementsBlock before adding any new elements to ArraysBlock. This eliminates all intermediate allocations for small arrays.

The problem with this idea is that it'll chew through the stack for large structs. This could cause stack overflows when user-defined types come into the picture, even if the entire ElementsBlock is not filled.

The solutions are not pretty. You'd have to defer the stack allocation of ElementsBlock until you're sure that its size is reasonable. But, as far as I'm aware, the only way to reliably do this is with a non-inlineable method at every callsite of ArrayBuilder, plus a very unpleasant contortion of the callsite's actual logic. Doesn't seem very appealing.

I'm curious to hear if there's a way to salvage this idea.

@eiriktsarpaliseiriktsarpalis added this to the 9.0.0 milestone Aug 13, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 27, 2023
@ghostghost removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@eiriktsarpaliseiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 31, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@eiriktsarpaliseiriktsarpalis self-assigned this Nov 15, 2023
@ghostghost removed the no-recent-activity label Nov 15, 2023
@ghost

Copy link
Copy Markdown

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost

Copy link
Copy Markdown

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Collectionscommunity-contributionIndicates that the PR has been added by a community memberneeds-author-actionAn issue or pull request that requires more info or actions from the author.no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@neuecc@Windows10CE@reflectronic@stephentoub@eiriktsarpalis@MichalPetryka@tarekgh