Array.Copy与Buffer.BlockCopy


124

Array.Copy Buffer.BlockCopy都做同样的事情,但是BlockCopy目标是快速字节级原始数组复制,而这Copy是通用的实现。我的问题是-在什么情况下应该使用BlockCopy?在复制原始类型数组时,应该在任何时候使用它,还是仅在为性能而编码时才使用它?使用Buffer.BlockCopyover 有天生的危险Array.Copy吗?


3
不要忘记Marshal.Copy:-)。好吧,Array.Copy用于引用类型,复杂值类型,如果类型不变,则Buffer.BlockCopy用于值类型,字节数组和字节魔术之间的“转换”。F.ex. StructLayout如果您知道自己在做什么,则与的组合将非常强大。至于性能方面,它似乎非托管的呼叫memcpy/ cpblk最快为-看到code4k.blogspot.nl/2010/10/...
atlaste 2013年

1
我使用进行了一些基准测试byte[]。发行版本没有什么区别。有时Array.Copy(有时Buffer.BlockCopy)更快。
Bitterblue 2014年

新的综合答案刚刚发布在下面。请注意,在缓冲区较小的情况下,通常最好进行显式循环复制。
Special Sauce 2015年

我不认为他们总是做同样的事情-您不能使用Array.Copy将Ints数组复制到Bytes数组
mcmillab

Array.Copy而是一个专门的版本-例如,它只能复制相同的等级数组。
astrowalker

Answers:


59

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


9
完全同意。Buffer.BlockCopy有太多的错误余地。保持简单,在知道果汁在哪里之前,不要尝试从程序中榨汁。
斯蒂芬,

5
如果要处理一个字节[]怎么办?BlockCopy还有其他陷阱吗?
thecoop

4
@thecoop:如果您要处理byte [],那么使用BlockCopy可能会很好,除非稍后将“ byte”的定义更改为非byte的内容,这可能会对它的其他部分产生负面影响无论如何,您的代码。:)唯一可能遇到的问题是BlockCopy仅处理直字节,因此不考虑字节顺序,但这仅在非Windows计算机上起作用,并且仅当您弄乱了其中的代码时才会起作用。第一名。另外,如果您使用单声道,可能会有一些怪异的区别。
MusiGenesis

6
在我自己的测试中,Array.Copy()在性能上与Buffer.BlockCopy()非常相似。在处理640个元素字节数组(这是我最感兴趣的排序)时,Buffer.BlockCopy对我而言始终比<10%快。但是您应该对自己的数据进行测试,因为它可能会根据数据,数据类型,数组大小等而有所不同。我应该注意到,这两种方法的速度大约比使用Array.Clone()快3倍,也许比在for循环中复制它快20倍。
肯·史密斯,

3
@KevinMiller:嗯,UInt16每个元素两个字节。如果将此数组与数组中的元素数一起传递给BlockCopy,则当然只会复制一半的数组。为了使此方法正常运行,您需要将元素数乘以每个元素(2)的大小作为长度参数。msdn.microsoft.com/en-us/library/…INT_SIZE在示例中进行搜索。
MusiGenesis

129

序幕

我参加晚会很晚,但是有32,000次观看,值得做对这一点。到目前为止,已发布答案中的大多数微基准测试代码都存在一个或多个严重的技术缺陷,包括没有将内存分配移出测试循环(这会引入严重的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位计算机上测试的所有3种原始类型中的任何一种Array.Copy()Buffer.BlockCopy()全部都快(约5%)。另外,与这两种选择相比,显式循环复制例程的性能差异明显较低。几乎可以肯定,良好的性能归因于CPU L1 / L2 / L3内存缓存利用的引用局部性以及无方法调用开销。
    • 仅对于32位计算机上的double缓冲区对于测试到100k以下的所有缓冲区大小,显式循环复制例程都优于这两种选择。与其他方法相比,改进幅度为3-5%。这是因为性能和变得完全时使本机32比特宽度降解。因此,我假设相同的效果也适用于缓冲区。Array.Copy()Buffer.BlockCopy()long
  • 对于超过约100的缓冲区,显式循环复制的速度将比其他两种方法慢得多(只有一种特殊的例外情况刚刚提到)。两者之间的差异最为明显byte[],在大缓冲区大小下,显式循环复制的速度可能会慢7倍或更多。
  • 通常,对于所有3种原始类型进行测试,并在所有缓冲区大小中进行测试,Array.Copy()并且Buffer.BlockCopy()执行几乎相同。平均而言,Array.Copy()虽然Buffer.BlockCopy()偶尔会击败它,但似乎只占用了大约2%或更短时间的极微优势(但通常会提高0.2%-0.5%)。由于未知原因,Buffer.BlockCopy()测试内变异性明显高于Array.Copy()。尽管我尝试了多种缓解措施,但对原因却没有可操作的理论,但无法消除这种影响。
  • 因为它Array.Copy()是一种“更智能”,更通用且更安全的方法,除了速度稍快且平均变异性较小外,Buffer.BlockCopy()在几乎所有常见情况下均应优先使用该方法。唯一Buffer.BlockCopy()显着更好的用例是当源数组和目标数组的值类型不同时(如Ken Smith的答案所指出)。虽然这种情况并不常见,Array.Copy()但与的直接转换相比,由于连续的“安全”值类型转换,此处的效果可能会很差Buffer.BlockCopy()
  • 此处可以找到来自StackOverflow外部的其他证据,该证据Array.Copy()Buffer.BlockCopy()同类型数组复制的速度更快。

顺便说一句,它也证明,周围的100的阵列长度是当NET的Array.Clear()第一开始打阵列的显式循环分配清零(设置false0null)。这与我上面类似的发现是一致的。这些单独的基准是在此处在线找到的:manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Special Sauce

当您说缓冲区大小时;你是指字节还是元素计数?
dmarra

在我上面的答案中,“缓冲区长度”和“缓冲区大小”通常都指元素计数。
特制酱

我有一个示例,在该示例中,我需要经常将大约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字节,则显式循环会明显变慢。
Tod Cunningham

@ TodCunningham,8个字节的数据?你是说长等价的?要么投射并复制单个元素(很快就死了),要么简单地手动展开该循环。
astrowalker

67

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

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

同样的技巧也可以反向使用:

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

这与您在安全的C#中接近(void *)C和C ++中常见的那种内存管理差不多。


6
那是个好主意-您是否遇到过字节序问题?
菲利普(Phillip)

是的,我认为您可能会遇到此问题,具体取决于您的情况。我自己的场景通常是(a)我需要在同一台机器上的字节数组和短数组之间来回切换,或者(b)我碰巧知道我正在将数据发送到同一台机器上字节序,由我控制远程端。但是,如果您使用的协议要求远程计算机以网络顺序而不是主机顺序发送数据,是的,这种方法会给您带来麻烦。
肯·史密斯,

肯(Ken)在他的博客上也发表了有关BlockCopy的文章:blog.wouldbetheologian.com/2011/11/…–
Drew Noakes

4
请注意,自.Net Core 2.1起,您无需复制即可执行此操作。MemoryMarshal.AsBytes<T>MemoryMarshal.Cast<TFrom, TTo>让您将一个原语的序列解释为另一原语的后代。
蒂莫

16

根据我的测试,性能并不是理由要比Array.Copy更喜欢Buffer.BlockCopy。在我的测试中,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

1
抱歉,此答案更多是评论,但评论太久了。由于共识似乎是Buffer.BlockCopy的性能更好,因此我认为每个人都应该意识到我无法通过测试来确认该共识。
凯文

10
我认为您的测试方法存在问题。您注意到的大多数时差是应用程序旋转,缓存自身,运行JIT等等的结果。尝试使用较小的缓冲区,但要尝试数千次;然后在一个循环中重复整个测试六次,只注意最后一次运行。我自己的测试中,对于640字节数组,Buffer.BlockCopy()的运行速度可能比Array.Copy()快5%。快不了多少,但快了一点。
肯·史密斯,

2
我针对一个特定问题进行了相同的测量,我看不到Array.Copy()和Buffer.BlockCopy()之间的性能差异。如果有的话,BlockCopy引入了不安全因素,实际上在一个实例中杀死了我的应用程序
gatopeich 2012年

1
就像添加Array.Copy对于源位置来说支持很长的时间,因此分成字节数组不会引发超出范围的异常。
Alxwest 2012年

2
基于我刚刚进行的测试(bitbucket.org/breki74/tutis/commits/…),我要说的是,在处理字节数组时,这两种方法之间没有实际的性能差异。
Igor Brejc 2014年

4

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具有带有longsize参数的重载。BlockCopy没有那个。


2
使用BlockCopy进行测试的结果并不意外。这是因为块复制尝试一次复制数据块,而不是一次复制一个字节。在32位系统上,它一次复制4个字节,在64位系统上,它一次复制8个字节。
法拉普

因此预期的未定义行为。
binki 2015年

2

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

我采取的措施是按1000000次尝试对1000000个连续双打数组进行每次维护。但是,然后我忽略了前900000个周期,并对其余的取平均值。在这种情况下,缓冲区是优越的。

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


5
您的回答没有任何计时结果。请包括控制台输出。
制造商史蒂夫(Steve)

0

只是想添加我的测试用例,它再次显示BlockCopy与Array.Copy相比没有“性能”优势。它们在我的机器上的释放模式下似乎具有相同的性能(复制5000万个整数都花费大约66ms)。在调试模式下,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);
    }

3
没冒犯,但您的测试结果并没有真正的帮助;)首先,“不快于20ms”不会告诉您任何事情,而无需了解整个时间。您还以非常不同的方式执行了这两个测试。BlockCopy案例有一个额外的方法调用,以及您在Array.Copy案例中没有的目标数组的分配。由于多线程的波动(可能的任务切换,核心切换),您每次执行测试时都可以轻松获得不同的结果。
Bunny83年

@ Bunny83感谢您的评论。我稍微修改了计时器的位置,现在应该可以进行更公平的比较。让我感到惊讶的是,块复制根本没有比array.copy快。
stt106 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.