make EmptyEnumerable a singleton
Meir017 opened this issue · 5 comments
no need to create an instance every time the method is called
for reference:
https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Enumerable.SpeedOpt.cs#L11
and
or am I missing something? - https://stackoverflow.com/questions/14131168/singletons-using-structs-in-c-sharp
You're right when using reference types. This is a value type so it would be copied anyway, when not using ref. I wrote an article where I evaluate all possible implementations of Empty I could come up with: https://medium.com/@antao.almada/performance-of-value-type-vs-reference-type-enumerators-820ab1acc291
In the article I do recognize that the singleton implementation is better when using IEnumerable<T>
but in this project I want to use value types as much as possible: https://medium.com/@antao.almada/netfabric-hyperlinq-optimizing-linq-348e02566cef
Thanks for the feedback!
This benchmark shows that the non-singleton value type implementation has zero-allocation and can perform much better than System.Linq.Enumerable.Empty<T>()
.
BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17763.292 (1809/October2018Update/Redstone5)
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.1.600-preview-009472
[Host] : .NET Core 2.1.7 (CoreCLR 4.6.27129.04, CoreFX 4.6.27129.04), 64bit RyuJIT
DefaultJob : .NET Core 2.1.7 (CoreCLR 4.6.27129.04, CoreFX 4.6.27129.04), 64bit RyuJIT
Method | Categories | Mean | Error | StdDev | Median | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
---|---|---|---|---|---|---|---|---|---|---|
Linq_Empty_ForEach | Empty() | 11.9732 ns | 0.0798 ns | 0.0747 ns | 11.9627 ns | 1.000 | - | - | - | - |
Hyperlinq_Empty_ForEach | Empty() | 0.0013 ns | 0.0027 ns | 0.0025 ns | 0.0000 ns | 0.000 | - | - | - | - |
Hyperlinq_Empty_For | Empty() | 0.2649 ns | 0.0273 ns | 0.0255 ns | 0.2501 ns | 0.022 | - | - | - | - |
Linq_Empty_Count | Empty().Count() | 17.9902 ns | 0.3772 ns | 0.3705 ns | 17.8045 ns | 1.000 | - | - | - | - |
Hyperlinq_Empty_Count | Empty().Count() | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.000 | - | - | - | - |
Linq_Empty_Select_ForEach | Empty().Select() | 32.4302 ns | 0.1482 ns | 0.1314 ns | 32.4508 ns | 1.00 | - | - | - | - |
Hyperlinq_Empty_Select_ForEach | Empty().Select() | 1.5091 ns | 0.0118 ns | 0.0105 ns | 1.5100 ns | 0.05 | - | - | - | - |
Hyperlinq_Empty_Select_For | Empty().Select() | 9.9025 ns | 0.1389 ns | 0.1232 ns | 9.9451 ns | 0.31 | - | - | - | - |
Linq_Empty_Where_ForEach | Empty().Where() | 19.6587 ns | 0.4065 ns | 0.3604 ns | 19.6749 ns | 1.00 | - | - | - | - |
Hyperlinq_Empty_Where_ForEach | Empty().Where() | 1.7173 ns | 0.0085 ns | 0.0076 ns | 1.7196 ns | 0.09 | - | - | - | - |
Linq_Empty_Where_Select_ForEach | Empty().Where().Select() | 36.1020 ns | 0.7234 ns | 0.6767 ns | 36.1354 ns | 1.00 | - | - | - | - |
Hyperlinq_Empty_Where_Select_ForEach | Empty().Where().Select() | 2.2709 ns | 0.0698 ns | 0.0653 ns | 2.2433 ns | 0.06 | - | - | - | - |
hmm, so does it make sense to open a pull-request in the corefx repo?
Their version of Empty()
makes sense for their implementation of LINQ. My implementation depends on the use of interface constraints and the interface IValueEnumerable
so that it's not boxed. It only makes sense when adopting it all together.
Closing this issue but more feedback is welcome!