skip to content
alvaro_sh

Initializing single-item collections on C#

| 2 min read

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  
MethodMeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
ArrayNew8.327 ns0.1395 ns0.1236 ns1.000.000.015332 B1.00
AppendEmptyEnumerable17.590 ns0.1036 ns0.0865 ns2.110.030.030664 B2.00
AppendArrayEmpty15.326 ns0.3325 ns0.2947 ns1.840.050.030664 B2.00
ArrayPool33.842 ns0.1715 ns0.1432 ns4.060.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!

Footnotes

  1. Extension method implementation AppendPrepend.cs