Array.Copy versus Buffer.BlockCopy

Array.Copy y Buffer.BlockCopy hacen lo mismo, pero BlockCopy apunta a la copia de matriz primitiva de nivel de byte rápido, mientras que Copy es la implementación de propósito general. Mi pregunta es: ¿en qué circunstancias debe usar BlockCopy ? ¿Debería usarlo en cualquier momento cuando está copiando matrices de tipo primitivo, o debería usarlo solo si está codificando el rendimiento? ¿Hay algo inherentemente peligroso sobre usar Buffer.BlockCopy sobre Array.Copy ?

Dado que los parámetros de Buffer.BlockCopy están basados ​​en bytes en lugar de basados ​​en índices, es más probable que Array.Copy tu código que si utilizas Array.Copy , así que solo usaría Buffer.BlockCopy en una sección de rendimiento crítico. mi código.

Preludio

Me estoy uniendo a la fiesta tarde, pero con 32k visitas, vale la pena hacer esto bien. La mayoría del código de microencuadre en las respuestas publicadas hasta ahora sufre de uno o más defectos técnicos severos, incluyendo no mover las asignaciones de memoria de los bucles de prueba (lo que introduce artefactos severos de GC), no probar flujos de ejecución variables vs. deterministas, calentamiento JIT, y no rastreando la variabilidad dentro de la prueba. Además, la mayoría de las respuestas no probaron los efectos de diferentes tamaños de buffer y tipos primitivos variables (con respecto a los sistemas de 32 bits o de 64 bits). Para abordar esta cuestión de forma más exhaustiva, la conecté a un marco de microenmarcado personalizado que desarrollé y que reduce la mayoría de los “errores” comunes en la medida de lo posible. Las pruebas se realizaron en el modo de lanzamiento de .NET 4.0 tanto en una máquina de 32 bits como en una de 64 bits. Los resultados se promediaron en 20 pruebas, en las que cada experimento tuvo 1 millón de ensayos por método. Los tipos primitivos probados fueron byte (1 byte), int (4 bytes) y double (8 bytes). Se probaron tres métodos: Array.Copy() , Buffer.BlockCopy() y asignación simple por índice en un bucle. Los datos son demasiado voluminosos para publicarlos aquí, por lo que resumiré los puntos importantes.

The Takeaways

  • Si la longitud del búfer es de aproximadamente 75-100 o menos, una rutina de copia de bucle explícita suele ser más rápida (aproximadamente un 5%) que Array.Copy() o Buffer.BlockCopy() para los 3 tipos primitivos probados en ambos 32 bits y máquinas de 64 bits. Además, la rutina de copia de bucle explícita tiene una variabilidad notablemente menor en el rendimiento en comparación con las dos alternativas. El buen rendimiento se debe seguramente a la localidad de referencia explotada por el almacenamiento en memoria caché de la CPU L1 / L2 / L3 junto con la sobrecarga de llamadas a métodos.
    • Para búferes double solo en máquinas de 32 bits : la rutina de copia de bucle explícita es mejor que ambas alternativas para todos los tamaños de búfer probados hasta 100k. La mejora es 3-5% mejor que los otros métodos. Esto se debe a que el rendimiento de Array.Copy() y Buffer.BlockCopy() se degradan por completo al pasar el ancho nativo de 32 bits. Por lo tanto, supongo que el mismo efecto se aplicaría a long buffers long también.
  • Para tamaños de buffer superiores a ~ 100, la copia explícita de bucles se vuelve mucho más lenta que los otros 2 métodos (con la única excepción particular que se acaba de mencionar). La diferencia es más notable con byte[] , donde la copia de bucle explícita puede ser 7x o más lenta en tamaños de búfer grandes.
  • En general, para los 3 tipos primitivos probados y en todos los tamaños de búfer, Array.Copy() y Buffer.BlockCopy() comportaron de forma casi idéntica. En promedio, Array.Copy() parece tener un margen muy leve de aproximadamente 2% o menos de tiempo tomado (pero 0.2% – 0.5% mejor es típico), aunque Buffer.BlockCopy() ocasionalmente lo superó. Por razones desconocidas, Buffer.BlockCopy() tiene una variabilidad Array.Copy() notablemente mayor que Array.Copy() . Este efecto no pudo ser eliminado a pesar de que intenté múltiples mitigaciones y no tenía una teoría operable sobre por qué.
  • Debido a que Array.Copy() es un método “más inteligente”, más general y mucho más seguro, además de ser muy ligeramente más rápido y tener una menor variabilidad en promedio, debería preferirse a Buffer.BlockCopy() en casi todos los casos comunes. El único caso de uso donde Buffer.BlockCopy() será significativamente mejor es cuando los tipos de valor de matriz de origen y destino son diferentes (como se señala en la respuesta de Ken Smith). Si bien este escenario no es común, Array.Copy() puede funcionar muy mal aquí debido a la conversión de tipo de valor “seguro” continuo, en comparación con la conversión directa de Buffer.BlockCopy() .
  • Se puede encontrar evidencia adicional desde fuera de StackOverflow de que Array.Copy() es más rápido que Buffer.BlockCopy() para la copia de matriz del mismo tipo aquí .

Otro ejemplo de cuándo tiene sentido usar Buffer.BlockCopy() es cuando se le proporciona una matriz de elementos primitivos (por ejemplo, pantalones cortos) y necesita convertirla en una matriz de bytes (por ejemplo, para la transmisión a través de una red) . Utilizo este método con frecuencia cuando trato con el audio del Silverlight AudioSink. Proporciona la muestra como una matriz short[] , pero necesita convertirla a una matriz de byte[] cuando está Socket.SendAsync() el paquete que envía a Socket.SendAsync() . Puede usar BitConverter e iterar a través de la matriz uno por uno, pero es mucho más rápido (aproximadamente 20 veces en mis pruebas) solo para hacer esto:

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

Y el mismo truco funciona al revés también:

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

Esto es lo más cerca que se puede encontrar en C # seguro para el tipo de gestión de memoria (void *) que es tan común en C y C ++.

Según mis pruebas, el rendimiento no es una razón para preferir Buffer.BlockCopy a Array.Copy. De mi prueba Array.Copy es en realidad más rápido que 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)); 

Ejemplo de salida:

 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 es más inteligente que BlockCopy. Se da cuenta de cómo copiar elementos si el origen y el destino son el mismo conjunto.

Si llenamos una matriz int con 0,1,2,3,4 y aplicamos:

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

terminamos con 0,0,1,2,3 como se esperaba.

Pruebe esto con BlockCopy y obtenemos: 0,0,2,3,4. Si asigno array[0]=-1 después de eso, se convierte en -1,0,2,3,4 como se esperaba, pero si la longitud de la matriz es par, como 6, obtenemos -1,256,2,3,4, 5. Cosas peligrosas No use BlockCopy que no sea para copiar una matriz de bytes en otra.

Hay otro caso donde solo puedes usar Array.Copy: si el tamaño de la matriz es más largo que 2 ^ 31. Array.Copy tiene una sobrecarga con un parámetro de tamaño long . BlockCopy no tiene eso.

Para tener en cuenta este argumento, si uno no tiene cuidado de cómo crean este punto de referencia, podrían ser fácilmente engañados. Escribí una prueba muy simple para ilustrar esto. En mi prueba a continuación, si cambio el orden de mis pruebas entre iniciar Buffer.BlockCopy primero o Array.Copy, el que va primero es casi siempre el más lento (aunque está cerca). Esto significa que por un montón de razones que no voy a entrar simplemente ejecutando las pruebas varias veces, una después de la otra no dará resultados precisos.

Recurrí a mantener la prueba como está con 1000000 bashs cada uno para una matriz de 1000000 dobles secuenciales. Sin embargo, en ese momento, ignoro los primeros 900000 ciclos y promedió el rest. En ese caso, el Buffer es 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

Solo quiero agregar mi caso de prueba que muestra nuevamente que BlockCopy no tiene el beneficio ‘PERFORMANCE’ sobre Array.Copy. Parecen tener el mismo rendimiento en el modo de lanzamiento en mi máquina (ambos toman alrededor de 66ms para copiar 50 millones de enteros). En el modo de depuración, BlockCopy es marginalmente más rápido.

  private static T[] CopyArray(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); } 

La evaluación comparativa de ambos métodos copió la matriz de bytes y no encontró diferencias significativas. Aunque si el tipo de matriz se cambió a int , el rendimiento de Buffer.BlockCopy fue 4 veces mayor que Array.CopyTo.

Array.CopyTo (Mb / s): 2188,034

Buffer.BlockCopy (Mb / s): 8827,586

Core i5, Windows 10 64 bit, .NET Core, matriz de 1Gb

  private static void ArrayCopy4Byte(Stopwatch sw, int size, int[] src) { var dst = new int[size]; sw.Restart(); src.CopyTo(dst, 0); sw.Stop(); Console.WriteLine("Array.CopyTo (Mb/s): " + (float)size / 1024 / 1024 / sw.ElapsedMilliseconds * 1000 * sizeof(int)); } 

y

  private static void BufferCopy4Byte(Stopwatch sw, int size, int[] src) { var dst = new int[size]; sw.Restart(); Buffer.BlockCopy(src, 0, dst, 0, src.Length); sw.Stop(); Console.Write("Buffer.BlockCopy (Mb/s): " + (float)size / 1024 / 1024 / sw.ElapsedMilliseconds * 1000 * sizeof(int)); WriteFirst10(src); }