Initializing single-item collections on C#
CSharp interfaces are implementation contracts, enforcing that any implementation based on an interface MUST adhere to its requirements (properties, access modifiers, methods, etc.)
Playing around with a dead simple repository-like interface, it needs to retrieve instances of a given type and allow adding new ones. For simplicity I thought of making the Add() method accept an IEnumerable<T> as argument, so that it would be generic enough to allow adding multiple instance or just one.
public interface IDeadSimpleRepository
{
public void IAsyncEnumerable<string> GetAsync();
public void ValueTask AddAsync(IEnumerable<string> items);
}
Because this is not mission critical, I thought:
ok, this reduces the interface's impact surface. If using it becomes cumbersome when adding a single instance, surely a extension method would do the job
But, what would perform better when creating an enumerable collection from a single instance? I though that Enumerable.Empty<T>.Append(T) would to the job, as technically does not create any object to be allocated.
Which is true for Enumerable.Empty<T>, but not for the Append().
A quick and raw benchmark on .net 8 showed that creating a new array is twice as faster and allocates half memory than the Append() alternatives.
I also threw in there ArrayPool<string> simply to try it out. Although seemingly does not allocate, it gets more verbose on such a simple use case.
using System.Buffers;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<SingleItemEnumerableBenchmark>();
[SimpleJob(invocationCount: 10_000_000)]
[MemoryDiagnoser]
public class SingleItemEnumerableBenchmark
{
private const string Value = nameof(SingleItemEnumerableBenchmark);
public uint DoNothing(IEnumerable<string> values) => 1;
[Benchmark(Baseline = true)]
public uint ArrayNew() => DoNothing(new []{Value});
[Benchmark]
public uint AppendEmptyEnumerable() =>
DoNothing(Enumerable
.Empty<string>()
.Append(Value)
);
[Benchmark]
public uint AppendArrayEmpty() =>
DoNothing(Array
.Empty<string>()
.Append(Value)
);
[Benchmark]
public uint ArrayPool()
{
var pool = ArrayPool<string>.Shared;
string[] array = pool.Rent(1);
array[0] = Value;
try
{
return DoNothing(array);
} finally
{
pool.Return(array);
}
}
}
// * Summary *
BenchmarkDotNet v0.13.10, Ubuntu 22.04.3 LTS (Jammy Jellyfish)
Intel Core i7-4500U CPU 1.80GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100-rc.2.23502.2
[Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
Job-APTYLW : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
InvocationCount=10000000
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|
| ArrayNew | 8.327 ns | 0.1395 ns | 0.1236 ns | 1.00 | 0.00 | 0.0153 | 32 B | 1.00 |
| AppendEmptyEnumerable | 17.590 ns | 0.1036 ns | 0.0865 ns | 2.11 | 0.03 | 0.0306 | 64 B | 2.00 |
| AppendArrayEmpty | 15.326 ns | 0.3325 ns | 0.2947 ns | 1.84 | 0.05 | 0.0306 | 64 B | 2.00 |
| ArrayPool | 33.842 ns | 0.1715 ns | 0.1432 ns | 4.06 | 0.06 | - | - | 0.00 |
Turns out, the Append() LINQ extension method creates an instance of AppendPrepend1Iterator<TSource>[^1] which seems to be an internal IEnumerable<TSource> implementation that uses a linked list list approach to allow appending and traversing the enumerable.
Conclusion
Once again, LINQ methods make code readable and concise at the cost of performance. Who would have thought!
[^1]: Extension method implementation AppendPrepend.cs