ListSplit Benchmarkz

Benchmark 1: ChunkOfSizeT

The first implementation doesnt preallocate a List and thus yields an overhead as the array has to resized way often.

Code

    public static IEnumerable<List<T>> ToChunksOfMaxSizeN<T>(this IEnumerable<T> toChunk, int maxSize)
    {
        if (maxSize <= 0)
            throw new InvalidEnumArgumentException($"{nameof(maxSize)} must be greater than 0");
 
        var chunk = new List<T>();
        foreach (var o in toChunk)
        {
            chunk.Add(o);
            if (chunk.Count != maxSize)
                continue;
 
            yield return chunk;
            chunk = [];
        }
 
        if (chunk.Any())
            yield return chunk;
    }

Benchmark 2: List.Split()

The second implementation is a tad uglier but yields some micro optimizations by buffering the checks and resizing the array that is to be returned on demand

Code

internal static IEnumerable<TSource[]> Split<TSource>(this IEnumerable<TSource> source,
                                                          int size)
    {
        using var enumerator = source.GetEnumerator();
 
        if (!enumerator.MoveNext())
            yield break;
 
        var arrayBufferSize = Math.Min(size, 4);
 
        uint chunkIndex;
        do
        {
            var array = new TSource[arrayBufferSize];
            array[0] = enumerator.Current;
            chunkIndex = 1;
 
            if (size != array.Length)
            {
                while (chunkIndex < size && enumerator.MoveNext())
                {
                    // Manual resizings still more slim than lists dynamic allocation
                    if (chunkIndex >= array.Length)
                    {
                        arrayBufferSize = Math.Min(size, 2 * array.Length);
                        Array.Resize(ref array, arrayBufferSize);
                    }
 
                    PushToChunk(array);
                }
            }
            else
            {
                // ReSharper disable once InlineTemporaryVariable -- Suppresses bounding checks ( not required as resized above )
                var cache = array;
                while (chunkIndex < cache.Length && enumerator.MoveNext())
                    PushToChunk(array);
            }
 
            // Reduces the ( possible ) fragmentation of the last chunky
            if (chunkIndex != array.Length)
                Array.Resize(ref array, (int)chunkIndex);
 
            yield return array;
        }
        while (chunkIndex >= size && enumerator.MoveNext());
        void PushToChunk(TSource[] array)
        {
            array[chunkIndex] = enumerator.Current;
            chunkIndex += 1;
        }
    }

Results

// * Summary *                                                                                                                                                                                                                                                                                                                                                                          BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4046/22H2/2022Update)                                                                                                                      12th Gen Intel Core i9-12900H, 1 CPU, 20 logical and 14 physical cores                                                                                                                      .NET SDK 8.0.200                                                                                                                                                                              [Host]             : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2                                                                                                                             .NET 6.0           : .NET 6.0.27 (6.0.2724.6912), X64 RyuJIT AVX2                                                                                                                           .NET 8.0           : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2                                                                                                                             .NET Framework 4.8 : .NET Framework 4.8 (4.8.4645.0), X64 RyuJIT VectorSize=256
MethodJobRuntimeListSizeMeanErrorStdDevMedianGen0Gen1Gen2Allocated
Split10.NET 6.0.NET 6.0034.78 ns0.744 ns0.796 ns35.05 ns0.0095--120 B
Split100.NET 6.0.NET 6.0036.83 ns0.130 ns0.122 ns36.79 ns0.0095--120 B
Chunk10.NET 6.0.NET 6.0043.80 ns0.265 ns0.247 ns43.85 ns0.0108--136 B
Chunk100.NET 6.0.NET 6.0043.20 ns0.122 ns0.108 ns43.19 ns0.0108--136 B
Split10.NET 8.0.NET 8.0027.15 ns0.180 ns0.159 ns27.16 ns0.0095--120 B
Split100.NET 8.0.NET 8.0027.46 ns0.070 ns0.062 ns27.44 ns0.0095--120 B
Chunk10.NET 8.0.NET 8.0028.02 ns0.183 ns0.171 ns27.97 ns0.0108--136 B
Chunk100.NET 8.0.NET 8.0028.02 ns0.153 ns0.136 ns28.00 ns0.0108--136 B
Split10.NET Framework 4.8.NET Framework 4.8042.41 ns0.089 ns0.074 ns42.42 ns0.0229--144 B
Split100.NET Framework 4.8.NET Framework 4.8042.31 ns0.056 ns0.047 ns42.31 ns0.0229--144 B
Chunk10.NET Framework 4.8.NET Framework 4.8059.18 ns0.095 ns0.084 ns59.18 ns0.0331--209 B
Chunk100.NET Framework 4.8.NET Framework 4.8059.27 ns0.105 ns0.098 ns59.27 ns0.0331--209 B
Split10.NET 6.0.NET 6.0172.73 ns0.849 ns0.753 ns72.67 ns0.0223--280 B
Split100.NET 6.0.NET 6.0173.64 ns0.234 ns0.219 ns73.65 ns0.0223--280 B
Chunk10.NET 6.0.NET 6.0179.46 ns0.268 ns0.250 ns79.49 ns0.0210--264 B
Chunk100.NET 6.0.NET 6.0179.57 ns0.225 ns0.199 ns79.57 ns0.0210--264 B
Split10.NET 8.0.NET 8.0159.04 ns0.286 ns0.267 ns59.04 ns0.0223--280 B
Split100.NET 8.0.NET 8.0158.40 ns0.222 ns0.185 ns58.42 ns0.0223--280 B
Chunk10.NET 8.0.NET 8.0161.66 ns0.254 ns0.237 ns61.60 ns0.0210--264 B
Chunk100.NET 8.0.NET 8.0161.71 ns0.165 ns0.154 ns61.72 ns0.0210--264 B
Split10.NET Framework 4.8.NET Framework 4.8182.35 ns0.184 ns0.163 ns82.31 ns0.0446--281 B
Split100.NET Framework 4.8.NET Framework 4.8182.35 ns0.232 ns0.217 ns82.30 ns0.0446--281 B
Chunk10.NET Framework 4.8.NET Framework 4.8186.41 ns0.195 ns0.163 ns86.41 ns0.0497--313 B
Chunk100.NET Framework 4.8.NET Framework 4.8186.59 ns0.361 ns0.320 ns86.55 ns0.0497--313 B
Split10.NET 6.0.NET 6.020168.03 ns0.511 ns0.478 ns168.10 ns0.0350--440 B
Split100.NET 6.0.NET 6.020174.85 ns0.596 ns0.557 ns174.64 ns0.0515--648 B
Chunk10.NET 6.0.NET 6.020221.65 ns0.825 ns0.772 ns221.69 ns0.0527--664 B
Chunk100.NET 6.0.NET 6.020187.68 ns0.618 ns0.548 ns187.51 ns0.0446--560 B
Split10.NET 8.0.NET 8.020112.32 ns0.479 ns0.448 ns112.24 ns0.0350--440 B
Split100.NET 8.0.NET 8.020127.57 ns0.407 ns0.380 ns127.45 ns0.0515--648 B
Chunk10.NET 8.0.NET 8.020159.80 ns0.635 ns0.594 ns159.78 ns0.0527--664 B
Chunk100.NET 8.0.NET 8.020136.64 ns0.600 ns0.532 ns136.61 ns0.0446--560 B
Split10.NET Framework 4.8.NET Framework 4.820184.13 ns0.570 ns0.505 ns184.26 ns0.0701--441 B
Split100.NET Framework 4.8.NET Framework 4.820197.53 ns0.915 ns0.714 ns197.50 ns0.1032--650 B
Chunk10.NET Framework 4.8.NET Framework 4.820254.25 ns0.682 ns0.605 ns254.15 ns0.1159--730 B
Chunk100.NET Framework 4.8.NET Framework 4.820216.60 ns0.579 ns0.542 ns216.73 ns0.0968--610 B
Split10.NET 6.0.NET 6.010004,659.30 ns7.892 ns6.996 ns4,657.81 ns0.68660.0076-8696 B
Split100.NET 6.0.NET 6.010003,730.50 ns13.624 ns12.744 ns3,729.67 ns0.41960.0038-5312 B
Chunk10.NET 6.0.NET 6.010007,879.82 ns38.476 ns35.991 ns7,874.00 ns1.89210.0458-23816 B
Chunk100.NET 6.0.NET 6.010004,705.97 ns21.447 ns19.012 ns4,705.78 ns0.97660.0153-12312 B
Split10.NET 8.0.NET 8.010002,938.31 ns11.088 ns10.372 ns2,939.48 ns0.69050.0114-8696 B
Split100.NET 8.0.NET 8.010002,025.02 ns3.930 ns3.484 ns2,024.77 ns0.41960.0038-5312 B
Chunk10.NET 8.0.NET 8.010005,583.41 ns19.309 ns17.117 ns5,585.56 ns1.89210.0610-23816 B
Chunk100.NET 8.0.NET 8.010003,116.25 ns7.714 ns6.839 ns3,116.61 ns0.98040.0114-12312 B
Split10.NET Framework 4.8.NET Framework 4.810005,353.31 ns17.838 ns16.686 ns5,350.57 ns1.52590.0229-9628 B
Split100.NET Framework 4.8.NET Framework 4.810003,964.31 ns10.418 ns9.745 ns3,963.76 ns0.85450.0076-5392 B
Chunk10.NET Framework 4.8.NET Framework 4.810008,958.04 ns18.071 ns16.019 ns8,957.69 ns4.07410.1221-25643 B
Chunk100.NET Framework 4.8.NET Framework 4.810005,587.55 ns10.221 ns9.561 ns5,587.37 ns1.99130.0305-12541 B
Split10.NET 6.0.NET 6.01000044,285.11 ns197.826 ns185.047 ns44,257.20 ns6.40870.7324-80824 B
Split100.NET 6.0.NET 6.01000034,991.80 ns80.992 ns75.760 ns34,968.03 ns3.60110.3052-45216 B
Chunk10.NET 6.0.NET 6.01000076,120.59 ns284.533 ns252.231 ns76,194.53 ns18.43263.4180-232744 B
Chunk100.NET 6.0.NET 6.01000045,649.65 ns301.484 ns251.753 ns45,585.16 ns9.58251.1597-120616 B
Split10.NET 8.0.NET 8.01000027,637.12 ns115.936 ns108.446 ns27,680.14 ns6.43920.9155-80824 B
Split100.NET 8.0.NET 8.01000017,540.92 ns79.912 ns70.840 ns17,526.46 ns3.60110.3357-45216 B
Chunk10.NET 8.0.NET 8.01000051,725.74 ns207.773 ns184.186 ns51,727.44 ns18.49374.3945-232744 B
Chunk100.NET 8.0.NET 8.01000028,468.41 ns78.064 ns73.022 ns28,478.74 ns9.61301.2207-120616 B
Split10.NET Framework 4.8.NET Framework 4.81000052,063.23 ns589.596 ns522.662 ns51,859.29 ns14.09911.5259-89070 B
Split100.NET Framework 4.8.NET Framework 4.81000037,361.94 ns113.097 ns100.257 ns37,354.12 ns7.32420.5493-46258 B
Chunk10.NET Framework 4.8.NET Framework 4.81000089,433.33 ns354.915 ns277.095 ns89,371.21 ns39.55088.7891-249509 B
Chunk100.NET Framework 4.8.NET Framework 4.81000054,527.73 ns106.232 ns88.708 ns54,521.85 ns19.47022.1973-122730 B
Split10.NET 6.0.NET 6.010000005,533,921.21 ns22,337.859 ns19,801.930 ns5,538,833.98 ns664.0625492.1875492.18758249699 B
Split100.NET 6.0.NET 6.010000003,607,778.98 ns10,117.101 ns9,463.543 ns3,606,839.45 ns351.5625160.1563-4452475 B
Chunk10.NET 6.0.NET 6.0100000022,527,238.86 ns447,673.764 ns1,194,931.902 ns22,096,309.38 ns2093.75001281.2500500.000023449660 B
Chunk100.NET 6.0.NET 6.010000005,255,326.38 ns26,249.182 ns21,919.255 ns5,251,902.34 ns953.1250468.7500-12051877 B
Split10.NET 8.0.NET 8.010000004,054,003.67 ns58,829.348 ns49,125.170 ns4,052,357.81 ns664.0625492.1875492.18758249696 B
Split100.NET 8.0.NET 8.010000001,792,539.66 ns13,426.802 ns11,902.511 ns1,791,585.25 ns353.5156320.3125-4452473 B
Chunk10.NET 8.0.NET 8.0100000024,518,900.84 ns565,878.965 ns1,668,506.897 ns23,946,503.12 ns2312.50002281.2500843.750023449754 B
Chunk100.NET 8.0.NET 8.010000003,338,952.19 ns15,577.896 ns13,008.249 ns3,333,424.61 ns957.0313898.4375-12051874 B
Split10.NET Framework 4.8.NET Framework 4.8100000011,028,185.09 ns366,288.327 ns1,080,009.398 ns10,865,642.19 ns1437.5000984.3750671.87509319568 B
Split100.NET Framework 4.8.NET Framework 4.810000006,826,392.43 ns129,384.711 ns127,073.086 ns6,811,515.62 ns812.5000375.0000125.00004596194 B
Chunk10.NET Framework 4.8.NET Framework 4.8100000034,659,248.56 ns680,690.093 ns931,736.282 ns34,303,443.75 ns4062.50001812.5000750.000025372642 B
Chunk100.NET Framework 4.8.NET Framework 4.8100000011,817,508.54 ns207,145.941 ns193,764.442 ns11,794,673.44 ns2187.50001000.0000468.750012310156 B

Analyzing stuff

Well its obvious. Less allocation and faster using the split thingy

Testcode

Full SourceCode ```csharp using System.ComponentModel; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs;

namespace Benchmarkz;

[SimpleJob(RuntimeMoniker.Net48)] [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net80)] [MemoryDiagnoser] public class ChunkBenchmarks { private IEnumerable dataSet = Enumerable.Empty();

[Params(0, 1, 20, 1000, 10000, 1000000)]
public uint ListSize = 0;

[GlobalSetup]
public void Setup() => dataSet = FillArray(ListSize);

[Benchmark]
public void Split10() => _ = dataSet.Split(10).ToArray();
[Benchmark]
public void Split100() => _ = dataSet.Split(100).ToArray();

[Benchmark]
public void Chunk10() => _ = dataSet.ToChunksOfMaxSizeN(10).ToArray();
[Benchmark]
public void Chunk100() => _ = dataSet.ToChunksOfMaxSizeN(100).ToArray();

private static IEnumerable<uint> FillArray(uint count)
{
    for (uint i = 0; i < count; i++)
        yield return i;
}

}

internal static class SplitExtensions { internal static IEnumerable<TSource[]> Split(this IEnumerable source, int size) { using var enumerator = source.GetEnumerator();

    if (!enumerator.MoveNext())
        yield break;

    var arrayBufferSize = Math.Min(size, 4);

    uint chunkIndex;
    do
    {
        var array = new TSource[arrayBufferSize];
        array[0] = enumerator.Current;
        chunkIndex = 1;

        if (size != array.Length)
        {
            while (chunkIndex < size && enumerator.MoveNext())
            {
                // Manual resizings still more slim than lists dynamic allocation
                if (chunkIndex >= array.Length)
                {
                    arrayBufferSize = Math.Min(size, 2 * array.Length);
                    Array.Resize(ref array, arrayBufferSize);
                }

                PushToChunk(array);
            }
        }
        else
        {
            // ReSharper disable once InlineTemporaryVariable -- Suppresses bounding checks ( not required as resized above )
            var cache = array;
            while (chunkIndex < cache.Length && enumerator.MoveNext())
                PushToChunk(array);
        }

        // Reduces the ( possible ) fragmentation of the last chunky 
        if (chunkIndex != array.Length)
            Array.Resize(ref array, (int)chunkIndex);

        yield return array;
    }
    while (chunkIndex >= size && enumerator.MoveNext());

    void PushToChunk(TSource[] array)
    {
        array[chunkIndex] = enumerator.Current;
        chunkIndex += 1;
    }
}

public static IEnumerable<List<T>> ToChunksOfMaxSizeN<T>(this IEnumerable<T> toChunk, int maxSize)
{
    if (maxSize <= 0)
        throw new InvalidEnumArgumentException($"{nameof(maxSize)} must be greater than 0");

    var chunk = new List<T>();
    foreach (var o in toChunk)
    {
        chunk.Add(o);
        if (chunk.Count != maxSize)
            continue;
            
        yield return chunk;
        chunk = [];
    }

    if (chunk.Any())
        yield return chunk;
}

}


</details>