Array.Copy与Buffer.BlockCopy

时间:2009-09-07 15:12:20

标签: .net arrays

Array.CopyBuffer.BlockCopy都做同样的事情,但BlockCopy的目标是快速字节级原始数组复制,而Copy是通用实现。我的问题是 - 在什么情况下你应该使用BlockCopy?您是否应该在复制基本类型数组时随时使用它,或者只有在编写性能时才使用它?使用Buffer.BlockCopy优于Array.Copy是否存在任何内在危险?

7 个答案:

答案 0 :(得分:105)

<强>前奏

我很晚才加入这个聚会,但是有32k的观点,值得做到这一点。到目前为止,已发布的答案中的大多数微基准测试代码都存在一个或多个严重的技术缺陷,包括不将测试循环中的内存分配移出(这引入了严重的GC工件),而不测试变量与确定性执行流程,JIT预热,而不是跟踪测试内的可变性。此外,大多数答案没有测试不同缓冲区大小和不同原始类型(对于32位或64位系统)的影响。为了更全面地解决这个问题,我将它与我开发的自定义微基准测试框架联系起来,这种框架减少了大多数常见的问题&#34;对其他可能。测试在32位计算机和64位计算机上以.NET 4.0发布模式运行。结果在20次测试运行中取平均值,其中每次运行每次方法有100万次试验。测试的原始类型是byte(1字节),int(4字节)和double(8字节)。测试了三种方法:Array.Copy()Buffer.BlockCopy()和循环中的简单每索引分配。数据太大而无法在此发布,因此我将总结重点。

The Takeaways

  • 如果您的缓冲区长度大约为75-100或更短,则对于在两者上测试的所有3种基本类型,显式循环复制例程通常比Array.Copy()Buffer.BlockCopy()更快(约5%) 32位和64位机器。此外,与两个备选方案相比,显式循环复制例程在性能方面具有明显更低的可变性。良好的性能几乎肯定是由于CPU L1 / L2 / L3内存缓存利用locality of reference而没有方法调用开销。
    • 仅适用于32位计算机上的double缓冲区 显式循环复制例程优于所有测试高达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()是一种更智能的方式,更通用,更安全的方法,除了速度更快,平均变异性更小外,还应优先考虑{{1}在几乎所有常见情况下。 Buffer.BlockCopy()明显更好的唯一用例是源和目标数组值类型不同(正如Ken Smith的回答中所指出的那样)。虽然这种情况并不常见,但Buffer.BlockCopy()在这里表现得非常糟糕,因为这种情况不断安全。值类型转换,与直接转换Array.Copy()
  • 相比
  • 可以找到来自StackOverflow外部的其他证据Buffer.BlockCopy()Array.Copy()快于Buffer.BlockCopy()相同类型的数组复制here

答案 1 :(得分:63)

使用Buffer.BlockCopy()时有意义的另一个例子是当你提供一个基元数组(比如说short)时,需要将它转换成一个字节数组(比如,用于传输)一个网络)。在处理来自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#和C ++中常见的(void *)种内存管理中的安全C#一样接近。

答案 2 :(得分:52)

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

答案 3 :(得分:15)

根据我的测试,性能是偏向于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

答案 4 :(得分: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具有long大小参数的重载。 BlockCopy没有。

答案 5 :(得分:1)

To weigh in on this argument, if one is not careful how they author this benchmark they could be easily misled. I wrote a very simple test to illustrate this. In my test below if I swap the order of my tests between starting Buffer.BlockCopy first or Array.Copy the one that goes first is almost always the slowest (although its a close one). This means for a bunch of reasons which I wont go into simply running the tests multiple times esp one after the other will not give accurate results.

I resorted to maintaining the test as is with 1000000 tries each for an array of 1000000 sequential doubles. However in I then disregard the first 900000 cycles and average the remainder. In that case the Buffer is superior.

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

答案 6 :(得分:0)

只想添加我的测试用例,再次显示BlockCopy与Array.Copy相比没有'PERFORMANCE'优势。它们在我的机器上的释放模式下似乎具有相同的性能(复制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);
    }