2011-12-16 15 views
8

? Tenía curiosidad sobre si había o no alguna ventaja en cuanto a la eficacia para utilizar memset() en una situación similar a la siguiente.¿Cuál es la ventaja de usar memset() en C

Dadas las siguientes declaraciones tampón ...

struct More_Buffer_Info 
{ 
    unsigned char a[10]; 
    unsigned char b[10]; 
    unsigned char c[10]; 
}; 

struct My_Buffer_Type 
{ 
    struct More_Buffer_Info buffer_info[100]; 
}; 

struct My_Buffer_Type my_buffer[5]; 

unsigned char *p; 
p = (unsigned char *)my_buffer; 

Además de tener menos líneas de código, es que hay una ventaja de utilizar esta:

memset((void *)p, 0, sizeof(my_buffer)); 

Durante este:

for (i = 0; i < sizeof(my_buffer); i++) 
{ 
    *p++ = 0; 
} 
+2

Si 'p' va a apuntar a los objetos de tipo' struct My_Buffer_Type', por favor indíquelo de esa manera, en lugar de darle un tipo diferente y abarrotar el código con moldes innecesarios. – sarnold

+0

también considera la inicialización: 'struct My_Buffer_Type my_buffer [5] = {0};' recursivamente inicializa todo en (el tipo correcto de) '0'. – pmg

+0

@sarnold Gracias por el consejo. Estaba tratando de simplificar un buffer mucho más complicado para el ejemplo. –

Respuesta

23

Esto se aplica tanto a memset() como a memcpy():

  1. Menos Código: Como ya se ha mencionado, es más cortos - menos líneas de código.
  2. Más legible: Más corto también lo hace más legible. (memset() es más legible que ese bucle)
  3. Puede ser más rápido: A veces puede permitir optimizaciones de compilador más agresivas. (Lo que puede ser más rápido)
  4. desalineación: En algunos casos, cuando se trata con los datos mal alineados en un procesador que no admite accesos desalineados, memset() y memcpy() puede ser la única solución limpia.

para ampliar el punto tercero, memset() puede ser muy optimizado por el compilador usando SIMD y tal. Si escribe un bucle en su lugar, el compilador primero tendrá que "descubrir" qué es lo que hace antes de intentar optimizarlo.

La idea básica aquí es que memset() y funciones de biblioteca similares, en cierto sentido, "le dice" al compilador su intención.


Según lo mencionado por @Oli en los comentarios, hay algunas desventajas. Los ampliaré aquí:

  1. Debes asegurarte de que memset() realmente hace lo que quieres. La norma no dice que los ceros para los distintos tipos de datos sean necesariamente cero en la memoria.
  2. Para datos distintos de cero, memset() está restringido a solo contenido de 1 byte. Por lo tanto, no puede usar memset() si desea establecer una matriz de int a algo distinto de cero (o 0x01010101 o algo así ...).
  3. Aunque es raro, hay algunos casos de esquina, donde en realidad es posible batir al compilador en rendimiento con su propio bucle.*

* Voy a dar un ejemplo de esto desde mi experiencia:

Aunque memset() y memcpy() son por lo general las características intrínsecas del compilador con un manejo especial por el compilador, siguen siendo genéricos funciones. No dicen nada sobre el tipo de datos, incluida la alineación de los datos.

Por lo tanto, en unos pocos casos (abeit rare), el compilador no puede determinar la alineación de la región de la memoria, y por lo tanto debe producir código adicional para manejar la desalineación. Mientras que, si el programador, está 100% seguro de la alineación, el uso de un bucle en realidad podría ser más rápido.

Un ejemplo común es cuando se usan intrínsecos SSE/AVX. (como copiar una matriz alineada de 16/32 bytes de float s) Si el compilador no puede determinar la alineación de 16/32 bytes, tendrá que usar carga/almacenamiento desalineados y/o código de manejo. Si simplemente escribe un bucle utilizando los valores intrínsecos de carga/almacenamiento alineados SSE/AVX, puede probablemente hacerlo mejor.

float *ptrA = ... // some unknown source, guaranteed to be 32-byte aligned 
float *ptrB = ... // some unknown source, guaranteed to be 32-byte aligned 
int length = ... // some unknown source, guaranteed to be multiple of 8 

// memcopy() - Compiler can't read comments. It doesn't know the data is 32-byte 
// aligned. So it may generate unnecessary misalignment handling code. 
memcpy(ptrA, ptrB, length * sizeof(float)); 

// This loop could potentially be faster because it "uses" the fact that 
// the pointers are aligned. The compiler can also further optimize this. 
for (int c = 0; c < length; c += 8){ 
    _mm256_store_ps(ptrA + c, _mm256_load_ps(ptrB + c)); 
} 
+1

+1 para una respuesta completa. Sin embargo, una desventaja de 'memset' es que no siempre es el comportamiento que desea (según el estándar). Por ejemplo, ni un puntero ni un flotador necesariamente tienen una representación 0/NULL de todos los bits cero. –

+1

++ 1; la alineación y la zancada pueden marcar una gran diferencia en las operaciones de memoria masiva. Una plataforma en la que trabajé realmente proporcionaba múltiples implementaciones de memcpy/memset/etc basadas en la alineación conocida de los datos (ej. Memcpy32(), memcpy128(), memcpy256()) debido a las enormes ganancias de rendimiento posibles al usar el hardware especial " mover una línea de caché completa a la vez "ops. – Crashworks

+0

En las implementaciones 'memcpy' /' memset' vi que había prólogos y epílogos para copiar hasta cierto alineamiento, después de lo cual se usaron los registros más grandes posibles. Entonces, tu ciclo debe ser muy pequeño para notar la diferencia, ¿no? – Aktau

1

memset proporciona una forma estándar de escribir código, permitiendo que las bibliotecas de plataforma/compilador particulares determinen el mecanismo más eficiente. Según el tamaño de los datos, puede hacer, por ejemplo, tiendas de 32 o 64 bits tanto como sea posible.

7

Depende de la calidad del compilador y de las bibliotecas. En la mayoría de los casos, memset es superior.

La ventaja de memset es que en muchas plataformas es en realidad un compiler intrinsic; es decir, el compilador puede "entender" la intención de establecer una gran franja de memoria en un cierto valor, y posiblemente generar un mejor código.

En particular, eso podría significar el uso de operaciones de hardware específicas para configurar grandes regiones de memoria, como SSE en el x86, AltiVec en el PowerPC, NEON en el ARM, y así sucesivamente. Esto puede ser una enorme mejora en el rendimiento.

Por otro lado, al usar un bucle for le dice al compilador que haga algo más específico, "cargue esta dirección en un registro .Escriba un número. Agregue uno a la dirección. Escríbalo un número. ," y así. En teoría, un compilador perfectamente inteligente reconocería este ciclo por lo que es y lo convertiría en un memset de todos modos; pero nunca me he encontrado con un compilador real que haya hecho esto.

Por lo tanto, se supone que memset fue escrito por personas inteligentes para ser la forma mejor y más rápida posible de establecer toda una región de memoria para la plataforma y el hardware específicos que admite el compilador. Eso es often, but not always, cierto.

3

dos ventajas:

  1. La versión con memset es más fácil de leer - Esto se relaciona con, pero no el mismo que, teniendo un menor número de líneas de código.Se tarda menos de pensar saber lo que hace la versión memset, especialmente si usted lo escribe

    memset(my_buffer, 0, sizeof(my_buffer)); 
    

    en lugar de la vía indirecta a través p y el elenco innecesario void * (NOTA: sólo es innecesario si estás realmente codificación en C y no en C++: algunas personas no tienen clara la diferencia).

  2. memset es probable a ser capaz de escribir 4 u 8 bytes a la vez y/o aprovechar las instrucciones especiales caché de sugerencia; por lo tanto, puede ser más rápido que su ciclo byte-a-tiempo. (NOTA: Algunos compiladores son lo suficientemente inteligente como para reconocer un bucle en masa-compensación y sustituir ya sea más amplia escribe en la memoria o una llamada a memset Su experiencia puede variar siempre medir el rendimiento antes de intentar afeitarse ciclos...)

+0

'memset (my_buffer, 0, sizeof my_buffer)' debe ser 'memset (my_buffer, 0, sizeof * my_buffer)' (si 'my_buffer' es un puntero) o' memset (& my_buffer, 0, sizeof my_buffer) '(de lo contrario) Desafortunadamente, no es trivial proporcionar un diagnóstico para el primero de esos ... –

+0

@TobySpeight En el contexto del código del OP, lo que escribí es correcto (excepto posiblemente si su guía de estilo requiere 'sizeof' sin paréntesis cuando se aplica a una variable, que en mi opinión no tan humilde es Incorrecta) – zwol

+0

Usted "Está bien @zwol, porque' my_buffer' en el primer argumento se descompone en un puntero, pero en 'sizeof my_buffer' es la matriz completa. Debería haber mirado hacia atrás al OP en lugar de asumir que era un puntero o un tipo de valor. –

5

Recuerde que este

for (i = 0; i < sizeof(my_buffer); i++) 
{ 
    p[i] = 0; 
} 

también puede ser más rápido que

for (i = 0; i < sizeof(my_buffer); i++) 
{ 
    *p++ = 0; 
} 

Como ya se ha contestado, el compilador a menudo ha optimizado la mano rutinas f o memset() memcpy() y otras funciones de cadena. Y estamos hablando significativamente más rápido. ahora la cantidad de código, número de instrucciones, que un fast memcpy o memset del compilador, es generalmente mucho más grande que la solución de bucle que sugirió. menos líneas de código, menos instrucciones no significan más rápido.

De todos modos, mi mensaje es probar ambos. diagrame el código, vea la diferencia, intente comprender, haga preguntas en el desbordamiento de la pila si no lo hace. y luego use un temporizador y cronometra las dos soluciones, llame a cualquiera de las funciones de memcpy miles o cientos de miles de veces y tiempo todo (para eliminar el error en el tiempo). Asegúrese de hacer copias cortas como digamos 7 elementos o 5 elementos, y copias grandes como cientos de bytes por memset y pruebe algunos números primos mientras está en ello. En algunos procesadores de algunos sistemas, su ciclo puede ser más rápido para algunos elementos, como 3 o 5, o algo así, muy rápidamente, aunque se vuelve lento.

Aquí hay una pista sobre el rendimiento. La memoria DDR en su computadora tiene 64 bits de ancho y necesita escribirse 64 bits a la vez, quizás tiene ecc y debe calcular a través de esos bits y escribir 72 bits a la vez. No siempre es ese número exacto, pero síguelo, tendrá sentido para 32 bits o 64 o 128 o lo que sea. Si realiza una instrucción de escritura de byte único para ejecutar ram, el hardware necesitará hacer una de estas dos cosas, si no hay cachés en el camino, el sistema de memoria debe realizar una lectura de 64 bits, modificar el byte y luego escríbelo. Sin algún tipo de optimización de hardware, escribir 8 bytes dentro de esa fila de una copita, es de 16 ciclos de memoria, y dram es muy muy lento, no te dejes engañar por los números de 1333mhz.

Ahora, si tiene una memoria caché, la primera escritura de bytes va a requerir una línea de caché leída de dram, que es una o varias de estas lecturas de 64 bits, las próximas 7 o 15 o cualquier escritura de byte probablemente vaya a sea ​​realmente rápido, ya que solo van a la caché y no a ddr, eventualmente esa línea de caché se apaga, dramáticamente, una o dos o cuatro, etc. de estos 64 bits o cualquier ubicación de ddr. Así que, aunque solo está escribiendo, todavía tiene que leer todo el ram y escribirlo, por lo tanto, el doble de ciclos que desee. Si es posible, y es con algunos procesadores y sistemas de memoria, el memset o la parte de escritura de una memcpy, pueden ser instrucciones individuales con una línea de caché completa o una ubicación ddr completa y no se requiere lectura, la velocidad duplicada al instante.Esta no es la forma en que funcionan todas las optimizaciones, pero con suerte le da una idea de cómo pensar sobre el problema. Con su programa siendo arrastrado a la memoria caché en las líneas de caché, puede duplicar o triplicar la cantidad de instrucciones ejecutadas si, a cambio, obtiene la mitad o un trimestre o más recortes en el número de ciclos de DDR y gana en general.

Como mínimo, las rutinas memset y memcpy del compilador van a realizar una operación de bytes si la dirección de inicio es impar, entonces un 16 bit si no está alineado en 32 bits. Luego, un 32 bit si no está alineado en 64 y en adelante hasta que alcancen el tamaño de transferencia óptimo para ese conjunto de instrucciones/sistema. En el brazo tienden a apuntar a 128 bits. Entonces, el peor de los casos en la parte frontal sería un byte único, luego una palabra y luego unas pocas palabras, luego ingrese al conjunto principal o al ciclo de copia. En el caso de las transferencias ARM de 128 bits, se escribieron 128 bits por instrucción. Luego en la parte de atrás si no se alinea el mismo trato, unas pocas palabras, una mitad de palabra, un byte y el peor de los casos. También verá que las bibliotecas hacen cosas como, si el número de bytes es menor que X donde X es un número pequeño como 13 o más, entonces entra en un bucle como el suyo, simplemente copie algunos bytes debido a la cantidad de instrucciones y ciclos de reloj para apoyar ese ciclo es más pequeño/más rápido. Desmontar o encontrar el código fuente de gcc para ARM y probablemente mips y algunos otros buenos procesadores y ver de lo que estoy hablando.

+0

Fantástica respuesta. Gracias por ser tan completo. Estaba pensando que desmontaría y miraría las diferencias de instrucción. –

1

Su variable p solo es necesaria para el ciclo de inicialización. El código para el memset debe ser simplemente

memset(my_buffer, 0, sizeof(my_buffer)); 

que es más simple y menos propenso a errores. El objetivo de un parámetro void* es exactamente que aceptará cualquier tipo de puntero, el reparto explícito es innecesario y la asignación a un puntero de un tipo diferente no tiene sentido.

Por lo tanto, uno de los beneficios de usar memset() en este caso es evitar una variable intermedia innecesaria.

Otra ventaja es que memset() en cualquier plataforma en particular es probable que se optimice para la plataforma de destino, mientras que la eficiencia de su bucle depende de la configuración del compilador y el compilador.

Cuestiones relacionadas