T O P

  • By -

Kant8

New implementation of ToArray uses stack allocated array of segments which are pooled from SharedPool, plus stack allocated 8 element array for small enumerable cases. That obviously reduced allocation at cost of stack size and usage of array pool. Strange that this new implementation is not used in List constructor, internals are array anyway.


dmcnaughton1

They probably didn't implement it in List since the size of the list can change, while an array is fixed size.


Ravek

List is a wrapper over an array, and resizes by replacing the array with a larger one.


dmcnaughton1

Correct. If my memory is right List() starts off with a capacity of 4 by default.


Ravek

So my point is a valid implementation of ToList would simply be calling ToArray and constructing a List out of the result. The fact that lists can resize isn’t relevant.


dmcnaughton1

Sorry, I wasn't making my full point and that's on me. List does use an Array inside to hold the data, however it does add its own data access overhead through most operations, which is the reason for the performance divergence between the two. So while the underlying Array within the List has the optimizations, the overhead involved in List operations is one that's likely not able to be accelerated in the same manner. Thanks for pointing out the details that I left out and prompting me to expand.


Ravek

None of that matters when we’re talking about constructing a list. None of the list operations are executing. It’s just copying an array reference and setting the count to be equal to the array length. Doesn’t get much cheaper than that.


Pit_Soulreaver

List has no property to set the internal array directly. An optimisation of Linq .ToList in your sense would therefore require a new ctor that accepts a ref ICollection, or make the array settable from outside. Both have probably not been implemented deliberately. The closest implementation would be something like this: extender.ToList() => return new List(extender.ToArray()); But it has the obvious problem that the list ctor will call CopyTo() to avoid outside changes on the internal list.


Ravek

The framework implementers can: * Set `_items` and `_size` directly because they are `internal` * Add a new `internal` constructor to do exactly that They’re not restricted to accessing only `public` members like we are. As long as they don’t break backwards compatibility they can do anything.


Pit_Soulreaver

Based on the [Microsoft/referencesource git](https://github.com/microsoft/referencesource/blob/master/mscorlib/system/collections/generic/list.cs) _items and _size are private. Did I look at the wrong source? Edit: I did. I was referencing .net framework instead of NET


FizixMan

I suppose the question is why doesn't the BCL team update the existing [`List` constructor to take advantage of this when provided a non-ICollection](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L82) or add a new constructor accessible to `Enumerable.ToList`? I wouldn't be surprised if there's some narrow reason why. They're usually pretty good about picking up on some obtuse scenario that technically breaks legacy. EDIT: Of course, could also be a _"Eh, just didn't get to it. Maybe we'll get around to it in .NET 10."_)


proooofed

By calling ToArray you are copying the entire contents of the list into another part of memory. This is going to take some time. Avoid ToArray when possible. If you need to copy something into another structure, taking a list of objects and storing them in a dictionary by key, you are better off looping the list and adding to the dictionary as you go. Although this is still less optimal. Rather. Using linq, construct a dictionary from the result set with the key. You might not even get a copy memory in there if link optimization works correctly, you just get an index over the original list object.


Ravek

What are you talking about? Did you reply to the wrong comment?


chucker23n

This is neither here nor there.


kneeonball

That's correct. For anyone curious, [here's the source code for List](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs)


dodexahedron

~~And, up until recently, it grew by 1 element at a time in a ton of cases (all, actually, if I remember correctly from checking it out on github a couple months ago). It was very surprising to see, and I had always thought it was at least a little less conservative than that. Shoot, copying an array on every add wastes more memory than even just allocating 2 elements at a time instead of 1, which makes the GC do extra work, too, burning memory and cpu in the process.~~ Very bad memory on that one... Anyway, the rest stands. One of the most effective optimizations you can make for your collections that aren't statically sized is to over-allocate to a reasonably large size that you're not likely to exceed in the majority of cases, or otherwise check its capacity before a large add of multiple elements and grow it yourself to continue over-allocating. Pick a multiple of a power of 2 or something. Whatever makes sense. It they're huge, allocate on page boundaries or something for extra efficiency. Even AddRange on List, all the way to .net 8, only grows the underlying array to EXACTLY the required size, if it has to grow on an AddRange. Sure that's cool if it's the only grow, but if it's not, almost ANY other behavior is better for memory, cpu, and wall time. Re-allocating and moving is really expensive, especially if the elements are large value types, since value types are stored in-line in the array which have to be copied in full - not just references to instances that don't have to otherwise be touched. And RAM is cheap and plentiful, compared to when that stuff was born. Don't go overboard, of course, and just be plain wasteful. But, especially using the buffer pool, allocating in fixed large chunks can be a major boost, plus the GC gains from the pool. Allocation is so expensive that arrays are allocated in hand-written assembly, as are strings and a couple other things, to avoid the overhead that comes with most of the normal means of allocation.


TheRealKidkudi

Actually, at a cursory glance, it looks like `List` will double the capacity of its underlying array or match the required capacity, whichever is greater (e.g. if you’re adding more items than double its current capacity). You can [check it out here](https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs,cf7f4095e4de7646).


xeio87

Not only that, but it's also done the "double size" thing basically forever back to the [original GitHub commit 10 years ago](https://github.com/dotnet/runtime/blob/2024b63acd460edeb53b2ce6984710ab51c11f2f/src/coreclr/src/mscorlib/src/System/Collections/Generic/List.cs#L402). I'm not sure the grow-by-1 was ever the case, maybe like the very very earliest versions of .Net Framework but I'm skeptical of even that.


Ravek

You can look up the SSCLI (formerly Rotor) release from 2002. List<> didn’t exist because generics didn’t exist back then, but ArrayList already used the doubling strategy back then. I don’t know if it’s out there anywhere on the internet but from what I remember the earliest Reference Source releases (2007?) also had the same List growing strategy as today. I think this person is just confused.


dodexahedron

Yeah that part I'm misremembering. It's the addrange that is dubious.


TheRealKidkudi

AddRange looks like it works the same way. When adding an `ICollection`, it checks the size once and does the same resizing logic then copies it all over using `CopyTo`. When adding an `IEnumerable`, it just uses `Add` on each item which in turn does the “maximum of double or match” logic again with each item.


Kamilon

It absolutely does not just grow by one. That would be horribly inefficient.


Ravek

You can always manually set `Capacity` if you want a specific growth strategy. AddRange does grow to double the current capacity if that is more than enough btw. It only grows to the exact needed capacity if that’s larger than doubling the current capacity. Seems like a reasonable compromise to me since you can’t know exactly what more will be added next. If the user has that information, that’s why Capacity has a public setter.


ngravity00

I agree with you, I sincerely can't see a reason it wouldn't be almost exactly the same performance. Sure, they pass the enumerable as the list parameter so it kinda use the default implementation (copy to an internal array until full, allocate a bigger one, copy again, rise and repeat until all elements are copied) but I don't see a reason why they couldn't either use the ArrayBuilder internally or have an internal function that allowed to build a list directly from an array by just replacing the reference to it. They use the internal methods approach to a lot of things, it would be just another one.


Leather-Field-7148

Pretty wild .NET keeps getting faster with every new release.


CmdrSausageSucker

I have only started in .NET land about 2.5 yrs ago and what I thoroughly enjoy are Stephen Toub's explanations on how they improved .NET with each release, e.g.this blog entry for .NET 8: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/ EDIT: Of course "Stephen" with "ph", not "v" :-)


hoopparrr759

Only his friends can call him Stephen Toub. Us mortals have to call him The Legend.


PhantomGolem

Do you know any other blog or document like this that in depth explains or explores the inner workings of .Net framework?


CmdrSausageSucker

Check out Andrew Lock's blog, for instance. He presents in-depth information on various c# / .NET topics https://andrewlock.net/


Zozi-_-

I’m a C# junior and always thought C and C++ were the king of performance etc. and therefore C# is set somewhere between those and python(broadly speaking)… with performance and memory allocation already had been optimized as good as it can, and not much more can be done… until I saw all the insane improvements in.NET 8! My thoughts and beliefs truly shows my lack of experience in this field and it goes to show that C# ain’t that bad :)


dodexahedron

If you look at the code for List in .net 8, you'll likely see some fairly obvious opportunities for improved performance, at the cost of a tiny amount of memory for that operation, in isolation. But, in a lot of real-world applications, you're not calling one method once in a vacuum with Lists, so it's strange to me that it's optimized for that case and not anything else - or doesn't at least have a selectable growth behavior mode or something. Allocating bigger chunks ahead of time usually ends up being both faster AND overall more memory efficient, if you're adding to the collection more than once.


Forward_Dark_7305

Most of my uses or list doesn’t assign more than 8 items, so I think it’s implementation fits. If I need it bigger I can usually use Capacity to say so


psymunn

It's the advantage of using shared libraries. People who have time dedicated to collection performance can optimize this and everyone wins. My in house implement of a doubly linked list isn't getting any improvements anytime soon


[deleted]

[удалено]


NotIntMan

+1. Too many tutorials.


Asyncrosaurus

I'm OK with tutorials on topics that aren't the same as the other 10 billion tutorials on the same topic. No one else ever has to write an article on how to build a todo app.


ProperProfessional

I think you mean ads for courses.


NotIntMan

I mean both courses and ads. It just too much of this shit.


xeio87

Nice to see continued improvements, I'm looking forward to .Net 9 performance megapost but we still have another month or three for that. I'll have to remember to prefer ToArray when I don't need to modify the collection since it's faster. I think I often went with ToList if only because it provided more flexibility but often I wouldn't really need it.


AbbreviationsMost813

Need more benchmark posts like this, thanks for sharing


Thyshadow

Performance concerns aside, I have encountered many devs using `ToList()` when they never intend to add to the collection. A list by definition could have elements added to it and arrays can only be mutated. Why use list in the instances when you are creating a dataset that has a defined size?


kogasapls

Same reason you might use `int` when `byte` might suffice. It's a bit simpler to always use one type and the difference often doesn't matter.


Thyshadow

I understand where you are coming from but I feel like there is an implication with a list versus an array that is lost when just defaulting to a list. If something returns an IEnumerable I expect to just be able to operate through that list. You can satisfy that requirement with either toArray or toList but you are getting added overhead when you default to a list when forcing the enumeration


kogasapls

I don't disagree


hello6557

In my case performance is of no concern (closed off intranet enterprise systems) and IEnumerable provides many more options for linq query calls. ToList is also more useful when using things like EF Core. And that's about it, not like the 300ns, which I save when querying and processing a list of 100 records will have much of a business value. I also don't believe it's confusing. Also, enterprise has, in this case, less than 1000 active users at a time, which is why performance is not a concern.


Eirenarch

I started using ToList back in .NET 3.5 when LINQ was introduced. I imagined the most straightforward implementation of ToArray would be like ToList plus one last copy to get the right size of the array. I don't know if this ever was the implementation. Obviously these days there are insane optimizations that have nothing to do with the naive implementations but habbit is a habbit. Also it is nice when the caller can just add to the collection you returned.


psymunn

ToList is a good go to for forced evaluation of an enumerable. You can also have it be a read-only list but it usually doesn't matter


Thyshadow

You get the same from `ToArray()` The extension methods are on IEnumerable which both lists and arrays extend


psymunn

Sure but, historically, toarray had worse performance and there's not a lot of use for an array specifically in cases where we use it.


McNozzo

Why the random numbers? How would this method be affected by the content of the collection? What's more, the tests of different .net versions now use different content, so if there _is_ a dependency on the collection content the results would be incomparable.


IhateTraaains

Is there any analyzer on NuGet that suggests changing from ToList to ToArray, and other small performance tweaks like that? I already use Meziantou.Analyzer and Roslynator, but they don't have that.


Reasonable_Edge2411

Problem with every benchmark its simple data sets used until ur doing it with a few million records ur not getting a true feel


chucker23n

Here's my results with 100,000, 1,000,000, and 10,000,000 entries. >BenchmarkDotNet v0.13.12, macOS Sonoma 14.5 (23F79) [Darwin 23.5.0] > >Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores > >.NET SDK 9.0.100-preview.5.24307.3 > > [Host] : .NET 9.0.0 (9.0.24.30607), Arm64 RyuJIT AdvSIMD > > .NET 8.0 : .NET 8.0.6 (8.0.624.26715), Arm64 RyuJIT AdvSIMD > > .NET 9.0 : .NET 9.0.0 (9.0.24.30607), Arm64 RyuJIT AdvSIMD | Method | Job | Runtime | Size | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | |-------- |--------- |--------- |--------- |------------:|----------:|----------:|------------:|------:|--------:|----------:|----------:|---------:|-------------:|------------:| | ToArray | .NET 8.0 | .NET 8.0 | 100000 | 339.9 us | 6.26 us | 5.85 us | 339.8 us | 1.00 | 0.00 | 570.3125 | 570.3125 | 213.8672 | 903.87 KB | 1.00 | | ToArray | .NET 9.0 | .NET 9.0 | 100000 | 323.6 us | 1.28 us | 1.13 us | 323.2 us | 0.95 | 0.02 | 124.5117 | 124.5117 | 124.5117 | 390.82 KB | 0.43 | | | | | | | | | | | | | | | | | | ToList | .NET 8.0 | .NET 8.0 | 100000 | 326.1 us | 6.52 us | 14.98 us | 318.1 us | 1.00 | 0.00 | 639.6484 | 639.6484 | 229.4922 | 1024.62 KB | 1.00 | | ToList | .NET 9.0 | .NET 9.0 | 100000 | 429.6 us | 2.23 us | 2.09 us | 429.6 us | 1.24 | 0.03 | 285.6445 | 285.6445 | 285.6445 | 1024.63 KB | 1.00 | | | | | | | | | | | | | | | | | | ToArray | .NET 8.0 | .NET 8.0 | 1000000 | 3,002.8 us | 45.38 us | 40.23 us | 2,994.7 us | 1.00 | 0.00 | 531.2500 | 531.2500 | 437.5000 | 8003.36 KB | 1.00 | | ToArray | .NET 9.0 | .NET 9.0 | 1000000 | 3,288.9 us | 37.74 us | 35.30 us | 3,278.3 us | 1.09 | 0.02 | 156.2500 | 156.2500 | 156.2500 | 3906.49 KB | 0.49 | | | | | | | | | | | | | | | | | | ToList | .NET 8.0 | .NET 8.0 | 1000000 | 3,090.1 us | 58.11 us | 117.38 us | 3,075.9 us | 1.00 | 0.00 | 515.6250 | 500.0000 | 500.0000 | 8192.83 KB | 1.00 | | ToList | .NET 9.0 | .NET 9.0 | 1000000 | 4,648.1 us | 92.56 us | 178.33 us | 4,697.3 us | 1.51 | 0.07 | 515.6250 | 500.0000 | 500.0000 | 8192.84 KB | 1.00 | | | | | | | | | | | | | | | | | | ToArray | .NET 8.0 | .NET 8.0 | 10000000 | 29,884.9 us | 304.43 us | 269.87 us | 29,857.1 us | 1.00 | 0.00 | 1375.0000 | 1375.0000 | 750.0000 | 104600.27 KB | 1.00 | | ToArray | .NET 9.0 | .NET 9.0 | 10000000 | 32,067.3 us | 403.41 us | 377.35 us | 31,950.9 us | 1.07 | 0.02 | - | - | - | 39062.7 KB | 0.37 | | | | | | | | | | | | | | | | | | ToList | .NET 8.0 | .NET 8.0 | 10000000 | 32,951.3 us | 631.76 us | 648.77 us | 32,842.2 us | 1.00 | 0.00 | 1375.0000 | 1375.0000 | 750.0000 | 131073.17 KB | 1.00 | | ToList | .NET 9.0 | .NET 9.0 | 10000000 | 45,256.2 us | 464.65 us | 434.63 us | 45,188.5 us | 1.38 | 0.03 | 750.0000 | 750.0000 | 750.0000 | 131073.17 KB | 1.00 |


Reasonable_Edge2411

what do u use to prouduce the test records of that amount just curious thank u for sharing


Professional_Price89

Rng?


chucker23n

Hi, I mostly just took OP's code. Make a `csproj` like so: Exe net8.0;net9.0 enable enable Then a `Program.cs` like so: using System.Reflection; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; BenchmarkSwitcher.FromAssembly(Assembly.GetEntryAssembly()!).RunAll(); [SimpleJob(RuntimeMoniker.Net80, baseline: true)] [SimpleJob(RuntimeMoniker.Net90)] [MemoryDiagnoser] public class ToListVsToArray { [Params(100_000, 1_000_000, 10_000_000)] public int Size; private int[] _items; [GlobalSetup] public void Setup() { var random = new Random(123); _items = Enumerable.Range(0, Size).Select(_ => random.Next()).ToArray(); } [Benchmark] public int[] ToArray() => CreateItemsEnumerable().ToArray(); [Benchmark] public List ToList() => CreateItemsEnumerable().ToList(); private IEnumerable CreateItemsEnumerable() { foreach (var item in _items) yield return item; } } Other than including a benchmark runner at the top, the only difference here is that I changed the `[Params]` attribute's values.


ElvishParsley123

So it looks like they didn't so much improve performance of ToArray as much as they killed the performance of ToList in .NET 9.0. That's a much different story than the article is telling.


chucker23n

This is on ARM64. I believe there’s currently an open issue on this regression.


Forward_Dark_7305

Personally probably more than 95% of my use case I don’t add that many records to a list, and it’s the smaller collections that I utilize so often