Array.Copy与Buffer.BlockCopy

the*_*oop 119 .net arrays

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

Spe*_*uce 115

序幕

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

小贴士

  • 如果你的缓冲长度为约75-100或更小,显式循环拷贝例行通常比任一更快(约5%),Array.Copy()Buffer.BlockCopy()用于在两个32位和64位机器测试的所有3种原始类型.此外,与两个备选方案相比,显式循环复制例程在性能方面具有明显更低的可变性.良好的性能几乎肯定是由于CPU L1/L2/L3内存缓存利用的引用的局部性以及没有方法调用开销.
    • 仅适用于32位计算机上的double缓冲区:显式循环复制例程优于所有测试高达100k的缓冲区大小的两种选择.改善比其他方法好3-5%.这是因为性能和成为在使本地32位宽度完全降解.因此我假设同样的效果也适用于缓冲区.Array.Copy()Buffer.BlockCopy()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的回答所指出的那样).虽然这种情况并不常见,Array.Copy()但由于持续的"安全"值类型铸造,与直接铸造相比,在这里表现非常差Buffer.BlockCopy().
  • 可以在此处找到来自StackOverflow外部的Array.Copy()Buffer.BlockCopy()同类型阵列复制更快的其他证据.


Ken*_*ith 65

另一个有意义的例子Buffer.BlockCopy()就是当你提供一个基元数组(比如说短路)时,需要把它转换成一个字节数组(比如说,通过网络传输).在处理来自Silverlight AudioSink的音频时,我经常使用此方法.它将示例作为short[]数组提供,但是byte[]在构建提交的数据包时需要将其转换为数组Socket.SendAsync().您可以BitConverter逐个使用并迭代遍历数组,但这样做速度要快得多(在我的测试中大约20倍):

Buffer.BlockCopy(shortSamples, 0, packetBytes, 0, shortSamples.Length * sizeof(short)).  
Run Code Online (Sandbox Code Playgroud)

同样的技巧也是相反的:

Buffer.BlockCopy(packetBytes, readPosition, shortSamples, 0, payloadLength);
Run Code Online (Sandbox Code Playgroud)

这与您在C#(void *)和C++中常见的内存管理类型相当接近.

  • 这是一个很酷的主意 - 你是否遇到过有关字符串的问题? (5认同)
  • 我最下面的抽屉里有一系列短裤。 (3认同)
  • 请注意,自.Net Core 2.1起,您无需复制即可执行此操作。“ MemoryMarshal.AsBytes <T>”或“ MemoryMarshal.Cast <TFrom,TTo>”可让您将一个原语的序列解释为另一原语的后代。 (2认同)

Mus*_*sis 57

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

  • 完全同意.Buffer.BlockCopy存在太多错误空间.保持简单,不要试图从你的程序中挤出任何果汁,直到你知道果汁的位置(剖析). (8认同)
  • 在我自己的测试中,Array.Copy()在性能上与Buffer.BlockCopy()非常相似.在处理640个元素字节数组时,我对Buffer.BlockCopy的速度始终<10%(这是我最感兴趣的那种).但是您应该使用自己的数据进行自己的测试,因为它可能会根据数据,数据类型,数组大小等而有所不同.我应该注意,这两种方法比使用Array.Clone()大约快3倍,并且比在for循环中复制它快20倍. (6认同)
  • @thecoop:如果你正在处理一个byte []那么使用BlockCopy可能没什么问题,除非"byte"的定义后来改为某个字节以外的东西,这可能会对其他部分产生负面影响.无论如何你的代码.:)唯一的另一个潜在问题是BlockCopy只做直接字节,所以它不考虑字节顺序,但这只会在非Windows机器上发挥作用,并且只有当你搞砸了代码时第一名.此外,如果您使用单声道,可能会有一些奇怪的区别. (4认同)
  • 如果你正在处理一个字节[]怎么办?BlockCopy还有其他问题吗? (3认同)
  • @KevinMiller:呃,'UInt16`是每个元素两个字节.如果将此数组与数组中的元素数一起传递给BlockCopy,则当然只会复制一半数组.为了使其正常工作,您需要将元素的数量*times*传递给每个元素(2)的大小作为length参数.https://msdn.microsoft.com/en-us/library/system.buffer.blockcopy(v=vs.110).aspx并在示例中搜索`INT_SIZE`. (2认同)

Kev*_*vin 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));
Run Code Online (Sandbox Code Playgroud)

示例输出:

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000
Run Code Online (Sandbox Code Playgroud)

  • 我认为您的测试方法存在问题.你注意到的大部分时间差异都是应用程序启动,缓存,运行JIT等结果.尝试用较小的缓冲区,但几千次; 然后在一个循环内重复整个测试六次,并且只关注最后一次运行.对于640字节的数组,我自己的测试运行的Buffer.BlockCopy()运行速度比Array.Copy()快5%.不是更快,但有点. (7认同)
  • 我针对特定问题测量了相同的结果,我可以看到,Array.Copy()和Buffer.BlockCopy()**之间没有性能差异.如果有的话,_BlockCopy引入了unsafey,它实际上在一个实例中杀死了我的app_. (2认同)
  • 基于我刚刚制作的测试(https://bitbucket.org/breki74/tutis/commits/cffbddfdec985a66c1e71bf11044dd665c2fa690)我会说当你处理字节数组时,这两种方法之间没有实际的性能差异. (2认同)

use*_*091 6

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 个字节。 (2认同)

use*_*956 5

在 .NET 5.0.6 (x64) 上 - 用于将字节数组复制到字节数组 -Array.Copy即使对于短数组来说似乎也是赢家。有趣的Enumumerable.Concat是,在较长的数组上也相对较快,因为它针对ICollection<T>enumerable 实现它进行了优化(但 .NET Framework 的情况并非如此)。

基准测试结果和源代码:

方法 数组长度 数组数量 意思是 错误 标准差
枚举连接 50 1 63.54纳秒 1.863纳秒 5.435纳秒
For循环 50 1 95.01纳秒 2.008纳秒 4.694纳秒
Foreach循环 50 1 91.80纳秒 1.953纳秒 4.527纳秒
数组复制 50 1 26.66纳秒 1.043纳秒 3.075纳秒
缓冲区块复制 50 1 27.65纳秒 0.716纳秒 2.076纳秒
枚举连接 50 2 265.30纳秒 9.362纳秒 26.558纳秒
For循环 50 2 188.80纳秒 5.084纳秒 13.659纳秒
Foreach循环 50 2 180.16纳秒 4.953纳秒 14.448纳秒
数组复制 50 2 42.47纳秒 0.970纳秒 2.623纳秒
缓冲区块复制 50 2 47.28纳秒 1.038纳秒 2.024纳秒
枚举连接 50 3 327.81纳秒 9.332纳秒 27.368纳秒
For循环 50 3 285.21纳秒 6.028纳秒 17.680纳秒
Foreach循环 50 3 260.04纳秒 5.308纳秒 14.795纳秒
数组复制 50 3 62.97纳秒 1.505纳秒 4.366纳秒
缓冲区块复制 50 3 73.45纳秒 3.265纳秒 9.626纳秒
枚举连接 100 1 69.27纳秒 1.762纳秒 5.167纳秒
For循环 100 1 189.44纳秒 3.907纳秒 11.398纳秒
Foreach循环 100 1 163.03纳秒 3.311纳秒 5.057纳秒
数组复制 100 1 33.23纳秒 1.225纳秒 3.574纳秒
缓冲区块复制 100 1 35.55纳秒 1.004纳秒 2.865纳秒
枚举连接 100 2 291.20纳秒 10.245纳秒 30.207纳秒
For循环 100 2 363.01纳秒 7.160纳秒 9.310纳秒
Foreach循环 100 2 357.98纳秒 7.228纳秒 7.734纳秒
数组复制 100 2 56.59纳秒 1.702纳秒 5.019纳秒
缓冲区块复制 100 2 61.82纳秒 1.747纳秒 5.095纳秒
枚举连接 100 3 354.19纳秒 9.679纳秒 27.925纳秒
For循环 100 3 544.59纳秒 16.346纳秒 48.198纳秒
Foreach循环 100 3 522.59纳秒 12.927纳秒 37.914纳秒
数组复制 100 3 80.66纳秒 3.154纳秒 9.300纳秒
缓冲区块复制 100 3 87.21纳秒 2.414纳秒 7.081纳秒
枚举连接 1000 1 181.98纳秒 4.073纳秒 11.882纳秒
For循环 1000 1 1,643.59 纳秒 32.135纳秒 50.030纳秒
Foreach循环 1000 1 1,444.37 纳秒 28.705纳秒 70.951纳秒
数组复制 1000 1 143.55纳秒 3.874纳秒 11.301纳秒
缓冲区块复制 1000 1 146.69纳秒 3.349纳秒 9.662纳秒
枚举连接 1000 2 525.41纳秒 10.621纳秒 29.254纳秒
For循环 1000 2 3,264.64 纳秒 47.449纳秒 39.622纳秒
Foreach循环 1000 2 2,818.58 纳秒 56.489纳秒 126.345纳秒
数组复制 1000 2 283.73纳秒 5.613纳秒 15.175纳秒
缓冲区块复制 1000 2 292.29纳秒 5.827纳秒 15.654纳秒
枚举连接 1000 3 712.58纳秒 15.274纳秒 44.068纳秒
For循环 1000 3 5,005.50 纳秒 99.791纳秒 214.810纳秒
Foreach循环 1000 3 4,272.26 纳秒 89.589纳秒 261.335纳秒
数组复制 1000 3 422.30纳秒 8.542纳秒 22.502纳秒
缓冲区块复制 1000 3 433.49纳秒 8.808纳秒 20.587纳秒
枚举连接 10000 1 1,221.27 纳秒 28.138纳秒 82.964纳秒
For循环 10000 1 16,464.04 纳秒 441.552纳秒 1,294.995 纳秒
Foreach循环 10000 1 13,916.99 纳秒 273.792纳秒 676.746纳秒
数组复制 10000 1 1,150.18 纳秒 26.901纳秒 79.318纳秒
缓冲区块复制 10000 1 1,154.10 纳秒 23.094纳秒 60.025纳秒
枚举连接 10000 2 2,798.41 纳秒 54.615纳秒 141.952纳秒
For循环 10000 2 32,570.61 纳秒 646.828纳秒 1,473.154 纳秒
Foreach循环 10000 2 27,707.12 纳秒 545.888纳秒 1,051.741 纳秒
数组复制 10000 2 2,379.49 纳秒 72.264纳秒 213.073纳秒
缓冲区块复制 10000 2 2,374.17 纳秒 59.035纳秒 173.140纳秒
枚举连接 10000 3 3,885.27 纳秒 77.809纳秒 196.633纳秒
For循环 10000 3 49,833.15 纳秒 984.022纳秒 2,097.031 纳秒
Foreach循环 10000 3 41,174.21 纳秒 819.971纳秒 1,392.373 纳秒
数组复制 10000 3 3,738.32 纳秒 74.331纳秒 91.285纳秒
缓冲区块复制 10000 3 3,839.79 纳秒 78.865纳秒 231.298纳秒
public class ArrayConcatBenchmark
{
    [Params(50, 100, 1000, 10000)]
    public int ArrayLength;

    [Params(1, 2, 3)]
    public int NumberOfArrays;

    private byte[][] data;

    [GlobalSetup]
    public void GlobalSetup()
    {
        data = new byte[NumberOfArrays][];
        var random = new Random(42);
        for (int i = 0; i < NumberOfArrays; i++)
        {
            data[i] = new byte[ArrayLength];
            random.NextBytes(data[i]);
        }
    }

    [Benchmark]
    public byte[] EnumerableConcat()
    {
        IEnumerable<byte> enumerable = data[0];

        for (int n = 1; n < NumberOfArrays; n++)
        {
            enumerable = enumerable.Concat(data[n]);
        }

        return enumerable.ToArray();
    }

    [Benchmark]
    public byte[] ForLoop()
    {
        var result = new byte[ArrayLength * NumberOfArrays];

        for (int n = 0; n < NumberOfArrays; n++)
        {
            for (int i = 0; i < ArrayLength; i++)
            {
                result[i + n * ArrayLength] = data[n][i];
            }
        }

        return result;
    }

    [Benchmark]
    public byte[] ForeachLoop()
    {
        var result = new byte[ArrayLength * NumberOfArrays];

        for (int n = 0; n < NumberOfArrays; n++)
        {
            int i = 0;

            foreach (var item in data[n])
            {
                result[i + n * ArrayLength] = item;
                i++;
            }
        }

        return result;
    }

    [Benchmark]
    public byte[] ArrayCopy()
    {
        var result = new byte[ArrayLength * NumberOfArrays];

        for (int n = 0; n < NumberOfArrays; n++)
        {
            Array.Copy(data[n], 0, result, n * ArrayLength, ArrayLength);
        }

        return result;
    }

    [Benchmark]
    public byte[] BufferBlockCopy()
    {
        var result = new byte[ArrayLength * NumberOfArrays];

        for (int n = 0; n < NumberOfArrays; n++)
        {
            Buffer.BlockCopy(data[n], 0, result, n * ArrayLength, ArrayLength);
        }

        return result;
    }

    public static void Main(string[] args)
    {
        //Console.WriteLine("Are all results the same: " + AreAllResultsTheSame());
        BenchmarkRunner.Run<ArrayConcatBenchmark>();
    }

    private static bool AreAllResultsTheSame()
    {
        var ac = new ArrayConcatBenchmark()
        {
            NumberOfArrays = 2,
            ArrayLength = 100,
        };

        ac.GlobalSetup();

        var firstResult = ac.EnumerableConcat();
        var otherResults = new[]
        {
            ac.ForLoop(),
            ac.ForeachLoop(),
            ac.ArrayCopy(),
            ac.BufferBlockCopy(),
        };

        return otherResults.All(x => firstResult.SequenceEqual(x));
    }
}
Run Code Online (Sandbox Code Playgroud)