2010-06-03 22 views
34

Tengo una función que está haciendo memcpy, pero está tomando una enorme cantidad de ciclos. ¿Existe una alternativa/enfoque más rápido que el uso de memcpy para mover un trozo de memoria?alternativa más rápida a memcpy?

+1

Respuesta corta: Tal vez, es posible. Ofrezca más detalles como arquitectura, plataforma y otros. En el mundo integrado es muy probable reescribir algunas funciones de libc que no funcionan tan bien. – INS

Respuesta

111

memcpy es probable que sea la forma más rápida de copiar bytes en la memoria. Si necesita algo más rápido, intente averiguar cómo no copiando elementos, p. solo intercambia punteros, no los datos en sí.

+2

+1, Recientemente tuvimos un problema cuando algunos de nuestros códigos RECIENTEMENTE se ralentizaron tremendamente y consumieron mucha memoria extra cuando procesamos un determinado archivo.Resultó que el archivo tenía un gran bloque de metadatos, mientras que otras moscas no tenían metadatos o bloques pequeños. Y esos metadatos fueron copiados, copiados, copiados, consumiendo tiempo y memoria. Reemplazó la copia con pass-by-const-reference. – sharptooth

+6

Es una buena pregunta sobre memcpy más rápido, pero esta respuesta proporciona una solución, no una respuesta. P.ej. http://software.intel.com/en-us/articles/memcpy-performance/ explica algunas razones bastante serias por las cuales memcpy a menudo es mucho menos eficiente de lo que podría ser. –

+0

¿Podría ser posible utilizar una técnica de Copiar al escribir, ya sea en el nivel bajo o deliberadamente en el código? ¿Necesitaría trozos de memoria de tamaños similares a múltiplos enteros de páginas? Luego, deje los dos punteros apuntando en la vida real a la misma memoria y deje que el administrador de memoria haga copias de las páginas como lo necesite cuando se modifiquen los datos. –

6

Por lo general, la biblioteca estándar incluida con el compilador implementará memcpy() de la forma más rápida posible para la plataforma de destino.

3

En general es más rápido no hacer una copia. Si puede adaptar su función para no copiar, no lo sé, pero vale la pena buscarlo.

3

A veces funciona como establecimiento de memoria, memset, ... son implementados de dos maneras diferentes:

  • vez como una función real
  • vez como un poco de montaje que está inmediatamente inlined
No todos

los compiladores toman la versión de montaje en línea de forma predeterminada, su compilador puede usar la variante de función de manera predeterminada, lo que ocasiona cierta sobrecarga debido a la llamada a la función. Compruebe su compilador para ver cómo tomar la variante intrínseca de la función (opción de línea de comando, pragma, ...).

Editar: Consulte http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx para obtener una explicación de los intrínsecos en el compilador de Microsoft C.

0

Supongo que debe tener grandes áreas de memoria que desea copiar, si el rendimiento de memcpy se ha convertido en un problema para usted?

En este caso, estaría de acuerdo con la sugerencia de los números a buscar la manera de no copiar cosas ..

lugar de tener una enorme burbuja de la memoria a copiar todo cada vez que necesite cambiarlo, probablemente debería probar algunas estructuras de datos alternativas.

Sin realmente saber nada sobre su área problemática, le sugiero que eche un vistazo a persistent data structures y que implemente uno propio o reutilice una implementación existente.

2

Verifique el manual del compilador/plataforma. Para algunos microprocesadores y kits DSP que usan memcpy es mucho más lento que las operaciones intrinsic functions o DMA.

2

Si su plataforma lo admite, investigue si puede usar la llamada al sistema mmap() para dejar sus datos en el archivo ... generalmente el sistema operativo puede manejarlo mejor. Y, como todos han estado diciendo, evite copiar si es posible; los punteros son su amigo en casos como este.

10

Por favor, nos ofrece más detalles. En la arquitectura i386, es muy posible que memcpy sea la manera más rápida de copiar. Pero en una arquitectura diferente para la que el compilador no tiene una versión optimizada, es mejor que reescriba su función memcpy. Hice esto en una arquitectura ARM personalizada usando lenguaje ensamblador. Si transfiere GRANDES trozos de memoria, entonces DMA es probablemente la respuesta que está buscando.

Ofrezca más detalles - arquitectura, sistema operativo (si corresponde).

+1

Para ARM, la libc impl ahora es más rápida que lo que podrá crear usted mismo. Para copias pequeñas (cualquier cosa menos una página) puede ser más rápido usar un bucle ASM dentro de sus funciones. Pero, para copias grandes, no podrá superar el libc impl, porque los procesadores diff tienen rutas de código "óptimas" ligeramente diferentes. Por ejemplo, un Cortex8 funciona mejor con las instrucciones de copia de NEON, pero un Cortex9 es más rápido con las instrucciones de ARM ldm/stm. No puede escribir una pieza de código que sea rápida para ambos procesadores, pero puede llamar a memcpy para buffers grandes. – MoDJ

+0

@MoDJ: Desearía que la biblioteca C estándar incluyera algunas variantes memcpy diferentes con semántica generalmente idéntica en los casos en que todos arrojaran un comportamiento definido, pero diferentes casos optimizados y, en algunos casos, restricciones para el uso alineado o alineado. Si el código normalmente necesita copiar pequeñas cantidades de bytes o palabras conocidas para alinearse, una implementación ingenua de carácter a la vez podría hacer el trabajo en menos tiempo de lo que requerirían algunas implementaciones de memcpy() más sofisticadas para decidir sobre un curso de acción. – supercat

0

nos está bien, lo está llamando demasiado.

Para ver de dónde lo está llamando y por qué, simplemente deténgalo un par de veces debajo del depurador y mire la pila.

0

memoria a la memoria se admite generalmente en conjunto de comandos de la CPU, y el establecimiento de memoria suele usar eso. Y esta suele ser la forma más rápida.

Debería comprobar qué está haciendo exactamente su CPU. En Linux, observe la eficacia de la memoria virtual y del swapi con sar -B 1 o vmstat 1 o mirando en/proc/memstat. Puede ver que su copia tiene que sacar muchas páginas para liberar espacio, o leerlas, etc.

Eso significa que su problema no está en lo que usa para la copia, sino en cómo usa el sistema memoria. Es posible que necesite disminuir la caché de archivos o comenzar a escribir antes, o bloquear las páginas en la memoria, etc.

6

En realidad, memcpy NO es la manera más rápida, especialmente si la llama muchas veces. También tenía un código que realmente necesitaba acelerar, y memcpy es lento porque tiene demasiadas verificaciones innecesarias. Por ejemplo, comprueba si los bloques de destino y de memoria de origen se superponen y si debería comenzar a copiarse desde la parte posterior del bloque en lugar de desde el frente. Si no le importan estas consideraciones, definitivamente puede hacerlo mucho mejor. Tengo un código, pero aquí está quizás una versión cada vez mejor:

Very fast memcpy for image processing?.

Si busca, puede encontrar otras implementaciones también. Pero para una verdadera velocidad, necesitas una versión de ensamblaje.

+0

Probé un código similar a este usando sse2. Resulta que fue más lento en mi sistema amd por un factor de 4x que el built-in. Siempre es mejor no copiar si puedes evitarlo. – Matt

+0

Aunque 'memmove' debe verificar y manejar la superposición,' memcpy' no está obligado a hacerlo. El problema más grande es que para ser eficiente al copiar bloques grandes, las implementaciones de 'memcpy' necesitan seleccionar un enfoque de copia antes de que puedan comenzar a funcionar. Si el código necesita poder copiar un número arbitrario de bytes, ese número será un 90% del tiempo, dos el 9% del tiempo, tres el 0.9% del tiempo, etc. y los valores de 'count', 'dest', y' src' no serán necesarios después, luego un 'if (count) alineado for * dest + = * src; while (- count> 0); 'podría ser mejor que la rutina" más inteligente ". – supercat

+0

BTW, en algunos sistemas incorporados, otro motivo por el que 'memcpy' puede no ser el enfoque más rápido es que un controlador DMA algunas veces puede copiar un bloque de memoria con menos sobrecarga que la CPU, pero la forma más eficiente de hacerlo es copiar podría ser iniciar el DMA y luego hacer otro procesamiento mientras el DMA se está ejecutando. En un sistema con códigos de entrada y buses de datos por separado, es posible configurar el DMA para que copie datos en cada ciclo cuando la CPU no necesita el bus de datos para nada más. Esto puede lograr un rendimiento mucho mejor que usar la CPU para la copia, usando ... – supercat

1

Debe verificar el código de ensamblaje generado para su código. Lo que no desea es hacer que la llamada memcpy genere una llamada a la función memcpy en la biblioteca estándar; lo que quiere es tener una llamada repetida a la mejor instrucción ASM para copiar la mayor cantidad de datos, algo así como rep movsq.

¿Cómo se puede lograr esto? Bueno, el compilador optimiza las llamadas al memcpy reemplazándolas por simples mov s, siempre que sepa la cantidad de datos que debe copiar. Puede ver esto si escribe un memcpy con un valor bien determinado (constexpr). Si el compilador no conoce el valor, tendrá que recurrir a la implementación de nivel de bytes de memcpy; el problema es que memcpy tiene que respetar la granularidad de un byte. Todavía moverá 128 bits a la vez, pero después de cada 128b tendrá que verificar si tiene suficientes datos para copiar como 128b o tiene que volver a 64bits, luego a 32 y 8 (creo que 16 podría ser un valor inferior al óptimo) de todos modos, pero no estoy seguro).

Así que lo que quiere es saber memcpy cuál es el tamaño de sus datos con expresiones const que el compilador puede optimizar.De esta forma, no se realiza ninguna llamada al memcpy. Lo que no desea es pasar a memcpy una variable que solo se conocerá en tiempo de ejecución. Eso se traduce en una llamada a función y toneladas de pruebas para verificar la mejor instrucción de copia. A veces, un bucle for simple es mejor que memcpy por este motivo (eliminando una llamada de función). Y lo que realmente no desea es pasar a memcpy un número impar de bytes para copiar.

6

Esta es una respuesta para x86_64 con el conjunto de instrucciones AVX2 presente. Aunque algo similar puede aplicarse para ARM/AArch64 con SIMD.

En Ryzen 1800X con canal de memoria único lleno (2 ranuras, 16 GB DDR4 en cada uno), el siguiente código es 1.56 veces más rápido que memcpy() en el compilador MSVC++ 2017. Si llena ambos canales de memoria con 2 módulos DDR4, es decir, tiene las 4 ranuras DDR4 ocupadas, es posible que obtenga 2 copias de memoria más rápidas. Para sistemas de memoria de triple (cuádruple) canal, puede obtener 1.5 (2.0) veces más tiempo de copia de memoria si el código se extiende al código AVX512 análogo. Con los sistemas de canal triple/cuádruple AVX2 con todas las ranuras ocupadas no se espera que sean más rápidos porque para cargarlos completamente necesita cargar/almacenar más de 32 bytes a la vez (48 bytes para triple y 64 bytes para cuádruple canal sistemas), mientras que AVX2 puede cargar/almacenar no más de 32 bytes a la vez. Aunque el subprocesamiento múltiple en algunos sistemas puede aliviar esto sin AVX512 o incluso AVX2.

Así que aquí está el código de copia que asume que está copiando un gran bloque de memoria cuyo tamaño es un múltiplo de 32 y el bloque está alineado con 32 bytes.

Para bloques de tamaño no múltiple y bloques no alineados, se puede escribir el código de prólogo/epílogo reduciendo el ancho a 16 (SSE4.1), 8, 4, 2 y finalmente 1 byte para la cabeza y cola del bloque . También en el medio se puede usar una matriz local de 2-3 __m256i como un proxy entre las lecturas alineadas de la fuente y las escrituras alineadas en el destino.

#include <immintrin.h> 
#include <cstdint> 
/* ... */ 
void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) { 
    assert(nBytes % 32 == 0); 
    assert((intptr_t(pvDest) & 31) == 0); 
    assert((intptr_t(pvSrc) & 31) == 0); 
    const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc); 
    __m256i *pDest = reinterpret_cast<__m256i*>(pvDest); 
    int64_t nVects = nBytes/sizeof(*pSrc); 
    for (; nVects > 0; nVects--, pSrc++, pDest++) { 
    const __m256i loaded = _mm256_stream_load_si256(pSrc); 
    _mm256_stream_si256(pDest, loaded); 
    } 
    _mm_sfence(); 
} 

Una característica clave de este código es que se salta caché de la CPU cuando se copia: cuando se trata de caché de la CPU (es decir, se utilizan las instrucciones AVX sin _stream_), la velocidad de copia cae varias veces en mi sistema.

Mi memoria DDR4 es CL13 de 2.6GHz. Así que cuando se copia 8 GB de datos de una matriz a otra Tengo las siguientes velocidades:

memcpy(): 17 208 004 271 bytes/sec. 
Stream copy: 26 842 874 528 bytes/sec. 

Tenga en cuenta que en estas mediciones, el tamaño total de los dos buffers de entrada y de salida se divide por el número de segundos transcurridos. Porque para cada byte de la matriz hay 2 accesos de memoria: uno para leer el byte de la matriz de entrada, y otro para escribir el byte en la matriz de salida. En otras palabras, cuando se copian 8 GB de una matriz a otra, se realizan 16GB de operaciones de acceso a la memoria.

Moderada multihilo puede mejorar aún más el rendimiento alrededor de 1,44 veces, por lo que el aumento total de memcpy() llega a 2,55 veces en mi máquina. Así es como rendimiento de la copia corriente depende del número de hilos utilizados en mi máquina:

Stream copy 1 threads: 27114820909.821 bytes/sec 
Stream copy 2 threads: 37093291383.193 bytes/sec 
Stream copy 3 threads: 39133652655.437 bytes/sec 
Stream copy 4 threads: 39087442742.603 bytes/sec 
Stream copy 5 threads: 39184708231.360 bytes/sec 
Stream copy 6 threads: 38294071248.022 bytes/sec 
Stream copy 7 threads: 38015877356.925 bytes/sec 
Stream copy 8 threads: 38049387471.070 bytes/sec 
Stream copy 9 threads: 38044753158.979 bytes/sec 
Stream copy 10 threads: 37261031309.915 bytes/sec 
Stream copy 11 threads: 35868511432.914 bytes/sec 
Stream copy 12 threads: 36124795895.452 bytes/sec 
Stream copy 13 threads: 36321153287.851 bytes/sec 
Stream copy 14 threads: 36211294266.431 bytes/sec 
Stream copy 15 threads: 35032645421.251 bytes/sec 
Stream copy 16 threads: 33590712593.876 bytes/sec 

El código es:

void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) { 
    for (; nVects > 0; nVects--, pSrc++, pDest++) { 
    const __m256i loaded = _mm256_stream_load_si256(pSrc); 
    _mm256_stream_si256(pDest, loaded); 
    } 
} 

void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) { 
    assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0); 
    const uint32_t maxThreads = std::thread::hardware_concurrency(); 
    std::vector<std::thread> thrs; 
    thrs.reserve(maxThreads + 1); 

    const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput); 
    __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput); 
    const int64_t nVects = cnDoubles * sizeof(*gpdInput)/sizeof(*pSrc); 

    for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) { 
    auto start = std::chrono::high_resolution_clock::now(); 
    lldiv_t perWorker = div((long long)nVects, (long long)nThreads); 
    int64_t nextStart = 0; 
    for (uint32_t i = 0; i < nThreads; i++) { 
     const int64_t curStart = nextStart; 
     nextStart += perWorker.quot; 
     if ((long long)i < perWorker.rem) { 
     nextStart++; 
     } 
     thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart); 
    } 
    for (uint32_t i = 0; i < nThreads; i++) { 
     thrs[i].join(); 
    } 
    _mm_sfence(); 
    auto elapsed = std::chrono::high_resolution_clock::now() - start; 
    double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count(); 
    printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double)/nSec); 

    thrs.clear(); 
    } 
} 
Cuestiones relacionadas