c visual-studio memcpy cvi memory-bandwidth

memcpy en c



Cómo aumentar el rendimiento de memcpy (8)

Resumen:

memcpy parece incapaz de transferir más de 2GB / seg en mi sistema en una aplicación real o de prueba. ¿Qué puedo hacer para obtener copias más rápidas de memoria a memoria?

Todos los detalles:

Como parte de una aplicación de captura de datos (usando hardware especializado), necesito copiar unos 3 GB / seg de memorias temporales en la memoria principal. Para adquirir datos, proporciono al controlador de hardware una serie de búferes (2 MB cada uno). El hardware DMAs de datos a cada buffer, y luego notifica a mi programa cuando cada buffer está lleno. Mi programa vacía el búfer (memcpy en otro bloque más grande de RAM) y vuelve a colocar el búfer procesado en la tarjeta para que se llene nuevamente. Estoy teniendo problemas con memcpy para mover los datos lo suficientemente rápido. Parece que la copia de memoria a memoria debe ser lo suficientemente rápida para soportar 3GB / seg en el hardware en el que estoy corriendo. Lavalys EVEREST me da un resultado de referencia de copia de memoria de 9337MB / seg, pero no puedo acercarme a esas velocidades con memcpy, incluso en un programa de prueba simple.

He aislado el problema de rendimiento agregando / eliminando la llamada memcpy dentro del código de procesamiento del búfer. Sin la memcpy, puedo ejecutar una velocidad de datos completa, aproximadamente 3GB / seg. Con la memcpy habilitada, estoy limitado a aproximadamente 550Mb / seg (usando el compilador actual).

Con el fin de comparar membcpy en mi sistema, he escrito un programa de prueba separado que solo llama a memcpy en algunos bloques de datos. (He publicado el código a continuación) He ejecutado esto tanto en el compilador / IDE que estoy usando (National Instruments CVI) como en Visual Studio 2010. Aunque actualmente no estoy usando Visual Studio, estoy dispuesto para hacer el cambio si dará el rendimiento necesario. Sin embargo, antes de mudarme ciegamente, quería asegurarme de que resolvería mis problemas de rendimiento memcpy.

Visual C ++ 2010: 1900 MB / seg

NI CVI 2009: 550 MB / sec

Aunque no me sorprende que CVI sea significativamente más lento que Visual Studio, me sorprende que el rendimiento de memcpy sea tan bajo. Si bien no estoy seguro de si esto es directamente comparable, esto es mucho menor que el ancho de banda de referencia EVEREST. Si bien no necesito ese nivel de rendimiento, es necesario un mínimo de 3 GB / seg. ¡Seguramente la implementación de la biblioteca estándar no puede ser mucho peor que lo que sea que EVEREST esté usando!

¿Qué puedo hacer, si hay algo, para hacer que memcpy sea más rápido en esta situación?

Detalles del hardware: AMD Magny Cours- núcleo octal 4x 128 GB DDR3 Windows Server 2003 Enterprise X64

Programa de prueba:

#include <windows.h> #include <stdio.h> const size_t NUM_ELEMENTS = 2*1024 * 1024; const size_t ITERATIONS = 10000; int main (int argc, char *argv[]) { LARGE_INTEGER start, stop, frequency; QueryPerformanceFrequency(&frequency); unsigned short * src = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS); unsigned short * dest = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS); for(int ctr = 0; ctr < NUM_ELEMENTS; ctr++) { src[ctr] = rand(); } QueryPerformanceCounter(&start); for(int iter = 0; iter < ITERATIONS; iter++) memcpy(dest, src, NUM_ELEMENTS * sizeof(unsigned short)); QueryPerformanceCounter(&stop); __int64 duration = stop.QuadPart - start.QuadPart; double duration_d = (double)duration / (double) frequency.QuadPart; double bytes_sec = (ITERATIONS * (NUM_ELEMENTS/1024/1024) * sizeof(unsigned short)) / duration_d; printf("Duration: %.5lfs for %d iterations, %.3lfMB/sec/n", duration_d, ITERATIONS, bytes_sec); free(src); free(dest); getchar(); return 0; }

EDITAR: Si tiene cinco minutos adicionales y desea contribuir, ¿puede ejecutar el código anterior en su máquina y publicar su tiempo como un comentario?


¿Quizás pueda explicar algo más sobre cómo está procesando el área de memoria más grande?

¿Sería posible dentro de su aplicación simplemente pasar la propiedad del búfer, en lugar de copiarlo? Esto eliminaría el problema por completo.

¿O estás utilizando memcpy para algo más que copiar? ¿Tal vez está usando el área más grande de la memoria para construir una secuencia de datos a partir de lo que ha capturado? Especialmente si está procesando un personaje a la vez, es posible que pueda reunirse a mitad de camino. Por ejemplo, puede ser posible adaptar su código de procesamiento para adaptarse a una secuencia representada como ''una matriz de almacenamientos intermedios'', en lugar de ''un área de memoria continua''.


En primer lugar, debe verificar que la memoria esté alineada en el límite de 16 bytes; de lo contrario, recibirá penalizaciones. Esta es la cosa más importante.

Si no necesita una solución que cumpla con los estándares, puede verificar si las cosas mejoran utilizando alguna extensión específica del compilador como memcpy64 (consulte con su compilador si hay algo disponible). El hecho es que memcpy debe poder tratar con la copia de un solo byte, pero mover 4 u 8 bytes a la vez es mucho más rápido si no tiene esta restricción.

Nuevamente, ¿es una opción para usted escribir el código ensamblador en línea?


He encontrado una manera de aumentar la velocidad en esta situación. Escribí una versión multiproceso de memcpy, dividiendo el área para copiar entre hilos. Aquí hay algunos números de escala de rendimiento para un tamaño de bloque establecido, utilizando el mismo código de tiempo que se encuentra arriba. No tenía idea de que el rendimiento, especialmente para este pequeño tamaño de bloque, se ajustaría a esta cantidad de hilos. Sospecho que esto tiene algo que ver con la gran cantidad de controladores de memoria (16) en esta máquina.

Performance (10000x 4MB block memcpy): 1 thread : 1826 MB/sec 2 threads: 3118 MB/sec 3 threads: 4121 MB/sec 4 threads: 10020 MB/sec 5 threads: 12848 MB/sec 6 threads: 14340 MB/sec 8 threads: 17892 MB/sec 10 threads: 21781 MB/sec 12 threads: 25721 MB/sec 14 threads: 25318 MB/sec 16 threads: 19965 MB/sec 24 threads: 13158 MB/sec 32 threads: 12497 MB/sec

No entiendo el gran salto de rendimiento entre 3 y 4 hilos. ¿Qué causaría un salto como este?

He incluido el código memcpy que escribí a continuación para otros que pueden encontrarse con este mismo problema. Tenga en cuenta que no hay errores al verificar este código; es posible que deba agregarlo a su aplicación.

#define NUM_CPY_THREADS 4 HANDLE hCopyThreads[NUM_CPY_THREADS] = {0}; HANDLE hCopyStartSemaphores[NUM_CPY_THREADS] = {0}; HANDLE hCopyStopSemaphores[NUM_CPY_THREADS] = {0}; typedef struct { int ct; void * src, * dest; size_t size; } mt_cpy_t; mt_cpy_t mtParamters[NUM_CPY_THREADS] = {0}; DWORD WINAPI thread_copy_proc(LPVOID param) { mt_cpy_t * p = (mt_cpy_t * ) param; while(1) { WaitForSingleObject(hCopyStartSemaphores[p->ct], INFINITE); memcpy(p->dest, p->src, p->size); ReleaseSemaphore(hCopyStopSemaphores[p->ct], 1, NULL); } return 0; } int startCopyThreads() { for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++) { hCopyStartSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL); hCopyStopSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL); mtParamters[ctr].ct = ctr; hCopyThreads[ctr] = CreateThread(0, 0, thread_copy_proc, &mtParamters[ctr], 0, NULL); } return 0; } void * mt_memcpy(void * dest, void * src, size_t bytes) { //set up parameters for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++) { mtParamters[ctr].dest = (char *) dest + ctr * bytes / NUM_CPY_THREADS; mtParamters[ctr].src = (char *) src + ctr * bytes / NUM_CPY_THREADS; mtParamters[ctr].size = (ctr + 1) * bytes / NUM_CPY_THREADS - ctr * bytes / NUM_CPY_THREADS; } //release semaphores to start computation for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++) ReleaseSemaphore(hCopyStartSemaphores[ctr], 1, NULL); //wait for all threads to finish WaitForMultipleObjects(NUM_CPY_THREADS, hCopyStopSemaphores, TRUE, INFINITE); return dest; } int stopCopyThreads() { for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++) { TerminateThread(hCopyThreads[ctr], 0); CloseHandle(hCopyStartSemaphores[ctr]); CloseHandle(hCopyStopSemaphores[ctr]); } return 0; }


No estoy seguro de si está hecho en tiempo de ejecución o si tiene que hacerlo en tiempo de compilación, pero debe tener SSE o extensiones similares habilitadas, ya que la unidad vectorial a menudo puede escribir 128 bits en la memoria en comparación con 64 bits para la CPU.

Prueba esta implementación .

Sí, y asegúrese de que tanto el origen como el destino estén alineados a 128 bits. Si su fuente y destino no están alineados el uno con el otro su memcpy () tendrá que hacer algo de magia seria. :)


Puede escribir una mejor implementación de memcpy usando registros SSE2. La versión en VC2010 ya lo hace. Entonces la pregunta es más, si le está entregando la memoria alineada.

Tal vez pueda hacerlo mejor que la versión de VC 2010, pero necesita cierta comprensión de cómo hacerlo.

PD: puede pasar el búfer al programa de modo de usuario en una llamada invertida, para evitar la copia por completo.


Tiene algunas barreras para obtener el rendimiento de memoria requerido:

  1. Ancho de banda: existe un límite en la rapidez con que los datos pueden pasar de la memoria a la CPU y viceversa. Según este artículo de Wikipedia , la RAM DDR3 a 266 MHz tiene un límite superior de alrededor de 17 GB / s. Ahora, con una memcpy necesita reducir a la mitad esto para obtener su tasa de transferencia máxima, ya que los datos se leen y luego se escriben. Según sus resultados de referencia, parece que no está ejecutando la RAM más rápida posible en su sistema. Si puede pagarlo, actualice la placa base / RAM (y no será barato, los Overclockers en el Reino Unido actualmente tienen 3x4GB PC16000 a £ 400)

  2. El sistema operativo: Windows es un sistema operativo multitarea preventivo, por lo que de vez en cuando su proceso se suspenderá para permitir que otros procesos puedan ver y hacer cosas. Esto derrota tus cachés y atasca tu transferencia. ¡En el peor de los casos, todo su proceso podría almacenarse en caché en el disco!

  3. La CPU: los datos que se están moviendo tienen un largo camino por recorrer: RAM -> Caché L2 -> Caché L1 -> CPU -> L1 -> L2 -> RAM. Incluso puede haber un caché L3. Si desea involucrar a la CPU, realmente desea cargar L2 mientras copia L1. Desafortunadamente, las CPU modernas pueden ejecutarse a través de un bloque de caché L1 más rápido que el tiempo necesario para cargar el L1. La CPU tiene un controlador de memoria que ayuda mucho en estos casos en los que la transmisión de datos a la CPU se realiza de forma secuencial, pero aún así tendrá problemas.

Por supuesto, la forma más rápida de hacer algo es no hacerlo. ¿Pueden los datos capturados escribirse en cualquier lugar de la RAM o el búfer utilizado en una ubicación fija? Si puedes escribirlo en cualquier lugar, entonces no necesitas el memcpy en absoluto. Si se soluciona, ¿podría procesar los datos en su lugar y usar un sistema de doble buffer? Es decir, comience a capturar datos y cuando esté medio lleno, comience a procesar la primera mitad de los datos. Cuando el búfer está lleno, comience a escribir los datos capturados al inicio y procese la segunda mitad. Esto requiere que el algoritmo pueda procesar los datos más rápido de lo que la tarjeta de captura lo produce. También asume que los datos se descartan después del procesamiento. Efectivamente, esta es una memcpy con una transformación como parte del proceso de copia, por lo que tiene:

load -> transform -> save /--/ /--/ capture card RAM buffer

en lugar de:

load -> save -> load -> transform -> save /-----------/ memcpy from capture card buffer to RAM

¡O consigue RAM más rápido!

EDITAR: Otra opción es procesar los datos entre la fuente de datos y la PC. ¿Podría poner un DSP / FPGA allí? El hardware personalizado siempre será más rápido que una CPU de propósito general.

Otro pensamiento: Ha pasado un tiempo desde que hice algo de gráficos de alto rendimiento, pero ¿podría DMA los datos en la tarjeta gráfica y luego DMA de nuevo? Incluso podría aprovechar CUDA para hacer parte del procesamiento. Esto sacaría a la CPU del bucle de transferencia de memoria por completo.


Una cosa a tener en cuenta es que su proceso (y por lo tanto el rendimiento de memcpy() ) se ve afectado por la programación de tareas del sistema operativo. Es difícil decir cuánto de esto es un factor en sus tiempos, pero el control es difícil de controlar. . El funcionamiento del dispositivo DMA no está sujeto a esto, ya que no se ejecuta en la CPU una vez que se ha iniciado. Sin embargo, dado que su aplicación es una aplicación real en tiempo real, es posible que desee experimentar con la configuración de proceso / prioridad de subprocesos de Windows si no lo ha hecho aún. Solo tenga en cuenta que debe tener cuidado con esto porque puede tener un impacto realmente negativo en otros procesos (y la experiencia del usuario en la máquina).

Otra cosa a tener en cuenta es que la virtualización de memoria del sistema operativo puede tener un impacto aquí: si las páginas de memoria a las que está copiando no están realmente respaldadas por páginas RAM físicas, la operación memcpy() en el sistema operativo para obtener ese respaldo físico en su lugar. Es probable que sus páginas DMA estén bloqueadas en la memoria física (ya que tienen que ser para la operación DMA), por lo que es probable que la memoria fuente a memcpy() no sea un problema a este respecto. Podría considerar usar la API de Win32 VirtualAlloc() para asegurarse de que su memoria de destino para memcpy() está comprometida (creo que VirtualAlloc() es la API correcta para esto, pero podría haber una mejor que me estoy olvidando: es Ha pasado un tiempo desde que tuve la necesidad de hacer algo como esto).

Finalmente, fíjate si puedes usar la técnica explicada por Skizz para evitar memcpy() completo: esa es tu mejor opción si los recursos lo permiten.


Una fuente que recomendaría que fast_memcpy es la función fast_memcpy de MPlayer. También tenga en cuenta los patrones de uso esperados, y tenga en cuenta que las CPU modernas tienen instrucciones especiales de almacenamiento que le permiten informar a la CPU si necesita o no leer los datos que está escribiendo. El uso de las instrucciones que indican que no leerá los datos (y por lo tanto no necesita ser almacenado en la memcpy caché) puede ser una gran ganancia para las grandes operaciones memcpy .