【问题标题】:Array.Copy vs Buffer.BlockCopyArray.Copy 与 Buffer.BlockCopy
【发布时间】:2009-09-07 15:12:20
【问题描述】:

Array.Copy 和Buffer.BlockCopy 都做同样的事情,但BlockCopy 是针对快速字节级原始数组复制,而Copy 是通用实现。我的问题是——在什么情况下你应该使用BlockCopy?您应该在复制原始类型数组时随时使用它,还是只在为性能编码时才使用它?使用Buffer.BlockCopy 而不是Array.Copy 有什么本质上的危险吗?

【问题讨论】:

  • 别忘了Marshal.Copy :-)。好吧,将Array.Copy 用于引用类型、复杂值类型,如果类型没有改变,则Buffer.BlockCopy 用于值类型、字节数组和字节魔法之间的“转换”。前任如果您知道自己在做什么,那么与StructLayout 的组合非常强大。至于性能,似乎对memcpy/cpblk 的非托管调用是最快的 - 请参阅code4k.blogspot.nl/2010/10/…。
  • 我用byte[] 做了一些基准测试。发布版本没有区别。有时Array.Copy,有时Buffer.BlockCopy(稍微)更快。
  • 刚刚在下面发布的新综合答案。请注意,在缓冲区大小较小的情况下,显式循环复制通常是最好的。
  • 我不认为他们总是做同样的事情 - 例如,您不能使用 Array.Copy 将 Ints 数组复制到 Bytes 数组中
  • Array.Copy 是一个专门的版本——例如它只能复制相同等级的数组。

标签: .net arrays


【解决方案1】:

前奏

我加入聚会很晚,但观看次数已达 32k,值得一试。迄今为止,已发布答案中的大多数微基准测试代码都存在一个或多个严重的技术缺陷,包括没有将内存分配移出测试循环(这会引入严重的 GC 伪影),没有测试变量与确定性执行流程,JIT 预热,并且不跟踪测试内的可变性。此外,大多数答案都没有测试不同缓冲区大小和不同基元类型(相对于 32 位或 64 位系统)的影响。为了更全面地解决这个问题,我将它连接到我开发的自定义微基准测试框架,该框架尽可能减少了大多数常见的“陷阱”。测试在 32 位机器和 64 位机器上以 .NET 4.0 Release 模式运行。结果平均超过 20 次测试运行,其中每次运行每种方法都有 100 万次试验。测试的原始类型为byte(1 字节)、int(4 字节)和double(8 字节)。测试了三种方法:Array.Copy()、Buffer.BlockCopy() 和循环中的简单每个索引分配。数据量太大,这里就不贴了,我总结一下重点。

要点

  • 如果您的缓冲区长度约为 75-100 或更少,对于在 32 位和64 位机器。此外,与两种替代方案相比,显式循环复制例程的性能可变性明显较低。良好的性能几乎肯定是由于locality of reference 被 CPU L1/L2/L3 内存缓存所利用,并且没有方法调用开销。
    • 对于double 缓冲区仅在 32 位机器上:显式循环复制例程优于所有测试高达 100k 的缓冲区大小的替代方案。改进比其他方法好 3-5%。这是因为Array.Copy() 和Buffer.BlockCopy() 的性能在通过本机32 位宽度时会完全下降。因此,我假设同样的效果也适用于 long 缓冲区。
  • 对于超过 ~100 的缓冲区大小,显式循环复制很快就会比其他 2 种方法慢得多(刚刚提到的一个特殊例外)。 byte[] 的差异最为明显,其中显式循环复制在大缓冲区大小时可能会变慢 7 倍或更多。
  • 一般来说,对于测试的所有 3 种原始类型以及所有缓冲区大小,Array.Copy() 和 Buffer.BlockCopy() 的性能几乎相同。平均而言,Array.Copy() 似乎有一个非常轻微的优势,大约 2% 或更少的时间(但通常会提高 0.2% - 0.5%),尽管Buffer.BlockCopy() 确实偶尔会击败它。由于未知原因,Buffer.BlockCopy() 的测试内变异性明显高于Array.Copy()。尽管我尝试了多种缓解措施并且没有关于原因的可操作理论,但仍无法消除这种影响。
  • 因为Array.Copy() 是一种“更智能”、更通用、更安全的方法,除了速度稍快且平均可变性更小之外,在几乎所有常见情况下,它都应优先于Buffer.BlockCopy()。 Buffer.BlockCopy() 会明显更好的唯一用例是源和目标数组值类型不同时(如 Ken Smith 的回答中所指出的)。虽然这种情况并不常见,但与 Buffer.BlockCopy() 的直接转换相比,Array.Copy() 在此处的性能可能很差,因为它会持续进行“安全”值类型转换。
  • 可以在 here 找到来自 StackOverflow 外部的其他证据,即 Array.Copy() 比 Buffer.BlockCopy() 更快地进行相同类型的数组复制。

【讨论】:

  • 顺便说一句,当 .NET 的 Array.Clear() 第一次开始击败数组的显式循环分配清除(设置为 false、@ 987654351@,或null)。这与我上面的类似发现一致。这些单独的基准是在此处在线发现的:manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
  • 当你说缓冲区大小时;你的意思是字节数还是元素数?
  • 在我上面的回答中,“缓冲区长度”和“缓冲区大小”通常都是指元素数。
  • 我有一个例子,我需要经常将大约 8 个字节的数据复制到从源偏移 5 个字节读取的缓冲区中。我发现显式循环复制比使用 Buffer.BlockCopy 或 Array.Copy 快得多。 Loop Results for 1000000 iterations 17.9515ms. Buffer.BlockCopy Results for 1000000 iterations 39.8937ms. Array.Copy Results for 1000000 iterations 45.9059ms 但是,如果副本大小 > ~20 字节,则显式循环会明显变慢。
  • @TodCunningham,8 个字节的数据?你的意思是长等价?转换和复制单个元素(死得很快)或简单地手动展开该循环。
【解决方案2】:

另一个使用Buffer.BlockCopy() 有意义的例子是,当您获得一组原语(例如,短裤),并且需要将其转换为字节数组(例如,通过网络传输时) )。在处理来自 Silverlight AudioSink 的音频时,我经常使用这种方法。它以short[] 数组的形式提供样本,但在构建提交给Socket.SendAsync() 的数据包时,您需要将其转换为byte[] 数组。您可以使用BitConverter,并逐个遍历数组,但这样做要快得多(在我的测试中大约是 20 倍):

Buffer.BlockCopy(shortSamples, 0, packetBytes, 0, shortSamples.Length * sizeof(short)).  

同样的技巧也可以反过来:

Buffer.BlockCopy(packetBytes, readPosition, shortSamples, 0, payloadLength);

这与您在安全的 C# 中获得的 (void *) 类型的内存管理差不多,这种内存管理在 C 和 C++ 中非常常见。

【讨论】:

  • 这是一个很酷的想法——你遇到过字节序问题吗?
  • 是的,我认为您可能会遇到这个问题,具体取决于您的情况。我自己的场景通常是(a)我需要在同一台机器上的字节数组和短数组之间来回切换,或者(b)我碰巧知道我正在将我的数据发送到相同的机器字节序,我控制远程端。但是,如果您使用的协议是远程机器期望数据以网络顺序而不是主机顺序发送,是的,这种方法会给您带来问题。
  • Ken 在他的博客上也有一篇关于 BlockCopy 的文章:blog.wouldbetheologian.com/2011/11/…
  • 请注意,从 .Net Core 2.1 开始,您无需复制即可执行此操作。 MemoryMarshal.AsBytes<T> 或 MemoryMarshal.Cast<TFrom, TTo> 让您将一个基元的序列解释为另一个基元的序列。
  • 我的底部抽屉里有一排短裤。
【解决方案3】:

由于Buffer.BlockCopy 的参数是基于字节而不是基于索引的,因此与使用Array.Copy 相比,您更有可能搞砸代码,所以我只会在性能中使用Buffer.BlockCopy我的代码的关键部分。

【讨论】:

  • 完全同意。 Buffer.BlockCopy 有太多的错误空间。保持简单,不要试图从程序中榨取任何汁液,直到你知道汁液在哪里(分析)。
  • 如果你正在处理一个字节[]怎么办? BlockCopy 还有其他问题吗?
  • @thecoop:如果你正在处理一个字节[],那么使用 BlockCopy 可能没问题,除非“字节”的定义后来更改为字节以外的东西,这可能会有无论如何,这对代码的其他部分产生了相当负面的影响。 :) 唯一的其他潜在问题是 BlockCopy 只处理直接字节,因此它不考虑字节顺序,但这只会在非 Windows 机器上发挥作用,并且只有在你搞砸了代码的情况下第一名。此外,如果您使用单声道,可能会有一些奇怪的差异。
  • 在我自己的测试中,Array.Copy() 在性能上与 Buffer.BlockCopy() 非常相似。在处理 640 个元素字节数组(这是我最感兴趣的类型)时,Buffer.BlockCopy 对我来说始终快 10%。但是您应该使用自己的数据进行自己的测试,因为它可能会根据数据、数据类型、数组大小等而有所不同。我应该注意到,这两种方法都比使用 Array.Clone() 快大约 3 倍,并且可能比在 for 循环中复制它快 20 倍。
  • @KevinMiller:呃,UInt16 是每个元素两个字节。如果将此数组连同数组中的元素数量一起传递给 BlockCopy,当然只会复制一半数组。为了使其正常工作,您需要将元素的数量 times 作为长度参数传递每个元素的大小 (2)。 msdn.microsoft.com/en-us/library/… 并在示例中搜索 INT_SIZE。
【解决方案4】:

根据我的测试,性能不是选择 Buffer.BlockCopy 而不是 Array.Copy 的原因。根据我的测试,Array.Copy 实际上比 Buffer.BlockCopy快。

var buffer = File.ReadAllBytes(...);

var length = buffer.Length;
var copy = new byte[length];

var stopwatch = new Stopwatch();

TimeSpan blockCopyTotal = TimeSpan.Zero, arrayCopyTotal = TimeSpan.Zero;

const int times = 20;

for (int i = 0; i < times; ++i)
{
    stopwatch.Start();
    Buffer.BlockCopy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    blockCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();

    stopwatch.Start();
    Array.Copy(buffer, 0, copy, 0, length);
    stopwatch.Stop();

    arrayCopyTotal += stopwatch.Elapsed;

    stopwatch.Reset();
}

Console.WriteLine("bufferLength: {0}", length);
Console.WriteLine("BlockCopy: {0}", blockCopyTotal);
Console.WriteLine("ArrayCopy: {0}", arrayCopyTotal);
Console.WriteLine("BlockCopy (average): {0}", TimeSpan.FromMilliseconds(blockCopyTotal.TotalMilliseconds / times));
Console.WriteLine("ArrayCopy (average): {0}", TimeSpan.FromMilliseconds(arrayCopyTotal.TotalMilliseconds / times));

示例输出:

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000

【讨论】:

  • 很抱歉这个答案更像是评论,但评论太长了。由于共识似乎是 Buffer.BlockCopy 在性能方面更好,我想每个人都应该意识到我无法通过测试确认这一共识。
  • 我认为您的测试方法有问题。您注意到的大多数时间差异是应用程序启动、缓存自身、运行 JIT 之类的结果。尝试使用较小的缓冲区,但要几千次;然后在一个循环内重复整个测试六次,只注意最后一次运行。在我自己的测试中,对于 640 字节数组,Buffer.BlockCopy() 的运行速度可能比 Array.Copy() 快 5%。没有快多少,但有一点。
  • 我针对特定问题测量了相同的结果,我可以看到 Array.Copy() 和 Buffer.BlockCopy() 之间没有性能差异。如果有的话,BlockCopy 在一个实例中引入了实际上杀死了我的应用程序的不安全。
  • 就像添加 Array.Copy 支持 long 的源位置,因此分解成 big 字节数组不会抛出超出范围的异常。
  • 根据我刚刚进行的测试 (bitbucket.org/breki74/tutis/commits/…),我会说在处理字节数组时这两种方法之间没有实际的性能差异。
【解决方案5】:

ArrayCopy 比 BlockCopy 更智能。如果源和目标是同一个数组,它会计算出如何复制元素。

如果我们用 0,1,2,3,4 填充一个 int 数组并应用:

Array.Copy(array, 0, array, 1, array.Length - 1);

我们最终得到了预期的 0,0,1,2,3。

用 BlockCopy 试试这个,我们得到:0,0,2,3,4。如果我在那之后分配array[0]=-1,它会按预期变为-1,0,2,3,4,但如果数组长度是偶数,比如6,我们得到-1,256,2,3,4,5。危险的东西。除了将一个字节数组复制到另一个字节数组之外,不要使用 BlockCopy。

还有另一种情况,您只能使用 Array.Copy:如果数组大小长于 2^31。 Array.Copy 有一个带有long 大小参数的重载。 BlockCopy 没有。

【讨论】:

  • 使用 BlockCopy 的测试结果并不意外。这是因为块复制尝试一次复制数据块,而不是一次复制一个字节。在 32 位系统上一次复制 4 个字节,在 64 位系统上一次复制 8 个字节。
  • 所以预期的未定义行为。
【解决方案6】:

要权衡这一论点,如果人们不小心他们是如何编写这个基准的,他们很容易被误导。我写了一个非常简单的测试来说明这一点。在下面的测试中,如果我在首先启动 Buffer.BlockCopy 或 Array.Copy 之间交换测试顺序,那么首先启动的测试几乎总是最慢的(尽管它是最接近的)。这意味着出于一系列原因,我不会简单地多次运行测试,尤其是一个接一个地运行测试不会给出准确的结果。

我求助于维护测试,每次尝试 1000000 次,以获得 1000000 个连续双精度数的数组。然而,在我然后忽略前 900000 个周期并平均其余的。在这种情况下,Buffer 更胜一筹。

private static void BenchmarkArrayCopies()
        {
            long[] bufferRes = new long[1000000];
            long[] arrayCopyRes = new long[1000000];
            long[] manualCopyRes = new long[1000000];

            double[] src = Enumerable.Range(0, 1000000).Select(x => (double)x).ToArray();

            for (int i = 0; i < 1000000; i++)
            {
                bufferRes[i] = ArrayCopyTests.ArrayBufferBlockCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                arrayCopyRes[i] = ArrayCopyTests.ArrayCopy(src).Ticks;
            }

            for (int i = 0; i < 1000000; i++)
            {
                manualCopyRes[i] = ArrayCopyTests.ArrayManualCopy(src).Ticks;
            }

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Average());

            //more accurate results - average last 1000

            Console.WriteLine();
            Console.WriteLine("----More accurate comparisons----");

            Console.WriteLine("Loop Copy: {0}", manualCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Array.Copy Copy: {0}", arrayCopyRes.Where((l, i) => i > 900000).ToList().Average());
            Console.WriteLine("Buffer.BlockCopy Copy: {0}", bufferRes.Where((l, i) => i > 900000).ToList().Average());
            Console.ReadLine();
        }

public class ArrayCopyTests
    {
        private const int byteSize = sizeof(double);

        public static TimeSpan ArrayBufferBlockCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Buffer.BlockCopy(original, 0 * byteSize, copy, 0 * byteSize, original.Length * byteSize);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            Array.Copy(original, 0, copy, 0, original.Length);
            watch.Stop();
            return watch.Elapsed;
        }

        public static TimeSpan ArrayManualCopy(double[] original)
        {
            Stopwatch watch = new Stopwatch();
            double[] copy = new double[original.Length];
            watch.Start();
            for (int i = 0; i < original.Length; i++)
            {
                copy[i] = original[i];
            }
            watch.Stop();
            return watch.Elapsed;
        }
    }

https://github.com/chivandikwa/Random-Benchmarks

【讨论】:

  • 我在您的回答中看不到任何计时结果。请包含控制台输出。
【解决方案7】:

只想添加我的测试用例,它再次显示 BlockCopy 与 Array.Copy 相比没有“性能”优势。在我的机器上,它们在发布模式下似乎具有相同的性能(复制 5000 万个整数都需要大约 66 毫秒)。在调试模式下,BlockCopy 稍微快一点。

    private static T[] CopyArray<T>(T[] a) where T:struct 
    {
        T[] res = new T[a.Length];
        int size = Marshal.SizeOf(typeof(T));
        DateTime time1 = DateTime.Now;
        Buffer.BlockCopy(a,0,res,0, size*a.Length);
        Console.WriteLine("Using Buffer blockcopy: {0}", (DateTime.Now - time1).Milliseconds);
        return res;
    }

    static void Main(string[] args)
    {
        int simulation_number = 50000000;
        int[] testarray1 = new int[simulation_number];

        int begin = 0;
        Random r = new Random();
        while (begin != simulation_number)
        {
            testarray1[begin++] = r.Next(0, 10000);
        }

        var copiedarray = CopyArray(testarray1);

        var testarray2 = new int[testarray1.Length];
        DateTime time2 = DateTime.Now;
        Array.Copy(testarray1, testarray2, testarray1.Length);
        Console.WriteLine("Using Array.Copy(): {0}", (DateTime.Now - time2).Milliseconds);
    }

【讨论】:

  • 无意冒犯,但您的测试结果并没有真正的帮助;)首先,“快 20 毫秒”在不知道总时间的情况下不会告诉您任何信息。您还以非常不同的方式进行了这两个测试。 BlockCopy 案例有一个额外的方法调用和目标数组的分配,而您在 Array.Copy 案例中没有这些方法。由于多线程波动(可能的任务切换、核心切换),您每次执行测试时很容易得到不同的结果。
  • @Bunny83 感谢您的评论。我稍微修改了计时器位置,现在应该可以进行更公平的比较。我有点惊讶blockcopy根本不比array.copy快。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-13
  • 1970-01-01
  • 2020-11-14
相关资源
最近更新 更多