utiliza que para array c std

para - ¿Manera más rápida de cero memoria que con memset?



para que se utiliza memset en c (9)

Aprendí que memset(ptr, 0, nbytes) es realmente rápido, pero ¿hay una manera más rápida (al menos en x86)?

Supongo que memset usa mov , sin embargo, al poner a cero la memoria, la mayoría de los compiladores usan xor ya que es más rápido, ¿correcto? edit1: Incorrecto, como GregS señaló que solo funciona con registros. ¿Qué estaba pensando?

También le pedí a una persona que conocía al ensamblador más que a mí que mirara el stdlib, y me dijo que en x86 memset no está aprovechando al máximo los registros de 32 bits de ancho. Sin embargo, en ese momento estaba muy cansado, así que no estoy seguro de haberlo entendido correctamente.

edit2 : volví a visitar este tema e hice algunas pruebas. Esto es lo que probé:

#include <stdio.h> #include <malloc.h> #include <string.h> #include <sys/time.h> #define TIME(body) do { / struct timeval t1, t2; double elapsed; / gettimeofday(&t1, NULL); / body / gettimeofday(&t2, NULL); / elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; / printf("%s/n --- %f ---/n", #body, elapsed); } while(0) / #define SIZE 0x1000000 void zero_1(void* buff, size_t size) { size_t i; char* foo = buff; for (i = 0; i < size; i++) foo[i] = 0; } /* I foolishly assume size_t has register width */ void zero_sizet(void* buff, size_t size) { size_t i; char* bar; size_t* foo = buff; for (i = 0; i < size / sizeof(size_t); i++) foo[i] = 0; // fixes bug pointed out by tristopia bar = (char*)buff + size - size % sizeof(size_t); for (i = 0; i < size % sizeof(size_t); i++) bar[i] = 0; } int main() { char* buffer = malloc(SIZE); TIME( memset(buffer, 0, SIZE); ); TIME( zero_1(buffer, SIZE); ); TIME( zero_sizet(buffer, SIZE); ); return 0; }

resultados:

zero_1 es el más lento, excepto por -O3. zero_sizet es el más rápido con un rendimiento aproximadamente igual en -O1, -O2 y -O3. memset siempre fue más lento que zero_sizet. (dos veces más lento para -O3). Una cosa interesante es que en -O3 zero_1 fue igual de rápido que zero_sizet. sin embargo, la función desensamblada tenía aproximadamente cuatro veces más instrucciones (creo que causadas por el desenrollado del lazo). Además, traté de optimizar zero_sizet aún más, pero el compilador siempre me superó, pero no es una sorpresa.

Por ahora, memset gana, los resultados anteriores fueron distorsionados por el caché de la CPU. (todas las pruebas se ejecutaron en Linux) Se necesitan más pruebas. Voy a intentar montador siguiente :)

edit3: se corrigió el error en el código de prueba, los resultados de la prueba no se vieron afectados

edit4: Mientras hurgaba en el tiempo de ejecución desmontado del VS2010 C, me di cuenta de que memset tenía una rutina de SSE optimizada para cero. Será difícil vencer esto.


A menos que tenga necesidades específicas o sepa que su compilador / stdlib es sucky, quédese con memset. Es de uso general, y debe tener un rendimiento decente en general. Además, los compiladores podrían tener un tiempo más fácil para optimizar / incluir memset () porque puede tener soporte intrínseco para él.

Por ejemplo, Visual C ++ a menudo generará versiones en línea de memcpy / memset que son tan pequeñas como una llamada a la función de la biblioteca, evitando así la sobrecarga push / call / ret. Y hay más optimizaciones posibles cuando el parámetro de tamaño se puede evaluar en tiempo de compilación.

Dicho eso, si tiene necesidades específicas (donde el tamaño siempre será pequeño * o * enorme ), puede aumentar la velocidad bajando al nivel de ensamblaje. Por ejemplo, usando operaciones de escritura para poner a cero grandes cantidades de memoria sin contaminar su caché L2.

Pero todo depende, y para cosas normales, por favor, quédate con memset / memcpy :)


Esa es una pregunta interesante. Hice esta implementación que es ligeramente más rápida (pero difícilmente medible) cuando se compila la versión de 32 bits en VC ++ 2012. Probablemente se puede mejorar mucho. Agregar esto a su propia clase en un entorno multiproceso probablemente le otorgue aún más aumentos de rendimiento, ya que hay algunos problemas de cuello de botella informados con memset() en escenarios multiproceso.

// MemsetSpeedTest.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> #include "Windows.h" #include <time.h> #pragma comment(lib, "Winmm.lib") using namespace std; /** a signed 64-bit integer value type */ #define _INT64 __int64 /** a signed 32-bit integer value type */ #define _INT32 __int32 /** a signed 16-bit integer value type */ #define _INT16 __int16 /** a signed 8-bit integer value type */ #define _INT8 __int8 /** an unsigned 64-bit integer value type */ #define _UINT64 unsigned _INT64 /** an unsigned 32-bit integer value type */ #define _UINT32 unsigned _INT32 /** an unsigned 16-bit integer value type */ #define _UINT16 unsigned _INT16 /** an unsigned 8-bit integer value type */ #define _UINT8 unsigned _INT8 /** maximum allo wed value in an unsigned 64-bit integer value type */ #define _UINT64_MAX 18446744073709551615ULL #ifdef _WIN32 /** Use to init the clock */ #define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency); /** Use to start the performance timer */ #define TIMER_START QueryPerformanceCounter(&t1); /** Use to stop the performance timer and output the result to the standard stream. Less verbose than /c TIMER_STOP_VERBOSE */ #define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl; #else /** Use to init the clock */ #define TIMER_INIT clock_t start;double diff; /** Use to start the performance timer */ #define TIMER_START start=clock(); /** Use to stop the performance timer and output the result to the standard stream. Less verbose than /c TIMER_STOP_VERBOSE */ #define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl; #endif void *MemSet(void *dest, _UINT8 c, size_t count) { size_t blockIdx; size_t blocks = count >> 3; size_t bytesLeft = count - (blocks << 3); _UINT64 cUll = c | (((_UINT64)c) << 8 ) | (((_UINT64)c) << 16 ) | (((_UINT64)c) << 24 ) | (((_UINT64)c) << 32 ) | (((_UINT64)c) << 40 ) | (((_UINT64)c) << 48 ) | (((_UINT64)c) << 56 ); _UINT64 *destPtr8 = (_UINT64*)dest; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll; if (!bytesLeft) return dest; blocks = bytesLeft >> 2; bytesLeft = bytesLeft - (blocks << 2); _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx]; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll; if (!bytesLeft) return dest; blocks = bytesLeft >> 1; bytesLeft = bytesLeft - (blocks << 1); _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx]; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll; if (!bytesLeft) return dest; _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx]; for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll; return dest; } int _tmain(int argc, _TCHAR* argv[]) { TIMER_INIT const size_t n = 10000000; const _UINT64 m = _UINT64_MAX; const _UINT64 o = 1; char test[n]; { cout << "memset()" << endl; TIMER_START; for (int i = 0; i < m ; i++) for (int j = 0; j < o ; j++) memset((void*)test, 0, n); TIMER_STOP; } { cout << "MemSet() took:" << endl; TIMER_START; for (int i = 0; i < m ; i++) for (int j = 0; j < o ; j++) MemSet((void*)test, 0, n); TIMER_STOP; } cout << "Done" << endl; int wait; cin >> wait; return 0; }

La salida es la siguiente cuando se compila la versión para sistemas de 32 bits:

memset() took: 5.569000 MemSet() took: 5.544000 Done

El resultado es el siguiente cuando se compila la versión para sistemas de 64 bits:

memset() took: 2.781000 MemSet() took: 2.765000 Done

Aquí puede encontrar el código fuente Berkley''s memset() , que creo que es la implementación más común.


Hay una falla fatal en esta prueba, que de otro modo sería genial y útil: como memset es la primera instrucción, parece haber algo de "sobrecarga de memoria", lo que la hace extremadamente lenta. Al mover el tiempo de memset al segundo lugar y otra cosa al primer lugar o simplemente sincronizar dos veces el memset, ¡memset es el más rápido con todos los switches de compilación!


Hoy en día su compilador debería hacer todo el trabajo por usted. Al menos de lo que sé es que gcc es muy eficiente en la optimización de llamadas a memset lejos (sin embargo, revise mejor el ensamblador).

Luego también, evite memset si no tiene que:

  • use calloc para la memoria del montón
  • use la inicialización adecuada ( ... = { 0 } ) para la memoria de la pila

Y para fragmentos realmente grandes usa mmap si lo tienes. Esto simplemente obtiene cero memoria inicializada del sistema "gratis".


La función memset está diseñada para ser flexible y simple, incluso a expensas de la velocidad. En muchas implementaciones, es un bucle while simple que copia el valor especificado un byte a la vez sobre el número de bytes dado. Si quiere un memset más rápido (o memcpy, memmove, etc.), casi siempre es posible codificar uno usted mismo.

La personalización más sencilla sería realizar operaciones de "un solo byte" hasta que la dirección de destino esté alineada en 32 o 64 bits (lo que coincida con la arquitectura de su chip) y luego comenzar a copiar un registro de CPU completo a la vez. Es posible que tenga que hacer un par de operaciones de "ajuste" de un solo byte al final si su rango no termina en una dirección alineada.

Dependiendo de su CPU particular, también podría tener algunas instrucciones SIMD de transmisión que pueden ayudarle. Por lo general, estos funcionarán mejor en direcciones alineadas, por lo que la técnica anterior para usar direcciones alineadas también puede ser útil aquí.

Para poner a cero grandes secciones de la memoria, también puede ver un aumento de velocidad dividiendo el rango en secciones y procesando cada sección en paralelo (donde el número de secciones es el mismo que su número o núcleos / hilos de hardware).

Lo más importante, no hay forma de saber si algo de esto ayudará a menos que lo intente. Como mínimo, eche un vistazo a lo que su compilador emite para cada caso. Vea lo que otros compiladores emiten para su "memset" estándar también (su implementación puede ser más eficiente que la de su compilador).


Si mal no recuerdo (de hace un par de años), uno de los desarrolladores senior hablaba de una forma rápida de bzero () en PowerPC (las especificaciones decían que necesitábamos poner a cero casi toda la memoria en el encendido). Puede que no se traduzca bien (si es que lo hace) a x86, pero podría valer la pena explorarlo.

La idea era cargar una línea de caché de datos, borrar esa línea de caché de datos y luego volver a escribir la línea de caché de datos borrados en la memoria.

Por lo que vale, espero que ayude.



x86 es una amplia gama de dispositivos.

Para el objetivo x86 totalmente genérico, un bloque de ensamblaje con "rep movsd" podría lanzar ceros a la memoria de 32 bits al tiempo. Intenta asegurarte de que la mayor parte de este trabajo esté alineada con DWORD.

Para chips con mmx, un bucle de ensamblaje con movq podría golpear 64bits a la vez.

Es posible que pueda obtener un compilador C / C ++ para usar una escritura de 64 bits con un puntero a largo largo o _m64. El objetivo debe estar alineado en 8 bytes para obtener el mejor rendimiento.

para chips con sse, movaps es rápido, pero solo si la dirección es de 16 bytes alineados, por lo tanto, use un movsb hasta que esté alineado, y luego complete su clear con un ciclo de movaps

Win32 tiene "ZeroMemory ()", pero se me olvida si eso es una macro para memset, o una ''buena'' implementación real.


memset generalmente está diseñado para ser un código de configuración / cero de propósito general muy rápido. Maneja todos los casos con diferentes tamaños y alineaciones, que afectan los tipos de instrucciones que puede utilizar para hacer su trabajo. Según el sistema en el que se encuentre (y de qué proveedor proviene su stdlib), la implementación subyacente podría ser un ensamblador específico de esa arquitectura para aprovechar sus propiedades nativas. También podría tener casos especiales internos para manejar el caso de la puesta a cero (en lugar de establecer algún otro valor).

Dicho esto, si tiene una puesta a cero de memoria muy específica y de rendimiento muy crítico, es posible que pueda superar una implementación específica de memset haciéndolo usted mismo. memset y sus amigos en la biblioteca estándar siempre son objetivos divertidos para la programación de una sola mano. :)