LinkedIn Benchmarks again

9/15/2025
3 minute read

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.

An error has occurred. This application may no longer respond until reloaded. Reload x