Una de las razones declaradas para conocer al ensamblador es que, en ocasiones, puede emplearse para escribir código que será más eficaz que escribir ese código en un lenguaje de nivel superior, C en particular. Sin embargo, también he oído que declaró muchas veces que a pesar de que no es del todo falsa, los casos en los que el ensamblador puede en realidad ser utilizado para generar un código más performante son extremadamente raros y requiere un conocimiento experto y experiencia en el montaje.
Esta pregunta ni siquiera entra en el hecho de que las instrucciones del ensamblador serán específicas de la máquina y no portátiles, o cualquiera de los otros aspectos del ensamblador. Por supuesto, hay muchas buenas razones para conocer el ensamblaje además de este, pero se trata de una pregunta específica que solicita ejemplos y datos, no un discurso extendido sobre ensamblador versus lenguajes de nivel superior.
¿Alguien puede proporcionar algunos ejemplos específicos de casos en que el ensamblaje sea más rápido que el código C bien escrito utilizando un compilador moderno, y puede respaldar esa afirmación con evidencia de perfil? Estoy bastante seguro de que estos casos existen, pero realmente quiero saber exactamente qué tan esotéricos son estos casos, ya que parece ser un punto de discusión.
fuente
-O3
bandera, probablemente sea mejor dejar la optimización para el compilador de C :-)Respuestas:
Aquí hay un ejemplo del mundo real: el punto fijo se multiplica en compiladores antiguos.
Estos no solo son útiles en dispositivos sin punto flotante, sino que brillan cuando se trata de precisión, ya que le brindan 32 bits de precisión con un error predecible (el flotante solo tiene 23 bits y es más difícil predecir la pérdida de precisión). es decir, precisión absoluta uniforme en todo el rango, en lugar de precisión relativa cercana a la uniforme (
float
).Los compiladores modernos optimizan muy bien este ejemplo de punto fijo, por lo que para ver ejemplos más modernos que todavía necesitan código específico del compilador, vea
uint64_t
para multiplicaciones de 32x32 => 64 bits no se optimiza en una CPU de 64 bits, por lo que necesita intrínsecos o__int128
un código eficiente en sistemas de 64 bits.C no tiene un operador de multiplicación completa (resultado de 2 N bits de entradas de N bits). La forma habitual de expresarlo en C es convertir las entradas al tipo más amplio y esperar que el compilador reconozca que los bits superiores de las entradas no son interesantes:
El problema con este código es que hacemos algo que no se puede expresar directamente en el lenguaje C. Queremos multiplicar dos números de 32 bits y obtener un resultado de 64 bits, de los cuales devolvemos el medio de 32 bits. Sin embargo, en C esta multiplicación no existe. Todo lo que puede hacer es promover los enteros a 64 bits y multiplicar 64 * 64 = 64.
Sin embargo, x86 (y ARM, MIPS y otros) pueden hacer la multiplicación en una sola instrucción. Algunos compiladores solían ignorar este hecho y generar código que llama a una función de biblioteca de tiempo de ejecución para hacer la multiplicación. El cambio en 16 también lo hace a menudo una rutina de biblioteca (también el x86 puede hacer tales cambios).
Así que nos quedan una o dos llamadas a la biblioteca solo para una multiplicación. Esto tiene serias consecuencias. El cambio no solo es más lento, sino que los registros deben conservarse en todas las llamadas a funciones y tampoco ayuda a la inserción y el desenrollado de código.
Si reescribe el mismo código en el ensamblador (en línea), puede obtener un aumento de velocidad significativo.
Además de esto: usar ASM no es la mejor manera de resolver el problema. La mayoría de los compiladores le permiten usar algunas instrucciones de ensamblador en forma intrínseca si no puede expresarlas en C. El compilador VS.NET2008, por ejemplo, expone el mul de 32 * 32 = 64 bits como __emul y el cambio de 64 bits como __ll_rshift.
Usando intrínsecos, puede reescribir la función de manera que el compilador C tenga la oportunidad de comprender lo que está sucediendo. Esto permite que el código esté en línea, el registro asignado, la eliminación de subexpresión común y la propagación constante también se pueden hacer. Obtendrá una gran mejora en el rendimiento sobre el código de ensamblador escrito a mano de esa manera.
Como referencia: El resultado final para el mul de punto fijo para el compilador VS.NET es:
La diferencia de rendimiento de las divisiones de punto fijo es aún mayor. Tuve mejoras hasta el factor 10 para el código de punto fijo pesado de división escribiendo un par de líneas asm.
El uso de Visual C ++ 2013 proporciona el mismo código de ensamblaje en ambos sentidos.
gcc4.1 de 2007 también optimiza muy bien la versión C pura. (El explorador del compilador Godbolt no tiene instaladas versiones anteriores de gcc, pero presumiblemente incluso las versiones anteriores de GCC podrían hacerlo sin intrínsecos).
Vea source + asm para x86 (32 bits) y ARM en el explorador del compilador Godbolt . (Desafortunadamente no tiene ningún compilador lo suficientemente antiguo como para producir código incorrecto a partir de la versión C pura simple)
CPU modernas pueden hacer cosas C no tiene operadores para nada , al igual que
popcnt
o bit-exploración para encontrar el primer o el último bit activado . (POSIX tiene unaffs()
función, pero su semántica no coincide con x86bsf
/bsr
. Ver https://en.wikipedia.org/wiki/Find_first_set ).Algunos compiladores a veces pueden reconocer un bucle que cuenta el número de bits establecidos en un entero y compilarlo en una
popcnt
instrucción (si está habilitado en el momento de la compilación), pero es mucho más confiable usarlo__builtin_popcnt
en GNU C, o en x86 si solo está apuntar hardware con SSE4.2:_mm_popcnt_u32
desde<immintrin.h>
.O en C ++, asigne a ay
std::bitset<32>
use.count()
. (Este es un caso en el que el lenguaje ha encontrado una manera de exponer de manera portátil una implementación optimizada de popcount a través de la biblioteca estándar, de una manera que siempre se compilará a algo correcto, y puede aprovechar lo que sea compatible con el objetivo). Consulte también https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .Del mismo modo,
ntohl
puede compilar abswap
(intercambio de bytes de 32 bits x86 para conversión endian) en algunas implementaciones de C que lo tienen.Otra área importante para intrínsecos o asm escritos a mano es la vectorización manual con instrucciones SIMD. Los compiladores no son malos con bucles simples
dst[i] += src[i] * 10.0;
, pero a menudo funcionan mal o no se auto-vectorizan cuando las cosas se complican. Por ejemplo, es poco probable que obtenga algo como ¿Cómo implementar atoi usando SIMD? generado automáticamente por el compilador a partir del código escalar.fuente
Hace muchos años estaba enseñando a alguien a programar en C. El ejercicio consistía en rotar un gráfico 90 grados. Regresó con una solución que tardó varios minutos en completarse, principalmente porque estaba usando multiplicaciones y divisiones, etc.
Le mostré cómo relanzar el problema utilizando cambios de bits, y el tiempo para procesar se redujo a unos 30 segundos en el compilador no optimizador que tenía.
Acababa de obtener un compilador de optimización y el mismo código rotó el gráfico en <5 segundos. Miré el código de ensamblaje que estaba generando el compilador y, por lo que vi, decidí que mis días de ensamblador habían terminado.
fuente
add di,di / adc al,al / add di,di / adc ah,ah
etc. para los ocho registros de 8 bits, luego vuelva a hacer los 8 registros y luego repita todo el procedimiento tres más veces, y finalmente guardar cuatro palabras en ax / bx / cx / dx. De ninguna manera un ensamblador se acercará a eso.Casi siempre que el compilador vea código de coma flotante, una versión escrita a mano será más rápida si está utilizando un viejo compilador incorrecto. ( Actualización de 2019: esto no es cierto en general para los compiladores modernos. Especialmente cuando compilamos para algo que no sea x87; los compiladores tienen un tiempo más fácil con SSE2 o AVX para matemáticas escalares, o cualquier otro que no sea x86 con un conjunto de registro FP plano, a diferencia de los x87 registro de pila.)
La razón principal es que el compilador no puede realizar ninguna optimización robusta. Vea este artículo de MSDN para una discusión sobre el tema. Aquí hay un ejemplo donde la versión de ensamblaje tiene el doble de velocidad que la versión C (compilada con VS2K5):
Y algunos números de mi PC que ejecutan una versión de lanzamiento predeterminada * :
Por interés, cambié el ciclo con un dec / jnz y no hizo ninguna diferencia en los tiempos, a veces más rápido, a veces más lento. Supongo que el aspecto de memoria limitada eclipsa otras optimizaciones. (Nota del editor: lo más probable es que el cuello de botella de latencia FP sea suficiente para ocultar el costo adicional de
loop
. Hacer dos sumaciones de Kahan en paralelo para los elementos pares / impares, y agregar los que están al final, podría acelerar esto en un factor de 2. )Vaya, estaba ejecutando una versión ligeramente diferente del código y mostraba los números al revés (es decir, ¡C era más rápido!). Se corrigieron y actualizaron los resultados.
fuente
-ffast-math
. Tienen un nivel de optimización,-Ofast
que actualmente es equivalente a-O3 -ffast-math
, pero en el futuro pueden incluir más optimizaciones que pueden conducir a la generación de código incorrecto en casos de esquina (como el código que se basa en IEEE NaN).a+b == b+a
), pero no asociativo (reordenamiento de operaciones, por lo que el redondeo de intermedios es diferente). re: este código: No creo que x87 sin comentar y unaloop
instrucción sean una demostración increíble de asm rápido.loop
aparentemente no es realmente un cuello de botella debido a la latencia FP. No estoy seguro de si está canalizando operaciones de FP o no; x87 es difícil de leer para los humanos. Dosfstp results
insns al final claramente no son óptimos. Hacer estallar el resultado extra de la pila sería mejor hacerlo con una no tienda. Como elfstp st(0)
IIRC.Sin dar ningún ejemplo específico o evidencia de perfil, puede escribir un mejor ensamblador que el compilador cuando sepa más que el compilador.
En el caso general, un compilador de C moderno sabe mucho más sobre cómo optimizar el código en cuestión: sabe cómo funciona la canalización del procesador, puede intentar reordenar las instrucciones más rápido que un humano, y así sucesivamente; es básicamente lo mismo que una computadora es tan buena o mejor que el mejor jugador humano para juegos de mesa, etc. simplemente porque puede hacer búsquedas dentro del espacio del problema más rápido que la mayoría de los humanos. Aunque teóricamente puede funcionar tan bien como la computadora en un caso específico, ciertamente no puede hacerlo a la misma velocidad, lo que lo hace inviable durante más de unos pocos casos (es decir, el compilador seguramente lo superará si intenta escribir) más de unas pocas rutinas en ensamblador).
Por otro lado, hay casos en los que el compilador no tiene tanta información, diría principalmente cuando se trabaja con diferentes formas de hardware externo, del cual el compilador no tiene conocimiento. El ejemplo principal probablemente sean los controladores de dispositivos, donde el ensamblador combinado con el conocimiento íntimo de un humano del hardware en cuestión puede producir mejores resultados que un compilador de C.
Otros han mencionado instrucciones de propósito especial, que es lo que estoy hablando en el párrafo anterior, instrucciones de las cuales el compilador podría tener conocimiento limitado o ningún conocimiento, lo que hace posible que un humano escriba código más rápido.
fuente
ocamlopt
omite la programación de instrucciones en x86 y, en cambio, lo deja a la CPU porque puede reordenar de manera más efectiva en tiempo de ejecución.En mi trabajo, hay tres razones para conocer y usar el ensamblaje. En orden de importancia:
Depuración: a menudo obtengo código de biblioteca que tiene errores o documentación incompleta. Descubro lo que está haciendo interviniendo en el nivel de ensamblaje. Tengo que hacer esto una vez a la semana. También lo uso como herramienta para depurar problemas en los que mis ojos no detectan el error idiomático en C / C ++ / C #. Mirando la asamblea pasa eso.
Optimización: el compilador funciona bastante bien en la optimización, pero juego en un estadio diferente al de la mayoría. Escribo código de procesamiento de imágenes que generalmente comienza con un código que se ve así:
la "parte de hacer algo" generalmente ocurre en el orden de varios millones de veces (es decir, entre 3 y 30). Al eliminar los ciclos en esa fase de "hacer algo", las ganancias de rendimiento se magnifican enormemente. Por lo general, no empiezo allí, generalmente comienzo escribiendo el código para que funcione primero, luego hago todo lo posible para refactorizar el C para que sea naturalmente mejor (mejor algoritmo, menos carga en el bucle, etc.). Por lo general, necesito leer el ensamblaje para ver qué sucede y rara vez necesito escribirlo. Hago esto tal vez cada dos o tres meses.
haciendo algo que el lenguaje no me deja. Estos incluyen: obtener la arquitectura del procesador y las características específicas del procesador, acceder a los indicadores que no están en la CPU (hombre, realmente deseo que C te de acceso al indicador de acarreo), etc. Lo hago tal vez una vez al año o dos años.
fuente
Solo cuando se utilizan algunos conjuntos de instrucciones de propósito especial, el compilador no es compatible.
Para maximizar el poder de cómputo de una CPU moderna con múltiples canalizaciones y ramificaciones predictivas, debe estructurar el programa de ensamblaje de manera que sea a) casi imposible que un humano escriba b) aún más imposible de mantener.
Además, mejores algoritmos, estructuras de datos y administración de memoria le brindarán al menos un orden de magnitud más rendimiento que las microoptimizaciones que puede realizar en el ensamblaje.
fuente
Aunque C está "cerca" de la manipulación de bajo nivel de datos de 8 bits, 16 bits, 32 bits y 64 bits, hay algunas operaciones matemáticas que C no admite y que a menudo se pueden realizar de manera elegante en ciertas instrucciones de ensamblaje establece:
Multiplicación de punto fijo: el producto de dos números de 16 bits es un número de 32 bits. Pero las reglas en C dicen que el producto de dos números de 16 bits es un número de 16 bits, y el producto de dos números de 32 bits es un número de 32 bits, la mitad inferior en ambos casos. Si quieres la mitad superior de una multiplicación de 16x16 o una multiplicación de 32x32, debes jugar con el compilador. El método general es convertir a un ancho de bits mayor al necesario, multiplicar, desplazar hacia abajo y volver atrás:
En este caso, el compilador puede ser lo suficientemente inteligente como para saber que realmente solo está tratando de obtener la mitad superior de una multiplicación de 16x16 y hacer lo correcto con la multiplicidad de 16x16 nativa de la máquina. O puede ser estúpido y requerir una llamada a la biblioteca para hacer la multiplicación 32x32, eso es exagerado porque solo necesita 16 bits del producto, pero el estándar C no le brinda ninguna forma de expresarse.
Ciertas operaciones de desplazamiento de bits (rotación / transporte):
Esto no es demasiado poco elegante en C, pero de nuevo, a menos que el compilador sea lo suficientemente inteligente como para darse cuenta de lo que está haciendo, va a hacer mucho trabajo "innecesario". Muchos conjuntos de instrucciones de ensamblaje le permiten rotar o desplazarse hacia la izquierda / derecha con el resultado en el registro de acarreo, para que pueda cumplir lo anterior en 34 instrucciones: cargue un puntero al comienzo de la matriz, borre el acarreo y realice 32 8- bit a la derecha, utilizando el incremento automático en el puntero.
Para otro ejemplo, hay registros de desplazamiento de retroalimentación lineal (LFSR) que se realizan de manera elegante en el ensamblaje: tome un trozo de N bits (8, 16, 32, 64, 128, etc.), cambie todo por 1 (ver arriba) algoritmo), luego, si el acarreo resultante es 1, entonces XOR en un patrón de bits que representa el polinomio.
Dicho esto, no recurriría a estas técnicas a menos que tuviera serias limitaciones de rendimiento. Como otros han dicho, el ensamblaje es mucho más difícil de documentar / depurar / probar / mantener que el código C: la ganancia de rendimiento conlleva algunos costos serios.
editar: 3. La detección de desbordamiento es posible en el ensamblaje (realmente no puede hacerlo en C), esto hace que algunos algoritmos sean mucho más fáciles.
fuente
¿Respuesta corta? Algunas veces.
Técnicamente, cada abstracción tiene un costo y un lenguaje de programación es una abstracción de cómo funciona la CPU. C sin embargo está muy cerca. Hace años, recuerdo reírme a carcajadas cuando inicié sesión en mi cuenta UNIX y recibí el siguiente mensaje de fortuna (cuando esas cosas eran populares):
Es divertido porque es cierto: C es como un lenguaje ensamblador portátil.
Vale la pena señalar que el lenguaje ensamblador simplemente se ejecuta sin importar cómo lo escriba. Sin embargo, existe un compilador entre C y el lenguaje ensamblador que genera, y eso es extremadamente importante porque la rapidez con la que tiene su código C tiene mucho que ver con lo bueno que es su compilador.
Cuando gcc apareció en escena, una de las cosas que lo hizo tan popular fue que a menudo era mucho mejor que los compiladores de C que se enviaban con muchos sabores comerciales de UNIX. No solo era ANSI C (nada de esta basura de K&R C), era más robusto y normalmente producía un código mejor (más rápido). No siempre pero a menudo.
Te digo todo esto porque no hay una regla general sobre la velocidad de C y el ensamblador porque no hay un estándar objetivo para C.
Del mismo modo, el ensamblador varía mucho según el procesador que esté ejecutando, las especificaciones de su sistema, qué conjunto de instrucciones está utilizando, etc. Históricamente ha habido dos familias de arquitectura de CPU: CISC y RISC. El jugador más importante en CISC fue y sigue siendo la arquitectura Intel x86 (y el conjunto de instrucciones). RISC dominó el mundo UNIX (MIPS6000, Alpha, Sparc, etc.). CISC ganó la batalla por los corazones y las mentes.
De todos modos, la sabiduría popular cuando era un desarrollador más joven era que x86 escrito a mano a menudo podía ser mucho más rápido que C porque la forma en que funcionaba la arquitectura, tenía una complejidad que se beneficiaba de que un humano lo hiciera. RISC, por otro lado, parecía diseñado para compiladores, por lo que nadie (lo sabía) escribió decir ensamblador Sparc. Estoy seguro de que tales personas existieron, pero sin duda se han vuelto locos y han sido institucionalizados por ahora.
Los conjuntos de instrucciones son un punto importante incluso en la misma familia de procesadores. Ciertos procesadores Intel tienen extensiones como SSE a SSE4. AMD tenía sus propias instrucciones SIMD. El beneficio de un lenguaje de programación como C era que alguien podía escribir su biblioteca, por lo que estaba optimizado para cualquier procesador en el que estuviera ejecutando. Ese fue un trabajo duro en ensamblador.
Todavía hay optimizaciones que puede hacer en ensamblador que ningún compilador podría hacer y un algoritmo de ensamblador bien escrito será tan rápido o más rápido que su equivalente en C. La pregunta más importante es: ¿vale la pena?
Finalmente, el ensamblador era un producto de su tiempo y era más popular en un momento en que los ciclos de la CPU eran caros. Hoy en día, una CPU que cuesta $ 5-10 para fabricar (Intel Atom) puede hacer casi cualquier cosa que cualquiera pueda desear. La única razón real para escribir ensamblador en estos días es para cosas de bajo nivel como algunas partes de un sistema operativo (aun así, la gran mayoría del kernel de Linux está escrito en C), controladores de dispositivos, posiblemente dispositivos integrados (aunque C tiende a dominar allí). también) y así sucesivamente. O solo por patadas (que es algo masoquista).
fuente
Un caso de uso que podría no aplicarse más que para tu placer nerd: en el Amiga, la CPU y los chips de gráficos / audio lucharían por acceder a un área determinada de RAM (los primeros 2 MB de RAM para ser específicos). Entonces, cuando solo tenía 2 MB de RAM (o menos), mostrar gráficos complejos más reproducir sonido mataría el rendimiento de la CPU.
En ensamblador, podría intercalar su código de una manera tan inteligente que la CPU solo intentaría acceder a la RAM cuando los chips de gráficos / audio estuvieran ocupados internamente (es decir, cuando el bus estuviera libre). Entonces, al reordenar sus instrucciones, el uso inteligente de la memoria caché de la CPU, el tiempo del bus, podría lograr algunos efectos que simplemente no eran posibles utilizando un lenguaje de nivel superior porque tenía que cronometrar cada comando, incluso insertar NOP aquí y allá para mantener los diversos chips fuera del radar de los demás.
Esa es otra razón por la cual la instrucción NOP (Sin operación - no hacer nada) de la CPU puede hacer que toda su aplicación se ejecute más rápido.
[EDITAR] Por supuesto, la técnica depende de una configuración de hardware específica. Cuál fue la razón principal por la que muchos juegos de Amiga no podían hacer frente a CPU más rápidas: el tiempo de las instrucciones estaba apagado.
fuente
Punto uno que no es la respuesta.
Incluso si nunca programa en él, me resulta útil conocer al menos un conjunto de instrucciones de ensamblador. Esto es parte de la búsqueda interminable de los programadores para saber más y, por lo tanto, ser mejores. También es útil al ingresar a marcos en los que no tiene el código fuente y al menos tiene una idea aproximada de lo que está sucediendo. También le ayuda a comprender JavaByteCode y .Net IL, ya que ambos son similares al ensamblador.
Para responder la pregunta cuando tiene una pequeña cantidad de código o una gran cantidad de tiempo. Es más útil para usar en chips integrados, donde la baja complejidad del chip y la poca competencia en los compiladores que apuntan a estos chips pueden inclinar la balanza a favor de los humanos. Además, para dispositivos restringidos, a menudo está cambiando el tamaño del código / tamaño de memoria / rendimiento de una manera que sería difícil de instruir a un compilador. Por ejemplo, sé que esta acción del usuario no se llama con frecuencia, por lo que tendré un tamaño de código pequeño y un rendimiento deficiente, pero esta otra función que se ve similar se usa cada segundo, así que tendré un tamaño de código más grande y un rendimiento más rápido. Ese es el tipo de intercambio que puede usar un programador de ensamblaje experto.
También me gustaría agregar que hay una gran cantidad de puntos intermedios en los que puede codificar en la compilación C y examinar el ensamblaje producido, luego cambiar su código C o ajustar y mantener como ensamblaje.
Mi amigo trabaja en microcontroladores, actualmente chips para controlar pequeños motores eléctricos. Trabaja en una combinación de bajo nivel c y ensamblaje. Una vez me habló de un buen día en el trabajo donde redujo el bucle principal de 48 instrucciones a 43. También se enfrenta a opciones como que el código ha crecido para llenar el chip de 256k y la empresa quiere una nueva característica, ¿no?
Me gustaría agregar como desarrollador comercial con una gran cartera o idiomas, plataformas, tipos de aplicaciones que nunca antes sentí la necesidad de sumergirme en el ensamblaje de escritura. Siempre he apreciado el conocimiento que obtuve al respecto. Y a veces depurado en él.
Sé que he respondido mucho más a la pregunta "¿por qué debería aprender ensamblador?", Pero creo que es una pregunta más importante que cuándo es más rápido.
así que intentemos una vez más Deberías estar pensando en ensamblar
Recuerde comparar su ensamblaje con el compilador generado para ver cuál es más rápido / más pequeño / mejor.
David
fuente
sbi
ycbi
) que los compiladores solían (y a veces todavía lo hacen) no aprovechar al máximo, debido a su conocimiento limitado del hardware.Me sorprende que nadie haya dicho esto. ¡La
strlen()
función es mucho más rápida si se escribe en ensamblador! En C, lo mejor que puedes hacer esmientras está en ensamblaje puede acelerarlo considerablemente:
La longitud es en ecx. Esto compara 4 caracteres a la vez, por lo que es 4 veces más rápido. Y piense usando la palabra de orden superior de eax y ebx, ¡será 8 veces más rápido que la rutina C anterior!
fuente
(word & 0xFEFEFEFF) & (~word + 0x80808080)
es cero si todos los bytes en la palabra no son cero.Las operaciones matriciales que utilizan instrucciones SIMD son probablemente más rápidas que el código generado por el compilador.
fuente
No puedo dar los ejemplos específicos porque fue hace muchos años, pero hubo muchos casos en los que el ensamblador escrito a mano podría superar a cualquier compilador. Razones por las cuales:
Podrías desviarte de llamar convenciones, pasar argumentos en registros.
Podrías considerar cuidadosamente cómo usar los registros y evitar almacenar variables en la memoria.
Para cosas como las tablas de salto, puede evitar tener que revisar los límites del índice.
Básicamente, los compiladores hacen un buen trabajo de optimización, y eso casi siempre es "lo suficientemente bueno", pero en algunas situaciones (como la representación de gráficos) en las que está pagando caro por cada ciclo, puede tomar atajos porque conoce el código , donde un compilador no podría porque tiene que estar en el lado seguro.
De hecho, he oído hablar de algunos códigos de representación gráfica en los que una rutina, como una rutina de dibujo de líneas o de relleno de polígonos, en realidad generaba un pequeño bloque de código de máquina en la pila y lo ejecutaba allí, para evitar la toma continua de decisiones. sobre estilo de línea, ancho, patrón, etc.
Dicho esto, lo que quiero que haga un compilador es generar un buen código de ensamblaje para mí, pero no ser demasiado inteligente, y lo hacen principalmente. De hecho, una de las cosas que odio de Fortran es codificar el código en un intento de "optimizarlo", generalmente sin un propósito significativo.
Por lo general, cuando las aplicaciones tienen problemas de rendimiento, se debe a un diseño derrochador. En estos días, nunca recomendaría el ensamblador para el rendimiento a menos que la aplicación general ya se haya ajustado a una pulgada de su vida útil, todavía no era lo suficientemente rápida y pasaba todo su tiempo en bucles internos estrechos.
Agregado: He visto muchas aplicaciones escritas en lenguaje ensamblador, y la principal ventaja de velocidad sobre un lenguaje como C, Pascal, Fortran, etc. fue porque el programador fue mucho más cuidadoso al codificar en ensamblador. Él o ella va a escribir aproximadamente 100 líneas de código por día, independientemente del idioma, y en un lenguaje de compilación que será igual a 3 o 400 instrucciones.
fuente
Algunos ejemplos de mi experiencia:
Acceso a instrucciones que no son accesibles desde C. Por ejemplo, muchas arquitecturas (como x86-64, IA-64, DEC Alpha y 64 bits MIPS o PowerPC) admiten una multiplicación de 64 bits por 64 bits que produce un resultado de 128 bits. GCC agregó recientemente una extensión que proporciona acceso a dichas instrucciones, pero antes de que se requiriera ese ensamblaje. Y el acceso a esta instrucción puede marcar una gran diferencia en las CPU de 64 bits al implementar algo como RSA, a veces tanto como un factor de mejora en el rendimiento.
Acceso a banderas específicas de la CPU. La que me ha mordido mucho es la bandera de acarreo; al hacer una adición de precisión múltiple, si no tiene acceso al bit de transporte de la CPU, debe comparar el resultado para ver si se desbordó, lo que requiere de 3 a 5 instrucciones más por miembro; y lo que es peor, que son bastante seriales en términos de acceso a datos, lo que mata el rendimiento en los procesadores superescalares modernos. Cuando se procesan miles de estos enteros en una fila, poder usar addc es una gran victoria (también hay problemas superescalares con la contención en el bit de acarreo, pero las CPU modernas lo manejan bastante bien).
SIMD Incluso los compiladores de autovectorización solo pueden hacer casos relativamente simples, por lo que, si desea un buen rendimiento SIMD, desafortunadamente a menudo es necesario escribir el código directamente. Por supuesto, puede usar intrínsecos en lugar de ensamblado, pero una vez que está en el nivel intrínseco, básicamente está escribiendo ensamblaje de todos modos, solo usando el compilador como un asignador de registros y (nominalmente) programador de instrucciones. (Tiendo a usar intrínsecos para SIMD simplemente porque el compilador puede generar los prólogos de funciones y otras cosas para mí, así que puedo usar el mismo código en Linux, OS X y Windows sin tener que lidiar con problemas ABI como convenciones de llamadas de funciones, pero otros que los intrínsecos SSE realmente no son muy agradables, los Altivec parecen mejores, aunque no tengo mucha experiencia con ellos).corrección de errores AES o SIMD de bits de corte : uno podría imaginar un compilador que pudiera analizar algoritmos y generar dicho código, pero me parece que un compilador tan inteligente está al menos a 30 años de existir (en el mejor de los casos).
Por otro lado, las máquinas multinúcleo y los sistemas distribuidos han cambiado muchas de las mayores ganancias de rendimiento en la otra dirección: obtenga una velocidad adicional del 20% al escribir sus bucles internos en el ensamblaje, o 300% al ejecutarlos en múltiples núcleos, o 10000% por ejecutándolos en un grupo de máquinas. Y, por supuesto, las optimizaciones de alto nivel (cosas como futuros, memorización, etc.) a menudo son mucho más fáciles de hacer en un lenguaje de nivel superior como ML o Scala que C o asm, y a menudo pueden proporcionar una ganancia de rendimiento mucho mayor. Entonces, como siempre, hay que hacer concesiones.
fuente
Bucles estrechos, como cuando se juega con imágenes, ya que una imagen puede costar millones de píxeles. Sentarse y descubrir cómo hacer un mejor uso del número limitado de registros del procesador puede marcar la diferencia. Aquí hay una muestra de la vida real:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Entonces, a menudo los procesadores tienen algunas instrucciones esotéricas que son demasiado especializadas para que un compilador las moleste, pero en ocasiones un programador ensamblador puede hacer un buen uso de ellas. Tome la instrucción XLAT por ejemplo. ¡Realmente genial si necesita hacer búsquedas de tabla en un bucle y la tabla está limitada a 256 bytes!
Actualizado: ¡Oh, solo piense en lo que es más crucial cuando hablamos de bucles en general: el compilador a menudo no tiene idea de cuántas iteraciones será el caso común! Solo el programador sabe que un bucle se repetirá MUCHAS veces y que, por lo tanto, será beneficioso prepararse para el bucle con algo de trabajo adicional, o si se repetirá tan pocas veces que la configuración realmente llevará más tiempo que las iteraciones. esperado.
fuente
Más a menudo de lo que piensa, C necesita hacer cosas que parecen innecesarias desde el punto de vista del codificador de la Asamblea solo porque los estándares de C lo dicen.
Promoción de enteros, por ejemplo. Si desea cambiar una variable char en C, generalmente se esperaría que el código hiciera precisamente eso, un cambio de un solo bit.
Sin embargo, los estándares obligan al compilador a hacer una extensión de señal a int antes del cambio y truncar el resultado a char después, lo que podría complicar el código dependiendo de la arquitectura del procesador de destino.
fuente
En realidad, no sabe si su código C bien escrito es realmente rápido si no ha analizado el desmontaje de lo que produce el compilador. Muchas veces lo miras y ves que "bien escrito" era subjetivo.
Por lo tanto, no es necesario escribir en ensamblador para obtener el código más rápido, pero ciertamente vale la pena conocer el ensamblador por la misma razón.
fuente
He leído todas las respuestas (más de 30) y no encontré una razón simple: el ensamblador es más rápido que C si ha leído y practicado el Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 , entonces la razón por la cual el ensamblaje puede ser más lento es que las personas que escriben un ensamblaje tan lento no leyeron el Manual de optimización .
En los viejos tiempos de Intel 80286, cada instrucción se ejecutaba con un conteo fijo de ciclos de CPU, pero desde Pentium Pro, lanzado en 1995, los procesadores Intel se convirtieron en superescalares, utilizando la canalización compleja: ejecución fuera de orden y cambio de nombre de registro. Antes de eso, en Pentium, producido en 1993, había tuberías U y V: líneas de tubería doble que podían ejecutar dos instrucciones simples en un ciclo de reloj si no dependían entre sí; pero esto no fue nada comparado con lo que es Ejecución fuera de orden y cambio de nombre de registro apareció en Pentium Pro, y casi no se modificó en la actualidad.
Para explicar en pocas palabras, el código más rápido es donde las instrucciones no dependen de resultados anteriores, por ejemplo, siempre debe borrar registros completos (por movzx) o usar
add rax, 1
en su lugar oinc rax
eliminar la dependencia del estado anterior de las banderas, etc.Puede leer más sobre Ejecución fuera de orden y cambio de nombre de registro si el tiempo lo permite, hay mucha información disponible en Internet.
También hay otros problemas importantes como la predicción de sucursales, el número de unidades de carga y almacenamiento, el número de puertas que ejecutan micro-operaciones, etc., pero lo más importante a considerar es la Ejecución fuera de orden.
La mayoría de las personas simplemente no son conscientes de la Ejecución fuera de orden, por lo que escriben sus programas de ensamblaje como para 80286, esperando que su instrucción tarde un tiempo fijo en ejecutarse independientemente del contexto; mientras que los compiladores de C están al tanto de la ejecución fuera de orden y generan el código correctamente. Es por eso que el código de personas tan inconscientes es más lento, pero si se da cuenta, su código será más rápido.
fuente
Creo que el caso general cuando el ensamblador es más rápido es cuando un programador de ensamblaje inteligente mira la salida del compilador y dice "esta es una ruta crítica para el rendimiento y puedo escribir esto para que sea más eficiente" y luego esa persona ajusta ese ensamblador o lo reescribe desde cero
fuente
Todo depende de tu carga de trabajo.
Para las operaciones del día a día, C y C ++ están bien, pero hay ciertas cargas de trabajo (cualquier transformación que involucre video (compresión, descompresión, efectos de imagen, etc.)) que prácticamente requieren que el ensamblaje sea eficiente.
También suelen implicar el uso de extensiones de chipset específicas de la CPU (MME / MMX / SSE / lo que sea) que se ajustan para ese tipo de operaciones.
fuente
Tengo una operación de transposición de bits que debe hacerse, en 192 o 256 bits cada interrupción, que ocurre cada 50 microsegundos.
Sucede por un mapa fijo (restricciones de hardware). Usando C, tomó alrededor de 10 microsegundos para hacer. Cuando traduje esto a Assembler, teniendo en cuenta las características específicas de este mapa, el almacenamiento en caché de registros específicos y el uso de operaciones orientadas a bits; tardó menos de 3.5 microsegundos en funcionar.
fuente
Podría valer la pena mirar Optimizando la inmutable y la pureza por Walter Bright , no es una prueba perfilada, pero le muestra un buen ejemplo de una diferencia entre ASM generado a mano y generado por el compilador. Walter Bright escribe compiladores de optimización para que valga la pena mirar sus otras publicaciones en el blog.
fuente
El tutorial de ensamblaje de Linux , hace esta pregunta y ofrece los pros y los contras de usar el ensamblaje.
fuente
La respuesta simple ... Alguien que conoce bien el ensamblaje (también conocido como la referencia a su lado y aprovecha cada pequeño caché del procesador y función de canalización, etc.) tiene la capacidad de producir código mucho más rápido que cualquier compilador.
Sin embargo, la diferencia en estos días simplemente no importa en la aplicación típica.
fuente
Una de las posibilidades de la versión CP / M-86 de PolyPascal (hermano de Turbo Pascal) era reemplazar la instalación "use-bios-to-output-characters-to-the-screen" con una rutina de lenguaje de máquina que en esencia se le dio la x, yy, y la cadena para poner allí.
¡Esto permitió actualizar la pantalla mucho, mucho más rápido que antes!
Había espacio en el binario para incrustar código de máquina (unos pocos cientos de bytes) y también había otras cosas allí, por lo que era esencial exprimir lo más posible.
Resulta que, dado que la pantalla era 80x25, ambas coordenadas podían caber en un byte cada una, por lo que ambas podían caber en una palabra de dos bytes. Esto permitió hacer los cálculos necesarios en menos bytes, ya que una sola suma podría manipular ambos valores simultáneamente.
Que yo sepa, no hay compiladores de C que puedan fusionar múltiples valores en un registro, hacer instrucciones SIMD en ellos y dividirlos nuevamente más tarde (y no creo que las instrucciones de la máquina sean más cortas de todos modos).
fuente
Uno de los fragmentos de ensamblaje más famosos es el bucle de mapeo de texturas de Michael Abrash ( expandido en detalle aquí ):
Hoy en día, la mayoría de los compiladores expresan instrucciones específicas de CPU avanzadas como intrínsecas, es decir, funciones que se compilan a la instrucción real. MS Visual C ++ admite intrínsecos para MMX, SSE, SSE2, SSE3 y SSE4, por lo que debe preocuparse menos por desplegarse en el ensamblaje para aprovechar las instrucciones específicas de la plataforma. Visual C ++ también puede aprovechar la arquitectura real a la que apunta con la configuración apropiada / ARCH.
fuente
Dado el programador correcto, los programas Assembler siempre se pueden hacer más rápido que sus contrapartes C (al menos marginalmente). Sería difícil crear un programa en C donde no pudieras sacar al menos una instrucción del ensamblador.
fuente
http://cr.yp.to/qhasm.html tiene muchos ejemplos.
fuente
gcc se ha convertido en un compilador ampliamente utilizado. Sus optimizaciones en general no son tan buenas. Mucho mejor que el ensamblador de escritura de programador promedio, pero para un rendimiento real, no es tan bueno. Hay compiladores que son simplemente increíbles en el código que producen. Por lo tanto, como respuesta general, habrá muchos lugares donde puede ir a la salida del compilador y ajustar el ensamblador para obtener un rendimiento, y / o simplemente volver a escribir la rutina desde cero.
fuente
Longpoke, solo hay una limitación: el tiempo. Cuando no tiene los recursos para optimizar cada cambio en el código y dedicar su tiempo a asignar registros, optimizar pocos derrames y, lo que no, el compilador ganará cada vez. Usted hace su modificación al código, recompila y mide. Repita si es necesario.
Además, puedes hacer mucho en el lado de alto nivel. Además, inspeccionar el ensamblaje resultante puede dar la IMPRESIÓN de que el código es una mierda, pero en la práctica se ejecutará más rápido de lo que cree que sería más rápido. Ejemplo:
int y = datos [i]; // haz algunas cosas aquí ... call_function (y, ...);
El compilador leerá los datos, los empujará a la pila (derrame) y luego los leerá de la pila y los pasará como argumento. Suena mierda? En realidad, podría ser una compensación de latencia muy efectiva y resultar en un tiempo de ejecución más rápido.
// versión optimizada call_function (datos [i], ...); // no tan optimizado después de todo ...
La idea con la versión optimizada era que hemos reducido la presión de registro y evitamos el derrame. Pero en verdad, ¡la versión "de mierda" fue más rápida!
Mirando el código de ensamblaje, solo mirando las instrucciones y concluyendo: más instrucciones, más lento, sería un error de juicio.
Lo que hay que prestar atención es: muchos expertos en ensamblaje piensan que saben mucho, pero saben muy poco. Las reglas también cambian de arquitectura a siguiente. No hay un código x86 de bala de plata, por ejemplo, que siempre es el más rápido. En estos días es mejor seguir las reglas generales:
Además, confiar demasiado en el compilador que transforma mágicamente el código C / C ++ mal pensado en código "teóricamente óptimo" es una ilusión. Debe conocer el compilador y la cadena de herramientas que utiliza si le preocupa el "rendimiento" en este nivel bajo.
Los compiladores en C / C ++ generalmente no son muy buenos para reordenar sub-expresiones porque las funciones tienen efectos secundarios, para empezar. Los lenguajes funcionales no sufren esta advertencia pero no se ajustan bien al ecosistema actual. Hay opciones de compilación para permitir reglas de precisión relajadas que permiten que el compilador / enlazador / generador de código cambie el orden de las operaciones.
Este tema es un poco sin salida; para la mayoría no es relevante, y el resto, ya saben lo que están haciendo de todos modos.
Todo se reduce a esto: "entender lo que estás haciendo", es un poco diferente de saber lo que estás haciendo.
fuente