Another day, another dollar. LinkedIn has the tips for you! Performance, performance! Let's get right into it.
Slicing a list
The benchmarks shows roughly something like this:
[MemoryDiagnoser]
public class ListBenchmarks
{
private static readonly List<string> _userIds = Enumerable.Range(1, 1000).Select(i => $"user{i}").ToList();
[Benchmark(Baseline = true)]
public List<string> SkipAndTake()
{
return _userIds.Skip(200).Take(200).ToList();
}
// Range operator (C# 8.0+)
[Benchmark]
public List<string> Take()
{
return _userIds[200..400].ToList();
}
[Benchmark]
public List<string> GetRange()
{
return _userIds.GetRange(200, 200);
}
}
So we are slicing a list with different mechanism and compare the differences. If I run that on my machine, I get something like this:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------ |----------:|---------:|---------:|------:|--------:|-------:|-------:|----------:|------------:|
| SkipAndTake | 99.95 ns | 2.001 ns | 1.774 ns | 1.00 | 0.02 | 0.2093 | 0.0006 | 1.71 KB | 1.00 |
| Take | 169.00 ns | 3.373 ns | 6.335 ns | 1.69 | 0.07 | 0.3958 | 0.0036 | 3.23 KB | 1.89 |
| GetRange | 83.59 ns | 1.688 ns | 2.820 ns | 0.84 | 0.03 | 0.1979 | 0.0012 | 1.62 KB | 0.95 |
So GetRange
is by far the fastest with the least amount of allocations. Now let's ignore for a minute that the sample set isn't that high. The original post doesn't provide the full setup so I had to second-guess.
The issue
What is odd is the big difference in runtime and allocation for Take
(that uses the Range
object and operation introduced in C# 8). How can this take "so long"? Well the answer is, because the code is wrong: Using [start..end]
on a List, is basically calling the Slice
method. You can also play around on sharplab.io with that code.
Slice
is defined as: public System.Collections.Generic.List<T> Slice(int start, int length);
The compiler helps you out to transform your Range
object into start
and length
.
Creates a shallow copy [...]
So you get back a List
which holds that slice already. There is no reason to call ToList
again! Now, you could argue: "Hey Steven, but this is a shallow copy and therefore if you change index 212, the new slice would see that change as well. Skip(...).Take(...)
would behave differently here!" Well - you are technically correct, but the last one in the set, is: GetRange
and you can guess what this method does:
Creates a shallow copy of a range of elements in the source List
.
Soooooooo. The benchmark does not compare the same aspect across the board. Either GetRange
also gets a ToList
slapped on top, or we are checking way different things here!
As always: Check the code in depth - understand what folks are posting online. Often times, it makes a nice show but doesn't hold true if you undercover one layer below.