utiliza que para c optimization assembly x86 64bit

para que se utiliza memset en c



¿Por qué son complicados memcpy/memset superior? (6)

Al depurar, con frecuencia me metí en la implementación del ensamblaje manuscrito de memcpy y memset. Por lo general, estos se implementan usando instrucciones de transmisión si están disponibles, bucle desenrollado, alineación optimizada, etc. También recientemente me encontré con este ''error'' debido a la optimización de memcpy en glibc .

La pregunta es: ¿por qué los fabricantes de hardware (Intel, AMD) no pueden optimizar el caso específico de

rep stos

y

rep movs

ser reconocido como tal y hacer el relleno y copia más rápido posible en su propia arquitectura?


Érase una vez rep movsb fue la solución óptima.

La PC original de IBM tenía un procesador 8088 con un bus de datos de 8 bits y sin caches. Entonces, el programa más rápido fue generalmente el que menos bytes de instrucción tenía. Tener instrucciones especiales ayudadas.

Hoy en día, el programa más rápido es el que puede usar tantas funciones de CPU como sea posible en paralelo. Por extraño que parezca al principio, tener un código con muchas instrucciones simples en realidad puede correr más rápido que una sola instrucción de hágalo todo.

Intel y AMD mantienen las viejas instrucciones principalmente para compatibilidad con versiones anteriores.


Costo.

El costo de optimizar memcpy en su biblioteca C es bastante mínimo, tal vez unas pocas semanas de tiempo de desarrollador aquí y allá. Tendrá que hacer una nueva versión cada varios años más o menos cuando las características del procesador cambien lo suficiente como para justificar una nueva escritura. Por ejemplo, el glibc de GNU y el libSystem de Apple tienen un memcpy específicamente optimizado para SSE3.

El costo de optimizar en hardware es mucho más alto. No solo es más costoso en términos de costos de desarrollo (diseñar una CPU es mucho más difícil que escribir código de ensamblaje de espacio de usuario), pero aumentaría la cantidad de transistores del procesador. Eso podría tener una serie de efectos negativos:

  • Mayor consumo de energía
  • Mayor costo unitario
  • Mayor latencia para ciertos subsistemas de CPU
  • Menor velocidad máxima de reloj

En teoría, podría tener un impacto negativo general en el rendimiento y el costo unitario.

Maxim: No lo haga en hardware si la solución de software es lo suficientemente buena.

Nota: El error que ha citado no es realmente un error en glibc la especificación C. Es más complicado. Básicamente, la gente glibc dice que memcpy comporta exactamente como se anuncia en el estándar, y otras personas se quejan de que memcpy debe tener un alias para memmove .

Tiempo para una historia: me recuerda a una queja que tuvo un desarrollador de juegos de Mac cuando ejecutó su juego en un procesador 603 en lugar de un 601 (esto es de la década de 1990). El 601 tenía soporte de hardware para cargas y tiendas desalineadas con un mínimo de penalización de rendimiento. El 603 simplemente generó una excepción; al descargar al kernel, imagino que la unidad de carga / almacenamiento podría simplificarse mucho, posiblemente haciendo que el procesador sea más rápido y más barato en el proceso. El nanokernel de Mac OS manejó la excepción al realizar la operación de carga / almacenamiento requerida y devolver el control al proceso.

Pero este desarrollador tenía una rutina personalizada de blitting para escribir píxeles en la pantalla que no guardaba las cargas y las tiendas desalineadas. El rendimiento del juego estuvo bien en el 601 pero abominable en el 603. La mayoría de los otros desarrolladores no se dieron cuenta si usaron la función de blitting de Apple, ya que Apple podría simplemente reimplementarlo para procesadores más nuevos.

La moraleja de la historia es que un mejor rendimiento proviene tanto de mejoras de software y hardware.

En general, la tendencia parece estar en la dirección opuesta al tipo de optimizaciones de hardware mencionadas. Mientras que en x86 es fácil escribir memcpy en el ensamblaje, algunas arquitecturas más nuevas descargan aún más trabajo al software. De particular interés son las arquitecturas VLIW: Intel IA64 (Itanium), los DSP TI TMS320C64x y Transmeta Efficeon son ejemplos. Con VLIW, la programación en ensamblador se vuelve mucho más complicada: tienes que seleccionar explícitamente qué unidades de ejecución obtienen qué comandos y qué comandos se pueden hacer al mismo tiempo, algo que un x86 moderno hará por ti (a menos que sea un átomo). Así que escribir memcpy repente se vuelve mucho, mucho más difícil.

Estos trucos arquitectónicos le permiten cortar una gran parte del hardware de sus microprocesadores al tiempo que conservan los beneficios de rendimiento de un diseño superescalar. Imagine tener un chip con una huella más cercana a un Atom pero con un rendimiento más cercano a un Xeon. Sospecho que la dificultad de programar estos dispositivos es el principal factor que impide una adopción más amplia.


En los sistemas integrados, es común tener hardware especializado que hace memcpy / memset. Normalmente no se realiza como una instrucción especial de CPU, sino que es un periférico DMA que se encuentra en el bus de memoria. Usted escribe un par de registros para indicarle las direcciones, y HW hace el resto. En realidad, no garantiza una instrucción especial de la CPU, ya que realmente es solo un problema de interfaz de memoria que realmente no necesita involucrar a la CPU.


Si no está roto, no lo arregles. No está roto.

Un problema principal son los accesos no alineados. Van de malo a realmente malo dependiendo de la arquitectura en la que se esté ejecutando. Mucho de eso tiene que ver con los programadores, algunos con los compiladores.

La forma más económica de arreglar memcpy es no usarlo, mantener sus datos alineados en lindos límites y usar o hacer una alternativa a memcpy que solo admita buenas copias alineadas. Incluso mejor sería tener un compilador para sacrificar el espacio del programa y ram por la velocidad. La gente o los lenguajes que usan muchas estructuras de tal manera que el compilador genera internamente llamadas a memcpy o lo que sea que ese equivalente de idioma tenga, sus estructuras crecen de tal manera que hay una almohadilla entre ellas o relleno dentro. Una estructura de 59 bytes puede convertirse en 64 bytes en su lugar. malloc o una alternativa que solo da punteros a una dirección alineada como se especifica. etcétera etcétera.

Es considerablemente más fácil hacer todo esto usted mismo. Un malloc alineado, estructuras que son múltiplos del tamaño de alineación. Su propia memcpy alineada, etc. ya que es fácil, ¿por qué la gente del hardware arruinaría sus diseños, compiladores y usuarios? no hay argumentos comerciales para eso.

Otra razón es que los cachés han cambiado la imagen. su dram solo es accesible en un tamaño fijo, 32 bits y 64 bits, algo así, cualquier acceso directo más pequeño que eso es un gran golpe de rendimiento. Ponga el caché en frente de que el golpe de rendimiento baje, cualquier lectura-modificación-escritura ocurre en el caché con la modificación que permite mulitple modifica para una sola lectura y escritura de dram. Aún desea reducir el número de ciclos de memoria a la memoria caché, sí, y aún puede ver la ganancia de rendimiento suavizando eso con el cambio de marcha (primera velocidad de 8 bits, segunda velocidad de 16 bits, tercera velocidad de 32 bits, 64 velocidad de crucero de bits, desplazamiento de 32 bits hacia abajo, desplazamiento de 16 bits hacia abajo, cambio de 8 bits hacia abajo)

No puedo hablar por Intel pero sé que gente como ARM ha hecho lo que estás pidiendo

ldmia r0!,{r2,r3,r4,r5}

por ejemplo, todavía hay cuatro transferencias de 32 bits si el núcleo usa una interfaz de 32 bits. pero para las interfaces de 64 bits si están alineadas en un límite de 64 bits se convierte en una transferencia de 64 bits con una longitud de dos, un conjunto de negociaciones entre las partes y dos palabras de 64 bits se mueven. Si no está alineado en un límite de 64 bits, se convierte en tres transferencias de 32 bits, 64 bits y 32 bits. Debe tener cuidado, si se trata de registros de hardware que pueden no funcionar según el diseño de la lógica de registro, si solo admite transferencias de 32 bits individuales, no puede usar esa instrucción en ese espacio de direcciones. Ni idea de por qué intentarías algo así de todos modos.

El último comentario es ... me duele cuando hago esto ... bueno, no hagas eso. No haga un solo paso en las copias de memoria. el corolario de eso es que no hay forma de que alguien modifique el diseño del hardware para facilitar el paso de una copia de memoria al usuario, ese caso de uso es tan pequeño que no existe. Tome todas las computadoras que usan ese procesador funcionando a toda velocidad día y noche, medidas en comparación con todas las computadoras que pasan por un solo paso a través de las copias de memoria y otros códigos de rendimiento optimizado. Es como comparar un grano de arena con el ancho de la tierra. Si tiene un solo escalón, todavía tendrá que pasar de una sola vez a través de la nueva solución, si es que la hay. para evitar grandes latencias de interrupción, la memcpy sintonizada a mano aún comenzará con un if-then-else (si una copia demasiado pequeña entra en un pequeño conjunto de código desenrollado o un bucle de copia de byte) y luego ingresará en una serie de copias de bloques en una velocidad óptima sin tamaño de latencia horrible. Aún tendrá que pasar un solo paso por eso.

para hacer una depuración de paso único, tienes que compilar el código equivocado, lento, de todos modos, la forma más fácil de resolver un solo paso a través del problema memcpy, es tener el compilador y el enlazador cuando se construya para depurar, compilar y vincular memcpy optimizado o una biblioteca alternativa no optimizada en general. gnu / gcc y llvm son de código abierto, puedes hacer que hagan lo que quieras.


Una cosa que me gustaría agregar a las otras respuestas es que rep movs no es realmente lento en todos los procesadores modernos. Por ejemplo,

Por lo general, la instrucción REP MOVS tiene una gran sobrecarga para elegir y configurar el método correcto. Por lo tanto, no es óptimo para pequeños bloques de datos. Para grandes bloques de datos, puede ser bastante eficiente cuando se cumplen ciertas condiciones para la alineación, etc. Estas condiciones dependen de la CPU específica (consulte la página 143). En los procesadores Intel Nehalem y Sandy Bridge, este es el método más rápido para mover grandes bloques de datos , incluso si los datos no están alineados.

[Resaltar es mío.] Referencia: Agner Fog, Optimización de subrutinas en lenguaje ensamblador Una guía de optimización para plataformas x86. ,pag. 156 (y ver también la sección 16.10, p 143) [versión de 2011-06-08].


Propósito General vs. Especializado

Un factor es que esas instrucciones (instrucciones de prefijo / cadena de repetición) son de propósito general, por lo que manejarán cualquier alineación, cualquier número de bytes o palabras y tendrán cierto comportamiento relativo a la memoria caché o el estado de los registros, etc. efectos secundarios bien definidos que no se pueden cambiar.

La copia de memoria especializada solo puede funcionar para ciertas alineaciones, tamaños y puede tener un comportamiento diferente frente a la caché.

El ensamblaje escrito a mano (ya sea en la biblioteca o en el que los desarrolladores pueden implementarse por sí mismos) puede superar la implementación de la instrucción de cadena para los casos especiales en los que se usa. Los compiladores suelen tener varias implementaciones de memcpy para casos especiales y luego el desarrollador puede tener un caso "muy especial" en el que se desenrollan.

No tiene sentido hacer esta especialización en el nivel de hardware. Demasiada complejidad (= costo).

La ley de los rendimientos decrecientes

Otra forma de pensar es que cuando se introducen nuevas características, por ejemplo, SSE, los diseñadores realizan cambios arquitectónicos para admitir estas características, por ejemplo, una interfaz de ancho de banda mayor o mayor, cambios en la tubería, nuevas unidades de ejecución, etc. El diseñador es Es poco probable en este punto volver a la parte "legada" del diseño para tratar de ponerlo al día con las últimas funciones. Eso sería una especie de contraproducente. Si sigues esta filosofía, puedes preguntar por qué necesitamos SIMD en primer lugar, ¿no puede el diseñador simplemente hacer que las instrucciones estrechas funcionen tan rápido como SIMD para aquellos casos en que alguien usa SIMD? La respuesta suele ser que no vale la pena porque es más fácil incluir una nueva unidad de ejecución o instrucciones.