¿Es mejor usar std :: memcpy () o std :: copy () en términos de rendimiento?

163

¿Es mejor usar memcpycomo se muestra a continuación o es mejor usar std::copy()en términos de rendimiento? ¿Por qué?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);
user576670
fuente
Tenga en cuenta que charse puede firmar o no, según la implementación. Si el número de bytes puede ser> = 128, utilícelo unsigned charpara sus conjuntos de bytes. (El (int *)elenco también sería más seguro (unsigned int *)).
Dan Breslau
13
¿Por qué no estás usando std::vector<char>? O desde que usted dice bits, std::bitset?
GManNickG
2
En realidad, ¿podría explicarme qué (int*) copyMe->bits[0]hace?
user3728501
44
No estoy seguro de por qué algo que parece un desastre con tan poco contexto vital proporcionado estaba en +81, pero bueno. @ user3728501 supongo que el inicio del búfer tiene un inttamaño que dicta, pero parece una receta para un desastre definido por la implementación, como tantas otras cosas aquí.
underscore_d
2
De hecho, ese (int *)reparto es solo un comportamiento indefinido puro, no definido por la implementación. Tratar de escribir letras mediante un elenco viola las estrictas reglas de alias y, por lo tanto, no está totalmente definido por el Estándar. (Además, en C ++, aunque no en C, no puede escribir juegos de palabras a través de unionninguno de los dos). La única excepción es si está convirtiendo a una variante de char*, pero la asignación no es simétrica.
underscore_d

Respuestas:

207

Voy a ir en contra de la sabiduría general aquí que std::copytendrá una pérdida de rendimiento leve, casi imperceptible. Acabo de hacer una prueba y descubrí que no era cierto: noté una diferencia de rendimiento. Sin embargo, el ganador fue std::copy.

Escribí una implementación de C ++ SHA-2. En mi prueba, combiné 5 cadenas con las cuatro versiones SHA-2 (224, 256, 384, 512) y realicé un bucle 300 veces. Mido los tiempos usando Boost.timer. Ese contador de 300 bucles es suficiente para estabilizar completamente mis resultados. Ejecuté la prueba 5 veces cada uno, alternando entre la memcpyversión y la std::copyversión. Mi código aprovecha la captura de datos en la mayor cantidad de fragmentos posible (muchas otras implementaciones operan con char/ char *, mientras que opero con T/ T *(donde Tes el tipo más grande en la implementación del usuario que tiene un comportamiento de desbordamiento correcto), por lo que el acceso a la memoria es rápido Los tipos más grandes que puedo son fundamentales para el rendimiento de mi algoritmo. Estos son mis resultados:

Tiempo (en segundos) para completar la ejecución de las pruebas SHA-2

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

Incremento promedio total en la velocidad de std :: copy over memcpy: 2.99%

Mi compilador es gcc 4.6.3 en Fedora 16 x86_64. Mis banderas de optimización son -Ofast -march=native -funsafe-loop-optimizations.

Código para mis implementaciones SHA-2.

Decidí ejecutar una prueba en mi implementación MD5 también. Los resultados fueron mucho menos estables, así que decidí hacer 10 carreras. Sin embargo, después de mis primeros intentos, obtuve resultados que variaron enormemente de una ejecución a la siguiente, así que supongo que estaba ocurriendo algún tipo de actividad del sistema operativo. Decidí comenzar de nuevo.

La misma configuración y banderas del compilador. Solo hay una versión de MD5, y es más rápida que SHA-2, así que hice 3000 bucles en un conjunto similar de 5 cadenas de prueba.

Estos son mis 10 resultados finales:

Tiempo (en segundos) para completar la ejecución de las pruebas MD5

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

Disminución promedio total en la velocidad de std :: copy over memcpy: 0.11%

Código para mi implementación MD5

Estos resultados sugieren que hay alguna optimización que std :: copy utilizada en mis pruebas SHA-2 que std::copyno podría usar en mis pruebas MD5. En las pruebas SHA-2, ambas matrices se crearon en la misma función que llamó a std::copy/ memcpy. En mis pruebas MD5, una de las matrices se pasó a la función como parámetro de función.

Hice un poco más de pruebas para ver qué podía hacer para std::copyacelerar de nuevo. La respuesta resultó ser simple: active la optimización del tiempo de enlace. Estos son mis resultados con LTO activado (opción -flto en gcc):

Tiempo (en segundos) para completar la ejecución de las pruebas MD5 con -flto

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

Incremento promedio total en la velocidad de std :: copy over memcpy: 0.72%

En resumen, no parece haber una penalización de rendimiento por usar std::copy. De hecho, parece haber una ganancia de rendimiento.

Explicación de resultados.

Entonces, ¿por qué podría std::copyaumentar el rendimiento?

Primero, no esperaría que fuera más lenta para ninguna implementación, siempre y cuando la optimización de inlining esté activada. Todos los compiladores están en línea agresivamente; posiblemente sea la optimización más importante porque permite muchas otras optimizaciones. std::copypuedo (y sospecho que todas las implementaciones del mundo real lo hacen) detectar que los argumentos son trivialmente copiables y que la memoria se presenta secuencialmente. Esto significa que en el peor de los casos, cuando memcpyes legal, no std::copydebería funcionar peor. La implementación trivial de std::copyeso difiere memcpydebe cumplir con los criterios de su compilador de "siempre alinear esto al optimizar la velocidad o el tamaño".

Sin embargo, std::copytambién guarda más de su información. Cuando llama std::copy, la función mantiene los tipos intactos. memcpyopera en void *, que descarta casi toda la información útil. Por ejemplo, si paso una matriz de std::uint64_t, el compilador o el implementador de la biblioteca pueden aprovechar la alineación de 64 bits std::copy, pero puede ser más difícil hacerlo memcpy. Muchas implementaciones de algoritmos como este funcionan trabajando primero en la porción no alineada al comienzo del rango, luego la porción alineada, luego la porción no alineada al final. Si se garantiza que todo está alineado, entonces el código se vuelve más simple y rápido, y más fácil para que el predictor de rama en su procesador sea correcto.

Optimización prematura?

std::copyEstá en una posición interesante. Espero que nunca sea más lento memcpyy a veces más rápido con cualquier compilador de optimización moderno. Además, todo lo que puedas memcpy, puedes std::copy. memcpyno permite ninguna superposición en las memorias intermedias, mientras que std::copyadmite la superposición en una dirección (con std::copy_backwardla otra dirección de superposición). memcpysolo funciona con punteros, std::copyfunciona en cualquier iteradores ( std::map, std::vector, std::deque, o mi propio tipo personalizado). En otras palabras, solo debes usar std::copycuando necesites copiar fragmentos de datos.

David Stone
fuente
35
Quiero enfatizar que esto no significa que std::copysea ​​2.99% o 0.72% o -0.11% más rápido que memcpy, estos tiempos son para que se ejecute todo el programa. Sin embargo, generalmente siento que los puntos de referencia en código real son más útiles que los puntos de referencia en código falso. Todo mi programa consiguió ese cambio en la velocidad de ejecución. Los efectos reales de solo los dos esquemas de copia tendrán mayores diferencias que las que se muestran aquí cuando se toman de forma aislada, pero esto muestra que pueden tener diferencias medibles en el código real.
David Stone
2
Quiero estar en desacuerdo con sus hallazgos, pero los resultados son resultados: /. Sin embargo, una pregunta (sé que fue hace mucho tiempo y no recuerdas la investigación, así que solo comenta cómo piensas), probablemente no investigaste el código de ensamblaje;
ST3
2
En mi opinión, memcpyy std::copytiene diferentes implementaciones, por lo que en algunos casos el compilador optimiza el código circundante y el código de copia de memoria real como una pieza integral de código. En otras palabras, a veces uno es mejor que otro e incluso en otras palabras, decidir cuál usar es una optimización prematura o incluso estúpida, porque en cada situación tienes que hacer una nueva investigación y, lo que es más, los programas generalmente se están desarrollando, así que después algunos cambios menores pueden perderse la ventaja de la función sobre otros.
ST3
3
@ ST3: Me imagino que, en el peor de los casos, std::copyes una función en línea trivial que solo llama memcpycuando es legal. La alineación básica eliminaría cualquier diferencia de rendimiento negativa. Actualizaré la publicación con una pequeña explicación de por qué std :: copy podría ser más rápido.
David Stone
77
Análisis muy informativo. Re Disminución promedio total en la velocidad de std :: copy sobre memcpy: 0.11% , aunque el número es correcto, los resultados no son estadísticamente significativos. Un intervalo de confianza del 95% para la diferencia de medias es (-0.013s, 0.025), que incluye cero. Como señaló, hubo variaciones de otras fuentes y con sus datos, probablemente diría que el rendimiento es el mismo. Como referencia, los otros dos resultados son estadísticamente significativos: las posibilidades de que vea una diferencia en tiempos tan extremos por casualidad son de aproximadamente 1 en 100 millones (primero) y 1 en 20,000 (último).
TooTone
78

Todos los compiladores que conozco reemplazarán un simple std::copycon a memcpycuando sea apropiado, o incluso mejor, vectoricen la copia para que sea aún más rápido que a memcpy.

En cualquier caso: perfil y descúbrelo tú mismo. Diferentes compiladores harán diferentes cosas, y es muy posible que no haga exactamente lo que pides.

Vea esta presentación sobre optimizaciones del compilador (pdf).

Esto es lo que hace GCC para un simple std::copytipo de POD.

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

Aquí está el desmontaje (con solo -Ooptimización), que muestra la llamada a memmove:

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

Si cambia la firma de la función a

void bar(foo* __restrict a, foo* __restrict b, size_t n)

entonces se memmoveconvierte memcpyen una mejora leve en el rendimiento. Tenga en cuenta que memcpysí será fuertemente vectorizado.

Peter Alexander
fuente
1
¿Cómo puedo hacer perfiles? ¿Qué herramienta usar (en Windows y Linux)?
user576670
55
@ Konrad, tienes razón. Pero memmoveno debería ser más rápido; más bien, debería ser un poco más lento porque debe tener en cuenta la posibilidad de que los dos rangos de datos se superpongan. Creo que std::copypermite la superposición de datos, por lo que tiene que llamar memmove.
Charles Salvia
2
@ Konrad: Si memmove siempre fue más rápido que memcpy, entonces memcpy llamaría a memmove. Lo que std :: copy realmente podría enviar (si corresponde) está definido por la implementación, por lo que no es útil mencionar detalles sin mencionar la implementación.
Fred Nurk
1
Aunque, un programa simple para reproducir este comportamiento, compilado con -O3 bajo GCC me muestra a memcpy. Me lleva a creer que GCC verifica si hay superposición de memoria.
jweyrich
1
@Konrad: el estándar std::copypermite la superposición en una dirección pero no en la otra. El comienzo de la salida no puede estar dentro del rango de entrada, pero el comienzo de la entrada puede estar dentro del rango de salida. Esto es un poco extraño, porque el orden de las asignaciones está definido, y una llamada puede ser UB aunque el efecto de esas asignaciones, en ese orden, esté definido. Pero supongo que la restricción permite optimizaciones de vectorización.
Steve Jessop
24

Siempre use std::copyporque memcpyestá limitado solo a estructuras POD de estilo C, y el compilador probablemente reemplazará las llamadas std::copycon memcpysi los objetivos son de hecho POD.

Además, std::copyse puede usar con muchos tipos de iteradores, no solo punteros. std::copyes más flexible para no perder rendimiento y es el claro ganador.

Perrito
fuente
¿Por qué deberías copiar alrededor de iteradores?
Atmocreations
3
No está copiando los iteradores, sino el rango definido por dos iteradores. Por ejemplo, std::copy(container.begin(), container.end(), destination);copiará el contenido de container(todo entre beginy end) en el búfer indicado por destination. std::copyno requiere travesuras como &*container.begin()o &container.back() + 1.
David Stone
16

En teoría, memcpypodría tener una ligera , imperceptible , infinitesimal , ventaja de rendimiento, solo porque no tiene los mismos requisitos que std::copy. De la página del manual de memcpy:

Para evitar desbordamientos, el tamaño de las matrices señaladas por los parámetros de destino y de origen debe ser al menos num bytes y no debe superponerse (para superponer bloques de memoria, memmove es un enfoque más seguro).

En otras palabras, memcpypuede ignorar la posibilidad de superposición de datos. (Pasar matrices superpuestas a memcpyes un comportamiento indefinido). Por memcpylo tanto, no es necesario verificar explícitamente esta condición, mientras que std::copypuede usarse siempre que el OutputIteratorparámetro no esté en el rango de origen. Tenga en cuenta que esto no es lo mismo que decir que el rango de origen y el rango de destino no pueden superponerse.

Entonces, dado que std::copytiene requisitos algo diferentes, en teoría debería ser un poco (con un énfasis extremo en un poco ) más lento, ya que probablemente comprobará la superposición de los arreglos en C o delegará la copia de los arreglos en C memmove, que debe realizar el cheque. Pero en la práctica, usted (y la mayoría de los perfiladores) probablemente ni siquiera detectarán ninguna diferencia.

Por supuesto, si no está trabajando con POD , no puede usarlo de memcpytodos modos.

Charles Salvia
fuente
77
Esto es cierto para std::copy<char>. Pero std::copy<int>puede suponer que sus entradas están alineadas int. Eso hará una diferencia mucho mayor, porque afecta a cada elemento. La superposición es una verificación única.
MSalters
2
@MSalters, cierto, pero la mayoría de las implementaciones de memcpyHe visto comprueban la alineación e intentan copiar palabras en lugar de byte a byte.
Charles Salvia
1
std :: copy () también puede ignorar la memoria superpuesta. Si desea admitir memoria superpuesta, debe escribir la lógica usted mismo para llamar a std :: reverse_copy () en las situaciones apropiadas.
Cygon
2
Hay un argumento opuesto que se puede hacer: al pasar por la memcpyinterfaz pierde la información de alineación. Por lo tanto, memcpytiene que hacer comprobaciones de alineación en tiempo de ejecución para manejar comienzos y finales no alineados. Esos cheques pueden ser baratos pero no son gratuitos. Mientras que std::copypuede evitar estos controles y vectorizar. Además, el compilador puede probar que las matrices de origen y destino no se superponen y de nuevo se vectorizan sin que el usuario tenga que elegir entre memcpyy memmove.
Maxim Egorushkin
11

Mi regla es simple. Si está utilizando C ++, prefiera las bibliotecas de C ++ y no C :)

UmmaGumma
fuente
40
C ++ fue diseñado explícitamente para permitir el uso de bibliotecas C. Esto no fue un accidente. A menudo es mejor usar std :: copy que memcpy en C ++, pero esto no tiene nada que ver con cuál es C, y ese tipo de argumento suele ser el enfoque incorrecto.
Fred Nurk
2
@FredNurk Por lo general, desea evitar el área débil de C donde C ++ proporciona una alternativa más segura.
Phil1970
@ Phil1970 No estoy seguro de que C ++ sea mucho más seguro en este caso. Todavía tenemos que pasar iteradores válidos que no se sobrepasen, etc. ¿ Supongo que poder usarlos en std::end(c_arr)lugar de hacerlo c_arr + i_hope_this_is_the_right_number_of elementses más seguro? y quizás lo más importante, más claro. Y eso sería el punto subrayo en este caso concreto: std::copy()es más idiomática, más fácil de mantener si los tipos de los iteradores cambios más adelante, conduce a la sintaxis más clara, etc
underscore_d
1
@underscore_d std::copyes más seguro porque copia correctamente los datos pasados ​​en caso de que no sean de tipo POD. memcpyfelizmente copiará un std::stringobjeto a una nueva representación byte a byte.
Jens
3

Solo una pequeña adición: la diferencia de velocidad entre memcpy()y std::copy()puede variar bastante dependiendo de si las optimizaciones están habilitadas o deshabilitadas. Con g ++ 6.2.0 y sin optimizaciones memcpy()gana claramente:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

Cuando las optimizaciones están habilitadas ( -O3), todo se ve casi igual de nuevo:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

Cuanto más grande es la matriz, menos perceptible es el efecto, pero incluso N=1000 memcpy()es aproximadamente el doble de rápido cuando las optimizaciones no están habilitadas.

Código fuente (requiere Google Benchmark):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */
Grumbel
fuente
18
Medir el rendimiento con optimizaciones deshabilitadas es ... bueno ... bastante inútil ... Si está interesado en el rendimiento, no compilará sin optimizaciones.
bolov
3
@bolov No siempre. En algunos casos es importante tener un programa relativamente rápido bajo depuración.
Bellota
2

Si realmente necesita el máximo rendimiento de copia (que quizás no necesite), no use ninguno de ellos .

Hay una gran cantidad que se puede hacer para optimizar la copia de memoria - aún más si usted está dispuesto a utilizar múltiples hilos / núcleos para ello. Ver, por ejemplo:

¿Qué falta / subóptimo en esta implementación de memcpy?

Tanto la pregunta como algunas de las respuestas han sugerido implementaciones o enlaces a implementaciones.

einpoklum
fuente
44
modo pedante: con la advertencia habitual de que " no usar ninguno de los dos " significa si ha demostrado que tiene una situación / requisito muy específico para el cual ninguna de las funciones estándar proporcionadas por su implementación es lo suficientemente rápida ; de lo contrario, mi preocupación habitual es que las personas que no han demostrado que se desvíen de la optimización prematura del código de copia en lugar de las partes generalmente más útiles de su programa.
underscore_d
-2

La creación de perfiles muestra esa declaración: std::copy()siempre es tan rápido memcpy()o más rápido es falso.

Mi sistema:

HP-Compaq-dx7500-Microtower 3.13.0-24-generic # 47-Ubuntu SMP viernes 2 de mayo 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

El código (lenguaje: c ++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

memcpy () perfil: main: 21: ahora: 1422969084: 04859 transcurrido: 2650 us
std :: copy () perfil: main: 27: ahora: 1422969084: 04862 transcurrido: 2745 us
memcpy () transcurrido 44 s std :: copy ( ) transcurrieron 45 s

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

memcpy () perfil: main: 21: ahora: 1422969601: 04939 transcurrido: 2385 us
std :: copy () perfil: main: 28: ahora: 1422969601: 04941 transcurrido: 2690 us
memcpy () transcurrido 27 s std :: copy ( ) transcurrieron 43 s

Red Alert señaló que el código usa memcpy de matriz a matriz y std :: copy de matriz a vector. Esa podría ser una razón para una memoria más rápida.

Ya que hay

v.reserve (sizeof (arr1));

no habrá diferencia en la copia al vector o matriz.

El código está arreglado para usar una matriz en ambos casos. memcpy aún más rápido:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 
imatveev13
fuente
1
mal, su perfil muestra que copiar en una matriz es más rápido que copiar en un vector. Sin relación.
Alerta roja
Podría estar equivocado, pero en su ejemplo corregido, con memcpy, ¿no está copiando arr2 en arr1, mientras que con std :: copy, está copiando arr1 en arr2? ... Lo que podría hacer es hacer múltiples, alternando experimentos (una vez un lote de memcpy, una vez un lote de std :: copy, luego otra vez con memcopy, etc., varias veces). Entonces, usaría clock () en lugar de time (), porque quién sabe qué podría estar haciendo tu PC además de ese programa. Sin embargo, solo mis dos centavos ... :-)
paercebal
77
Entonces, ¿cambiar std::copyde un vector a una matriz de alguna manera hizo memcpyque casi el doble de tiempo? Estos datos son altamente sospechosos. Compilé su código usando gcc con -O3, y el ensamblado generado es el mismo para ambos bucles. Por lo tanto, cualquier diferencia en el tiempo que observe en su máquina es solo incidental.
Alerta roja