Array.Copy vs Buffer.BlockCopy

Array.Copy and Buffer.BlockCopy both do the same thing, but BlockCopy is aimed at fast byte-level primitive array copying, whereas Copy is the general-purpose implementation. My question is - under what circumstances should you use BlockCopy? Should you use it at any time when you are copying primitive type arrays, or should you only use it if you're coding for performance? Is there anything inherently dangerous about using Buffer.BlockCopy over Array.Copy?

100847 次浏览

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

使用 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 # 中,这与 C 和 C + + 中常见的 (void *)类型的内存管理非常接近。

根据我的测试,性能是 没有更喜欢 Buffer 的一个原因。区块复制数组。收到。从我的测试数组。复制实际上是 再快点而不是 Buffer。收到。

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

ArrayCopy 比 BlockCopy 更聪明。它指出如何复制元素,如果源和目标是相同的数组。

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

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

we end up with 0,0,1,2,3 as expected.

Try this with BlockCopy and we get: 0,0,2,3,4. If I assign array[0]=-1 after that, it becomes -1,0,2,3,4 as expected, but if the array length is even, like 6, we get -1,256,2,3,4,5. Dangerous stuff. Don't use BlockCopy other than for copying one byte array into another.

There is another case where you can only use Array.Copy: if the array size is longer than 2^31. Array.Copy has an overload with a long size parameter. BlockCopy does not have that.

只是想添加我的测试案例,再次显示 BlockCopy 没有’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);
}

为了参考这个论点,如果一个人不注意他们如何编写这个基准,他们很容易被误导。我编写了一个非常简单的测试来说明这一点。在下面的测试中,如果我在启动 Buffer 之间交换测试的顺序。块复制第一或数组。复制第一个几乎总是最慢的(尽管它很接近)。这意味着由于一系列的原因,我不会进入简单的运行测试多次,尤其是一个接一个将不会给出准确的结果。

我采取了维护测试的方法,对于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

前奏曲

我加入晚了,但有32000的浏览量,这是值得的。到目前为止,公布的答案中的大多数微基准测试代码都存在一个或多个严重的技术缺陷,包括没有将内存分配移出测试循环(这会引入严重的 GC 工件) ,没有测试变量和确定性执行流,JIT 预热,以及没有跟踪测试内部的可变性。此外,大多数答案没有测试不同缓冲区大小和不同基元类型(对于32位或64位系统)的影响。为了更全面地解决这个问题,我将它与我开发的自定义微基准测试框架连接起来,该框架尽可能地减少了大多数常见的“陷阱”。测试运行在。NET 4.0发布模式,同时适用于32位和64位机器。结果平均超过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内存缓存利用,并且没有方法调用开销。
    • 对于 double缓冲区 仅适用于32位机器 :显式循环复制例程比所有测试到100k 的缓冲区大小的替代方案都要好。与其他方法相比,改进效果提高了3-5% 。这是因为当通过本机32位宽度时,Array.Copy()Buffer.BlockCopy()的性能会完全降低。因此,我假设同样的效果也适用于 long缓冲区。
  • 对于超过 ~ 100的缓冲区大小,显式循环复制会比其他两种方法快得多(只有一个特殊的例外)。这种差异在 byte[]中最为明显,其中显式循环复制在较大的缓冲区大小下可以减慢7倍或更多。
  • 一般来说,对于测试的所有3种基本类型和所有缓冲区大小,Array.Copy()Buffer.BlockCopy()的执行几乎相同。平均来说,Array.Copy()似乎有一个非常轻微的边缘约2% 或更少的时间(但0.2% -0.5% 更好是典型的) ,虽然 Buffer.BlockCopy()确实偶尔击败它。由于未知的原因,Buffer.BlockCopy()Array.Copy()具有明显更高的测试内变异性。尽管我尝试了多种缓解措施,但仍无法消除这种影响,而且对其原因也没有一个可操作的理论。
  • 由于 Array.Copy()是一种“更智能”、更通用、更安全的方法,而且速度非常快,平均变异性较小,因此在几乎所有常见情况下,它都应该优于 Buffer.BlockCopy()。只有在源数组和目标数组值类型不同的情况下(正如 Ken Smith 的答案所指出的) ,Buffer.BlockCopy()才会有明显的改进。虽然这种情况并不常见,但是与 Buffer.BlockCopy()的直接转换相比,由于持续的“安全”值类型转换,Array.Copy()在这里的性能可能非常差。
  • 来自 StackOverflow 外部的其他证据表明,对于同一类型的数组复制,Array.Copy()Buffer.BlockCopy()更快,这一点可以在 给你中找到。

开始。NET 5.0.6(x64)-用于将字节数组复制到字节数组-即使对于短数组,Array.Copy似乎也是赢家。有趣的是,对于较长的数组,Enumumerable.Concat的速度也相对较快,因为如果可枚举数实现了 ICollection<T>,它会进行优化(但是对于。NET 框架)。

基准结果和源代码:

方法 数组长度 数组编号 刻薄 错误 性发展
EnumerableConcat 50 1 63.54 ns 1.863 ns 5.435 ns
为了循环 50 1 95.01 ns 2.008 ns 4.694 ns
ForeachLoop 50 1 91.80 ns 1.953 ns 4.527 ns
数组复制 50 1 26.66 ns 1.043 ns 3.075 ns
BufferBlockCopy 缓冲区块复制 50 1 27.65 ns 0.716 ns 2.076 ns
EnumerableConcat 50 2 265.30 ns 9.362 ns 26.558 ns
为了循环 50 2 188.80 ns 5.084 ns 13.659 ns
ForeachLoop 50 2 180.16 ns 4.953 ns 14.448 ns
数组复制 50 2 42.47 ns 0.970 ns 2.623纳秒
BufferBlockCopy 缓冲区块复制 50 2 47.28 ns 1.038 ns 2.024 ns
EnumerableConcat 50 3 327.81 ns 9.332 ns 27.368 ns
为了循环 50 3 285.21 ns 6.028 ns 17.680 ns
ForeachLoop 50 3 260.04 ns 5.308 ns 14.795 ns
数组复制 50 3 62.97 ns 1.505 ns 4.366 ns
BufferBlockCopy 缓冲区块复制 50 3 73.45 ns 3.265纳秒 9.626 ns
EnumerableConcat 100 1 69.27 ns 1.762 ns 5.167 ns
为了循环 100 1 189.44 ns 3.907 ns 11.398 ns
ForeachLoop 100 1 163.03 ns 3.311 ns 5.057 ns
数组复制 100 1 33.23 ns 1.225 ns 3.574 ns
BufferBlockCopy 缓冲区块复制 100 1 35.55 ns 1.004 ns 2.865 ns
EnumerableConcat 100 2 291.20 ns 10.245 ns 30.207 ns
为了循环 100 2 363.01 ns 7.160 ns 9.310 ns
ForeachLoop 100 2 357.98 ns 7.228 ns 7.734 ns
数组复制 100 2 56.59 ns 1.702 ns 5.019 ns
BufferBlockCopy 缓冲区块复制 100 2 61.82 ns 1.747 ns 5.095 ns
EnumerableConcat 100 3 354.19 ns 9.679 ns 27.925 ns
为了循环 100 3 544.59 ns 16.346 ns 48.198 ns
ForeachLoop 100 3 522.59 ns 12.927 ns 37.914 ns
数组复制 100 3 80.66 ns 3.154纳秒 9.300 ns
BufferBlockCopy 缓冲区块复制 100 3 87.21 ns 2.414 ns 7.081 ns
EnumerableConcat 1000 1 181.98 ns 4.073 ns 11.882 ns
为了循环 1000 1 1,643.59 ns 32.135 ns 50.030 ns
ForeachLoop 1000 1 1,444.37 ns 28.705 ns 70.951 ns
数组复制 1000 1 143.55 ns 3.874 ns 11.301 ns
BufferBlockCopy 缓冲区块复制 1000 1 146.69 ns 3.349 ns 9.662 ns
EnumerableConcat 1000 2 525.41 ns 10.621 ns 29.254 ns
为了循环 1000 2 3,264.64 ns 47.449 ns 39.622 ns
ForeachLoop 1000 2 2,818.58 ns 56.489 ns 126.345 ns
数组复制 1000 2 283.73 ns 5.613 ns 15.175 ns
BufferBlockCopy 缓冲区块复制 1000 2 292.29 ns 5.827 ns 15.654 ns
EnumerableConcat 1000 3 712.58 ns 15.274 ns 44.068 ns
为了循环 1000 3 5,005.50 ns 99.791 ns 214.810 ns
ForeachLoop 1000 3 4,272.26 ns 89.589 ns 261.335 ns
数组复制 1000 3 422.30 ns 8.542 ns 22.502 ns
BufferBlockCopy 缓冲区块复制 1000 3 433.49 ns 8.808 ns 20.587 ns
EnumerableConcat 一万 1 1.221.27 ns 28.138 ns 82.964 ns
为了循环 一万 1 16,464.04 ns 441.552 ns 1,294.995 ns
ForeachLoop 一万 1 13,916.99 ns 273.792 ns 676.746 ns
数组复制 一万 1 1150.18 ns 26.901 ns 79.318 ns
BufferBlockCopy 缓冲区块复制 一万 1 1154.10 ns 23.094 ns 60.025 ns
EnumerableConcat 一万 2 2,798.41 ns 54.615 ns 141.952 ns
为了循环 一万 2 32,570.61 ns 646.828 ns 1,473.154 ns
ForeachLoop 一万 2 27,707.12 ns 545.888 ns 1,051.741 ns
数组复制 一万 2 2,379.49 ns 72.264 ns 213.073 ns
BufferBlockCopy 缓冲区块复制 一万 2 2,374.17 ns 59.035 ns 173.140 ns
EnumerableConcat 一万 3 3,885.27 ns 77.809 ns 196.633 ns
为了循环 一万 3 49,833.15 ns 984.022 ns 2,097.031 ns
ForeachLoop 一万 3 41,174.21 ns 819.971 ns 1,392.373 ns
数组复制 一万 3 3,738.32 ns 74.331 ns 91.285 ns
BufferBlockCopy 缓冲区块复制 一万 3 3,839.79 ns 78.865 ns 231.298 ns
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));
}
}