Array.Copy 和 Buffer.BlockCopy 都可以完成相同的任务,但是 BlockCopy
旨在快速复制以字节为基础的原始数组,而 Copy
则是通用实现。我的问题是,在什么情况下应该使用 BlockCopy
?当复制基元类型数组时,是否应该始终使用它,或者只有在编写性能代码时才使用它?使用 Buffer.BlockCopy
是否存在任何固有风险,可能比 Array.Copy
更加危险?
Array.Copy 和 Buffer.BlockCopy 都可以完成相同的任务,但是 BlockCopy
旨在快速复制以字节为基础的原始数组,而 Copy
则是通用实现。我的问题是,在什么情况下应该使用 BlockCopy
?当复制基元类型数组时,是否应该始终使用它,或者只有在编写性能代码时才使用它?使用 Buffer.BlockCopy
是否存在任何固有风险,可能比 Array.Copy
更加危险?
前言
虽然我来晚了,但是这篇文章已经有32k的浏览量,所以值得认真对待。迄今为止,大部分微基准测试代码都存在一个或多个严重的技术缺陷,包括未将内存分配移出测试循环(这会引入严重的GC副作用)、未测试变量与确定性执行流、JIT预热以及未跟踪测试内部的可变性。此外,大多数答案都没有测试不同缓冲区大小和不同原始类型(针对32位或64位系统)的影响。为了更全面地回答这个问题,我使用了我开发的自定义微基准测试框架,尽可能地减少了大部分常见的“坑”。测试在.NET 4.0 Release模式下在32位机器和64位机器上运行。结果是在20次测试运行中平均计算的,每次运行每种方法都有100万次试验。测试的原始类型包括byte
(1字节)、int
(4字节)和double
(8字节)。测试了三种方法:Array.Copy()
、Buffer.BlockCopy()
和在循环中进行简单的索引赋值。数据太多了,无法在这里发布,所以我将总结重要的点。
结论
Array.Copy()
或Buffer.BlockCopy()
更快(对于在32位和64位机器上测试的所有3种原始类型,速度提高了约5%)。此外,与两种替代方法相比,显式循环复制例程的性能变化更小。这种良好的性能几乎肯定是由于CPU L1 / L2 / L3内存缓存利用引用局部性而产生的,同时没有方法调用开销。Array.Copy()
和Buffer.BlockCopy()
的性能完全降低。因此,我认为同样的效果也将适用于缓冲区。byte[]
中最为明显,其中显式循环复制在大缓冲区大小时可能变得慢7倍或更多。Array.Copy()
和Buffer.BlockCopy()
的性能几乎相同。平均而言,Array.Copy()
似乎需要更少的时间(但典型情况下只比Buffer.BlockCopy()
好0.2%-0.5%),尽管Buffer.BlockCopy()
有时会胜过它。由于未知原因,Buffer.BlockCopy()
的内部测试变异性明显高于Array.Copy()
。尽管我尝试了多种缓解措施并且没有可操作的理论,但这种影响无法消除。Array.Copy()
是一种“更智能”,更通用和更安全的方法,除了速度略微更快,平均变化更小之外,它还应该优先于Buffer.BlockCopy()
在几乎所有常见情况下使用。唯一的用例是当源和目标数组值类型不同时(如Ken Smith的答案所指出的那样),Buffer.BlockCopy()
将明显更好。虽然这种情况不常见,但由于不断的“安全”值类型转换,Array.Copy()
在这里的表现可能非常差,而Buffer.BlockCopy()
则直接进行强制转换。Array.Copy()
比Buffer.BlockCopy()
更快地复制相同类型的数组,可以在此处找到。Array.Clear()
开始胜过显式循环赋值清除数组(将其设置为false
、0
或null
)。这与我上面所说的类似结果一致。这些独立的基准测试是在此处发现的:http://manski.net/2012/12/net-array-clear-vs-arrayx-0-performance/ - Special SauceArray.Copy
和Buffer.BlockCopy
在能够操作机器字和/或页面边界时最强大。在64位机器上,这将是4字节边界。此外,正如本帖子所示,方法调用的开销加上边界检查以及缺乏局部性使得Array.Copy
对于小数组(<100个或更少的项)来说是一个相当糟糕的选择。一个(展开的)循环关闭边界检查会快得多,就像你也发现的那样。 - Abel由于Buffer.BlockCopy
的参数基于字节而不是索引,如果你使用Buffer.BlockCopy
,你更有可能搞砸你的代码,相比之下,使用Array.Copy
会更加安全,因此我只会在代码的性能关键部分使用Buffer.BlockCopy
。
UInt16
每个元素占据两个字节。如果您将此数组与数组中的元素数量一起传递给BlockCopy,当然只会复制一半的数组。为使其正常工作,您需要将元素数量乘以每个元素的大小(2)作为长度参数传递。请参考https://msdn.microsoft.com/zh-cn/library/system.buffer.blockcopy(v=vs.110).aspx并在示例中搜索`INT_SIZE`。 - MusiGenesisBuffer.BlockCopy()
的合理场景是,当你得到一组基元类型的数组(比如 shorts),并需要将其转换为字节数组(比如用于网络传输)时。我在处理 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 *)
类型内存管理的最接近方式。
MemoryMarshal.AsBytes<T>
或MemoryMarshal.Cast<TFrom, TTo>
可以让你将一个原始数据序列解释为另一个原始数据序列。 - Timo根据我的测试,性能不是优先选择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
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没有。
Array.Copy
,即使是对于短数组。有趣的是,对于较长的数组,Enumumerable.Concat
也相对较快,因为它会针对实现了ICollection<T>
的可枚举对象进行优化(尽管在.NET Framework中不是这种情况)。Method | ArrayLength | NumberOfArrays | Mean | Error | StdDev |
---|---|---|---|---|---|
EnumerableConcat | 50 | 1 | 63.54 ns | 1.863 ns | 5.435 ns |
ForLoop | 50 | 1 | 95.01 ns | 2.008 ns | 4.694 ns |
ForeachLoop | 50 | 1 | 91.80 ns | 1.953 ns | 4.527 ns |
ArrayCopy | 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 |
ForLoop | 50 | 2 | 188.80 ns | 5.084 ns | 13.659 ns |
ForeachLoop | 50 | 2 | 180.16 ns | 4.953 ns | 14.448 ns |
ArrayCopy | 50 | 2 | 42.47 ns | 0.970 ns | 2.623 ns |
BufferBlockCopy | 50 | 2 | 47.28 ns | 1.038 ns | 2.024 ns |
EnumerableConcat | 50 | 3 | 327.81 ns | 9.332 ns | 27.368 ns |
ForLoop | 50 | 3 | 285.21 ns | 6.028 ns | 17.680 ns |
ForeachLoop | 50 | 3 | 260.04 ns | 5.308 ns | 14.795 ns |
ArrayCopy | 50 | 3 | 62.97 ns | 1.505 ns | 4.366 ns |
BufferBlockCopy | 50 | 3 | 73.45 ns | 3.265 ns | 9.626 ns |
EnumerableConcat | 100 | 1 | 69.27 ns | 1.762 ns | 5.167 ns |
ForLoop | 100 | 1 | 189.44 ns | 3.907 ns | 11.398 ns |
ForeachLoop | 100 | 1 | 163.03 ns | 3.311 ns | 5.057 ns |
ArrayCopy | 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 |
ForLoop | 100 | 2 | 363.01 ns | 7.160 ns | 9.310 ns |
ForeachLoop | 100 | 2 | 357.98 ns | 7.228 ns | 7.734 ns |
ArrayCopy | 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 |
ForLoop | 100 | 3 | 544.59 ns | 16.346 ns | 48.198 ns |
ForeachLoop | 100 | 3 | 522.59 ns | 12.927 ns | 37.914 ns |
ArrayCopy | 100 | 3 | 80.66 ns | 3.154 ns | 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 |
ForLoop | 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 |
ArrayCopy | 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 |
ForLoop | 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 |
ArrayCopy | 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 |
ForLoop | 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 |
ArrayCopy | 1000 | 3 | 422.30 ns | 8.542 ns | 22.502 ns |
BufferBlockCopy | 1000 | 3 | 433.49 ns | 8.808 ns | 20.587 ns |
EnumerableConcat | 10000 | 1 | 1,221.27 ns | 28.138 ns | 82.964 ns |
ForLoop | 10000 | 1 | 16,464.04 ns | 441.552 ns | 1,294.995 ns |
ForeachLoop | 10000 | 1 | 13,916.99 ns | 273.792 ns | 676.746 ns |
ArrayCopy | 10000 | 1 | 1,150.18 ns | 26.901 ns | 79.318 ns |
BufferBlockCopy | 10000 | 1 | 1,154.10 ns | 23.094 ns | 60.025 ns |
EnumerableConcat | 10000 | 2 | 2,798.41 ns | 54.615 ns | 141.952 ns |
ForLoop | 10000 | 2 | 32,570.61 ns | 646.828 ns | 1,473.154 ns |
ForeachLoop | 10000 | 2 | 27,707.12 ns | 545.888 ns | 1,051.741 ns |
ArrayCopy | 10000 | 2 | 2,379.49 ns | 72.264 ns | 213.073 ns |
BufferBlockCopy | 10000 | 2 | 2,374.17 ns | 59.035 ns | 173.140 ns |
EnumerableConcat | 10000 | 3 | 3,885.27 ns | 77.809 ns | 196.633 ns |
ForLoop | 10000 | 3 | 49,833.15 ns | 984.022 ns | 2,097.031 ns |
ForeachLoop | 10000 | 3 | 41,174.21 ns | 819.971 ns | 1,392.373 ns |
ArrayCopy | 10000 | 3 | 3,738.32 ns | 74.331 ns | 91.285 ns |
BufferBlockCopy | 10000 | 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));
}
}
ArrayCopy
的性能比 BufferBlockCopy
更好(除了在长度为1000且有2个数组的情况下)。这是在 .NET 5 中测试的,您知道这种性能是否适用于其他 .NET 框架吗?我特别关注 .NET Standard。 - Péter Szilvásiprivate 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;
}
}
我只想加入我的测试案例,再次证明BlockCopy与Array.Copy相比在'PERFORMANCE'效能方面没有优势。它们在我的机器上在发布模式下似乎具有相同的性能(都需要约66ms才能复制5000万个整数)。在调试模式下,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);
}
Marshal.Copy
:-) 。对于引用类型、复杂值类型以及类型不变的情况,请使用Array.Copy
;对于值类型、字节数组和字节转换之间的“转换”,请使用Buffer.BlockCopy
。例如,如果你知道该怎么做,与StructLayout
结合使用就非常强大。就性能而言,调用非托管的memcpy
/cpblk
是最快的,可以参考 http://code4k.blogspot.nl/2010/10/high-performance-memcpy-gotchas-in-c.html 。 - atlastebyte[]
进行了一些基准测试。在发布版本中没有任何区别。有时候Array.Copy
,有时候Buffer.BlockCopy
(稍微)更快。 - Bitterblue