Array.Copy vs Buffer.BlockCopy


124

Array.Copy y Buffer.BlockCopy hacen lo mismo, pero BlockCopytienen como objetivo la copia rápida de matrices primitivas a nivel de bytes, mientras que Copyes la implementación de propósito general. Mi pregunta es: ¿bajo qué circunstancias debe usar BlockCopy? ¿Debería usarlo en cualquier momento cuando esté copiando matrices de tipos primitivos, o solo debe usarlo si está codificando para el rendimiento? ¿Hay algo inherentemente peligroso sobre el uso de Buffer.BlockCopymás Array.Copy?


3
No olvides Marshal.Copy:-). Bueno, utilícelo Array.Copypara tipos de referencia, tipos de valores complejos y, si el tipo no cambia, Buffer.BlockCopypara "conversión" entre tipos de valores, conjuntos de bytes y magia de bytes. F.ex. la combinación con StructLayoutes bastante poderosa si sabes lo que estás haciendo. En cuanto al rendimiento, parece que una llamada no administrada a memcpy/ cpblkes la más rápida para eso; consulte code4k.blogspot.nl/2010/10/… .
Atlas

1
Hice algunas pruebas de referencia con byte[]. No hubo diferencia en la versión de lanzamiento. A Array.Copyveces, a veces Buffer.BlockCopy(ligeramente) más rápido.
Bitterblue

Nueva respuesta integral que se acaba de publicar a continuación. Tenga en cuenta que en casos con tamaños de búfer pequeños, la copia de bucle explícita suele ser mejor.
Salsa especial

No creo que siempre hagan lo mismo: no puede usar Array. Copie para copiar una matriz de Ints en una matriz de Bytes, por ejemplo
mcmillab el

Array.Copyes más bien una versión especializada; por ejemplo, solo puede copiar las mismas matrices de rango.
astrowalker

Respuestas:


59

Dado que los parámetros Buffer.BlockCopyestán basados ​​en bytes en lugar de estar basados ​​en índices, es más probable que arruine su código que si lo usa Array.Copy, por lo que solo lo usaría Buffer.BlockCopyen una sección crítica de rendimiento de mi código.


9
Completamente de acuerdo. Hay demasiado margen de error con Buffer.BlockCopy. Hágalo simple y no intente exprimir ningún jugo de su programa hasta que sepa dónde está el jugo (perfil).
Stephen

55
¿Qué pasa si se trata de un byte []? ¿Hay alguna otra trampa con BlockCopy?
thecoop

44
@thecoop: si se trata de un byte [], entonces probablemente esté bien usar BlockCopy, a menos que la definición de "byte" luego se cambie a otra que no sea un byte, lo que probablemente tendría un efecto bastante negativo en otras partes de tu código de todos modos :) El único otro problema potencial es que BlockCopy solo hace bytes directos, por lo que no tiene en cuenta la endianidad, pero esto solo entraría en juego en una máquina que no sea Windows, y solo si hubiera arruinado el código El primer lugar. Además, puede haber alguna diferencia extraña si está usando mono.
MusiGenesis

66
En mis propias pruebas, Array.Copy () es muy similar en rendimiento a Buffer.BlockCopy (). Buffer.BlockCopy es consistentemente <10% más rápido para mí cuando se trata de matrices de bytes de 640 elementos (que es el tipo que más me interesa). Pero debe hacer sus propias pruebas con sus propios datos, ya que presumiblemente variará según los datos, los tipos de datos, los tamaños de matriz, etc. Debo señalar que ambos métodos son aproximadamente 3 veces más rápidos que usar Array.Clone (), y tal vez 20 veces más rápido que copiarlos en un bucle for.
Ken Smith

3
@KevinMiller: uh, UInt16son dos bytes por elemento. Si pasa esta matriz a BlockCopy junto con el número de elementos en la matriz, por supuesto, solo se copiará la mitad de la matriz. Para que esto funcione correctamente, deberá pasar el número de elementos multiplicado por el tamaño de cada elemento (2) como parámetro de longitud. msdn.microsoft.com/en-us/library/… y busque INT_SIZEen los ejemplos.
MusiGenesis

129

Preludio

Me uniré a la fiesta tarde, pero con 32k vistas, vale la pena hacerlo bien. La mayor parte del código de microbenchmarking en las respuestas publicadas hasta ahora adolece de uno o más defectos técnicos severos, que incluyen no mover las asignaciones de memoria fuera de los bucles de prueba (que introduce artefactos GC severos), no probar flujos de ejecución variables versus deterministas, calentamiento JIT, y no rastrear la variabilidad intratest. Además, la mayoría de las respuestas no probaron los efectos de diferentes tamaños de memoria intermedia y tipos primitivos variables (con respecto a los sistemas de 32 bits o de 64 bits). Para abordar esta cuestión de manera más integral, la conecté a un marco de microbenchmarking personalizado que desarrollé que reduce la mayoría de las "trampas" comunes en la medida de lo posible. Las pruebas se ejecutaron en modo de lanzamiento de .NET 4.0 tanto en una máquina de 32 bits como en una máquina de 64 bits. Los resultados se promediaron en 20 ejecuciones de prueba, en las que cada ejecución tuvo 1 millón de pruebas por método. Los tipos primitivos probados fueronbyte(1 byte), int(4 bytes) y double(8 bytes). Se ensayaron tres métodos: Array.Copy(), Buffer.BlockCopy(), y simple asignación per-índice en un bucle. Los datos son demasiado voluminosos para publicar aquí, así que resumiré los puntos importantes.

Las comida para llevar

  • Si la longitud de su 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 cualquiera Array.Copy()o Buffer.BlockCopy()para los 3 tipos primitivos probados en máquinas de 32 bits y 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 casi seguramente a la localidad de referencia explotada por el almacenamiento en memoria caché de la CPU L1 / L2 / L3 junto con la ausencia de sobrecarga de llamadas a métodos.
    • Solo para doublebúferes 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 degrada totalmente al pasar el ancho nativo de 32 bits. Por lo tanto, supongo que el mismo efecto se aplicaría a los longbúferes también.
  • Para tamaños de búfer superiores a ~ 100, la copia explícita en bucle se vuelve mucho más lenta que los otros 2 métodos (con la única excepción particular que acabo de mencionar). La diferencia es más notable byte[]cuando la copia explícita en bucle puede ser 7 veces 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 se Buffer.BlockCopy()realizó de forma casi idéntica. En promedio, Array.Copy()parece tener una ventaja muy leve de aproximadamente 2% o menos tiempo (pero es típico 0.2% - 0.5% mejor), aunque en Buffer.BlockCopy()ocasiones lo superó. Por razones desconocidas, Buffer.BlockCopy()tiene una variabilidad intratest significativamente mayor que Array.Copy(). Este efecto no se pudo eliminar a pesar de que probé mitigaciones múltiples y no tuve 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 un poco más rápido y tener menos variabilidad en promedio, debería preferirse Buffer.BlockCopy()en casi todos los casos comunes. El único caso de uso en el Buffer.BlockCopy()que será significativamente mejor es cuando los tipos de valores de la 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 continua del tipo de valor "seguro", en comparación con la conversión directa de Buffer.BlockCopy().
  • Aquí se puede encontrar evidencia adicional desde fuera de StackOverflow que Array.Copy()es más rápida que Buffer.BlockCopy()para la copia de matriz del mismo tipo .

Como un aparte, sino que también resulta que alrededor de una longitud de matriz de 100 es cuando .NET de Array.Clear()primero comienza a golpear un claro asignación bucle explícito de una matriz (ajuste a false, 0o null). Esto es consistente con mis hallazgos similares anteriores. Estos puntos de referencia separados se descubrieron en línea aquí: manski.net/2012/12/net-array-clear-vs-arrayx-0-performance
Salsa especial

Cuando dices el tamaño del búfer; ¿te refieres en bytes o recuento de elementos?
dmarra

En mi respuesta anterior, tanto "longitud del búfer" como "tamaño del búfer" generalmente se refieren al recuento de elementos.
Salsa especial

Tengo un ejemplo en el que necesito copiar con frecuencia alrededor de 8 bytes de datos en una lectura de búfer desde un desplazamiento de origen en 5 bytes. Encontré que la copia de bucle explícita es significativamente más rápida que usar Buffer.BlockCopy o Array.Copy. Loop Results for 1000000 iterations 17.9515ms. Buffer.BlockCopy Results for 1000000 iterations 39.8937ms. Array.Copy Results for 1000000 iterations 45.9059ms Sin embargo, si el tamaño de la copia> ~ 20 bytes, el bucle explícito es significativamente más lento.
Tod Cunningham el

@TodCunningham, 8 bytes de datos? ¿Te refieres a un equivalente largo? Emite y copia un solo elemento (muy rápido) o simplemente desenrolla ese bucle manualmente.
astrowalker

67

Otro ejemplo de cuándo tiene sentido usarlo Buffer.BlockCopy()es cuando se le proporciona un conjunto de primitivas (por ejemplo, cortos) y necesita convertirlo en un conjunto de bytes (por ejemplo, para la transmisión a través de una red). Utilizo este método con frecuencia cuando trato con audio de Silverlight AudioSink. Proporciona la muestra como una short[]matriz, pero debe convertirla en una byte[]matriz cuando está creando el paquete al que se envía Socket.SendAsync(). Podrías usar BitConvertere 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 también funciona a la inversa:

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

Esto es lo más cercano que puede estar en C # seguro al (void *)tipo de administración de memoria que es tan común en C y C ++.


66
Esa es una idea genial: ¿alguna vez has tenido problemas con la resistencia?
Phillip

Sí, creo que podrías encontrarte con ese problema, dependiendo de tu escenario. Por lo general, mis propios escenarios han sido (a) Necesito alternar entre conjuntos de bytes y conjuntos cortos en la misma máquina, o (b) Sé que estoy enviando mis datos a máquinas de la misma endianness, y que controlo el lado remoto. Pero si estaba utilizando un protocolo para el cual la máquina remota esperaba que los datos se enviaran en orden de red en lugar de orden de host, sí, este enfoque le daría problemas.
Ken Smith

Ken también tiene un artículo sobre BlockCopy en su blog: blog.wouldbetheologian.com/2011/11/…
Drew Noakes

44
Tenga en cuenta que desde .Net Core 2.1 puede hacer esto sin copiar. MemoryMarshal.AsBytes<T>o le MemoryMarshal.Cast<TFrom, TTo>permite interpretar su secuencia de una primitiva como una secuencia de otra primitiva.
Timo

16

Según mis pruebas, el rendimiento no es una razón para preferir Buffer.BlockCopy sobre Array.Copy. De mis pruebas, 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));

Salida de ejemplo:

bufferLength: 396011520
BlockCopy: 00:00:02.0441855
ArrayCopy: 00:00:01.8876299
BlockCopy (average): 00:00:00.1020000
ArrayCopy (average): 00:00:00.0940000

1
Lamento que esta respuesta sea más un comentario, pero fue demasiado largo para un comentario. Como el consenso parecía ser que Buffer.BlockCopy era mejor para el rendimiento, pensé que todos deberían saber que no podía confirmar ese consenso con las pruebas.
Kevin

10
Creo que hay un problema con su metodología de prueba. La mayor parte de la diferencia de tiempo que está notando es el resultado de la aplicación girando, almacenándose en caché, ejecutando el JIT, ese tipo de cosas. Pruébelo con un búfer más pequeño, pero miles de veces; y luego repita toda la prueba dentro de un bucle media docena de veces, y solo preste atención a la última ejecución. Mi propia prueba tiene Buffer.BlockCopy () ejecutándose quizás un 5% más rápido que Array.Copy () para matrices de 640 bytes. No mucho más rápido, pero un poco.
Ken Smith

2
Medí lo mismo para un problema específico, no pude ver ninguna diferencia de rendimiento entre Array.Copy () y Buffer.BlockCopy () . En todo caso, BlockCopy introdujo inseguridad que realmente mató mi aplicación en una instancia.
gatopeich

1
Solo me gusta agregar Array.Copy admite mucho tiempo para la posición de origen, por lo que dividir en grandes conjuntos de bytes no arrojará una excepción fuera de rango.
Alxwest

2
Según las pruebas que acabo de hacer ( bitbucket.org/breki74/tutis/commits/… ), diría que no hay una diferencia práctica de rendimiento entre los dos métodos cuando se trata de conjuntos de bytes.
Igor Brejc

4

ArrayCopy es más inteligente que BlockCopy. Descubre cómo copiar elementos si el origen y el destino son la misma matriz.

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]=-1despué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 en el que solo puede usar Array.Copy: si el tamaño de la matriz es mayor que 2 ^ 31. Array.Copy tiene una sobrecarga con un longparámetro de tamaño. BlockCopy no tiene eso.


2
Los resultados de sus pruebas con BlockCopy no son inesperados. Esto se debe a que Block copy intenta copiar fragmentos de datos a la vez en lugar de un byte a la vez. En un sistema de 32 bits copia 4 bytes a la vez, en un sistema de 64 bits copia 8 bytes a la vez.
Pharap

Tan esperado comportamiento indefinido.
binki

2

Para sopesar este argumento, si uno no tiene cuidado de cómo crean este punto de referencia, podrían ser engañados fácilmente. 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 por las que no entraré, simplemente ejecutar las pruebas varias veces, especialmente una tras otra, no dará resultados precisos.

Recurrí a mantener la prueba como está con 1000000 intentos cada uno para una matriz de 1000000 dobles secuenciales. Sin embargo, en este caso, ignoro los primeros 900000 ciclos y promedio el resto. 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


55
No veo ningún resultado de tiempo en tu respuesta. Incluya la salida de la consola.
ToolmakerSteve

0

Solo quiero agregar mi caso de prueba que muestra nuevamente que BlockCopy no tiene ningún beneficio de 'RENDIMIENTO' sobre Array.Copy. Parecen tener el mismo rendimiento en el modo de lanzamiento en mi máquina (ambos tardan unos 66 ms en copiar 50 millones de enteros). En modo de depuración, BlockCopy es solo un poco más rápido.

    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);
    }

3
Sin ofender, pero el resultado de su prueba no es realmente útil;) En primer lugar, "20 ms más rápido" no le dice nada sin saber el tiempo total. También llevó a cabo esas dos pruebas de manera muy diferente. El caso BlockCopy tiene una llamada al método adicional y la asignación de su matriz de destino que no tiene en su caso Array.Copy. Debido a las fluctuaciones de subprocesos múltiples (posible cambio de tarea, cambio de núcleo), puede obtener fácilmente diferentes resultados cada vez que ejecute la prueba.
Bunny83

@ Bunny83 gracias por el comentario. He modificado ligeramente la ubicación del temporizador, lo que debería dar una comparación más justa ahora. Y estoy un poco sorprendido de que blockcopy no sea más rápido que array.copy.
stt106
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.