将数组分成块 - 是否有更快的方法?

3
我正在寻找将数组按固定大小(最后一个可以更小)拆分成块的最快方法。我在整个站点上搜索并没有发现任何性能比较,因此我自己写了一下,以下是结果:
时间单位为微秒, 均值/误差/标准偏差 对于int[] - 30.02 | 0.1002 | 0.0937 对于IEnumerable - 76.67 | 0.2146 | 0.1902
更新: 版本如下(@Markus'回答中),为139.5 | 0.6702 | 0.5597
SO上最流行的、经常建议使用LINQ的GroupBy与index/chunkSize的方法不可用 - 267微秒远大于前述实现之一。
有没有更快的方法来拆分数组?
P.S. 这是Array和IEnumerable的代码:
    /// <summary>
    /// Splits <paramref name="source"/> into chunks of size not greater than <paramref name="chunkMaxSize"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">Array to be split</param>
    /// <param name="chunkMaxSize">Max size of chunk</param>
    /// <returns><see cref="IEnumerable{T}"/> of <see cref="Array"/> of <typeparam name="T"/></returns>
    public static IEnumerable<T[]> AsChunks<T>(this T[] source, int chunkMaxSize)
    {
        var pos = 0;
        var sourceLength = source.Length;
        do
        {
            var len = Math.Min(pos + chunkMaxSize, sourceLength) - pos;
            if (len == 0)
            {
                yield break;;
            }
            var arr = new T[len];
            Array.Copy(source, pos, arr, 0, len);
            pos += len;
            yield return arr;
        } while (pos < sourceLength);
    }

    /// <summary>
    /// Splits <paramref name="source"/> into chunks of size not greater than <paramref name="chunkMaxSize"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"><see cref="IEnumerable{T}"/> to be split</param>
    /// <param name="chunkMaxSize">Max size of chunk</param>
    /// <returns><see cref="IEnumerable{T}"/> of <see cref="Array"/> of <typeparam name="T"/></returns>
    public static IEnumerable<T[]> AsChunks<T>(this IEnumerable<T> source, int chunkMaxSize)
    {
        var arr = new T[chunkMaxSize];
        var pos = 0;
        foreach (var item in source)
        {
            arr[pos++] = item;
            if (pos == chunkMaxSize)
            {
                yield return arr;
                arr = new T[chunkMaxSize];
                pos = 0;
            }
        }
        if (pos > 0)
        {
            Array.Resize(ref arr, pos);
            yield return arr;
        }
    }

P.P.S 完整的解决方案和BenchmarkDotNet测试在GitHub上


4
为什么你关心性能? - Erik Philips
3
“Span<T>”即将到来。 - Theraot
1
@ErikPhilips,那是一件坏事吗?如果能够快速轻松地实现,为什么要坚持缓慢的实现呢? - Alex Seleznyov
@mjwills 基于LINQ的速度非常慢,我怀疑这个也不会更快。我将添加测试并更新问题。 - Alex Seleznyov
1
@ErikPhilips,你对于优先级的看法是正确的,但总有一个“但是”,对吧?虽然你可能不理解我的需求,但我希望这些话题可以在其他地方讨论。我只是请求编程同行的帮助,仅此而已。 - Alex Seleznyov
显示剩余9条评论
5个回答

10

不确定这个(ArraySegment)的效果如何,但可以尝试一下。我避免使用迭代器,但不确定在这里是否真的有所收益。

public static IEnumerable<T[]> AsChunks<T>(
    this T[] source, int chunkMaxSize)
{
    var chunks = source.Length / chunkMaxSize;
    var leftOver = source.Length % chunkMaxSize;
    var result = new List<T[]>(chunks + 1);
    var offset = 0;

    for (var i = 0; i < chunks; i++)
    {
        result.Add(new ArraySegment<T>(source,
                                       offset, 
                                       chunkMaxSize).ToArray());
        offset += chunkMaxSize;
    }

    if (leftOver > 0)
    {
        result.Add(new ArraySegment<T>(source, 
                                       offset, 
                                       leftOver).ToArray());
    }

    return result;
}

非常感谢ArraySegment。看起来我完全错过了它。学习新东西永远不会太晚 :) - Alex Seleznyov
4
注意,你可以考虑直接返回 ArraySegments 而不需要调用 ToArray。这样就完全不会发生复制操作。对于很多情况来说,这和返回多个数组是一样好的。 - Evk
1
@evk 很好的观点,我应该提到这一点。我想保留原始签名,但如果您可以使用 IEnumerable<ArraySegment<T>>,那么收益是巨大的。 - InBetween
1
是的,重构为 IEnumerable<IReadOnlyList<T>> 甚至更好。但最好不要返回 ArraySegment 本身,因为它甚至实现了索引器,所以使用起来相当困难。 - Evk
@AlexSeleznyov 没有 ToArray()?当然,这样做的缺点是,如果您删除 ToArray(),则块中的任何变异都会影响原始数组。不确定在您的情况下是否存在此问题。 - InBetween
显示剩余8条评论

3

我知道这是一个非常老的话题,但它缺乏使用Memory<T>yield的解决方案。 基于使用ArraySegment的解决方案,我们可以使用

public static IEnumerable<Memory<T>> ChunkViaMemory(this T[] source, int size)
{
    var chunks = source.Length / size;
    for (int i = 0; i < chunks; i++)
    {
        yield return source.AsMemory(i * size, size);
    }
    var leftOver = source.Length % size;
    if (leftOver > 0)
    {
        yield return source.AsMemory(chunks * size, leftOver);
    }
}

我对一个包含50k个项目的数组运行基准测试,将其分成100个项目,使用标准的Chunk方法进行比较,并与ArraySegmentMemory<T>进行比较。

结果显示,这种方法比其他替代方案快得多。

方法 平均值 误差 标准偏差 Gen0 分配的内存
Chunk 1,408.33 微秒 22.359 微秒 20.914 微秒 97.6563 412098 字节
ChunkArraySegment 113.89 微秒 0.750 微秒 0.626 微秒 - 80 字节
ChunkMemory 32.19 微秒 0.439 微秒 0.411 微秒 - 72 字节

如果我们正在寻找最短的执行时间,那么去除yield并分配结果数组可以将时间缩短至9.1微秒,但会增加堆上的内存分配。 - Arek Felinczak
实际上,没有使用yield的版本不会分配太多内存。 var result = new Memory<Person>[chunks + leftOver > 0 ? 1 : 0]; 根据我的基准测试,它的运行速度可以快5倍甚至25倍。 - Arek Felinczak

0
public static List<someObject[]> splitArrayIntoSmallerArrays<someObject>(someObject[] arrayOfSomeObject, int chunkSize)
{
    var output = new List<someObject[]>();
    var newestArray = new List<someObject>();
    for (int i = 0, loopTo = arrayOfSomeObject.Count() - 1; i <= loopTo; i++)
    {

        newestArray.Add(arrayOfSomeObject[i]);
        if (newestArray.Count == chunkSize)
        {
            output.Add(newestArray.ToArray());
            newestArray = new List<someObject>();
        }
    }
    output.Add(newestArray.ToArray());
    return output;
}

0

你可以使用这几行代码

                int chunkSize = 4;
                int skipIndex = 0;
                string[] words = { "one", "two", "three", "four", "five", "six" };
                string[][] chunckResult = new string[words.Length][];

                for (int index = 0; index < words.Length; index++)
                {
                    chunckResult[index] = words.Skip(skipIndex).Take(chunkSize).ToArray();
                    skipIndex += chunkSize;
                    if (skipIndex == words.Length)
                    {
                        break;
                    }
                }

1
你从性能角度测试过你的代码吗?现在几点了? - Alex Seleznyov
欢迎来到 Stack Overflow!通常情况下,如果答案包含代码意图的解释以及为什么这样做可以解决问题而不会引入其他问题,那么它们会更有帮助。 - Tim Diekmann
代码执行的包容率为100%,排除率为0%。 - Babaso Kale
执行时间为总秒数0.0002724。 - Babaso Kale

-1
我使用了这段代码:
源代码:http://blogs.msdn.com/b/pfxteam/archive/2012/11/16/plinq-and-int32-maxvalue.aspx
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int batchSize) {
    if (batchSize <= 0) {
        throw new ArgumentOutOfRangeException(nameof(batchSize));
    }
    using(var enumerator = source.GetEnumerator()) {
        while (enumerator.MoveNext()) {
            yield return YieldBatchElements(enumerator, batchSize - 1);
        }
    }
}

private static IEnumerable<T> YieldBatchElements<T>(IEnumerator<T> source, int batchSize) {
    yield return source.Current;
    for (var i = 0; i < batchSize && source.MoveNext(); i++) {
        yield return source.Current;
    }
}

139.5 微秒。比 Group 好,但比我的任何一个都慢。 - Alex Seleznyov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接