¿Cuándo es el ensamblaje más rápido que C?

475

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.

Adam Bellaire
fuente
17
En realidad, es bastante trivial mejorar el código compilado. Cualquier persona con un conocimiento sólido del lenguaje ensamblador y C puede ver esto examinando el código generado. Cualquiera fácil es el primer precipicio de rendimiento del que se cae cuando se queda sin registros desechables en la versión compilada. En promedio, el compilador funcionará mucho mejor que un humano para un proyecto grande, pero no es difícil en un proyecto de tamaño decente encontrar problemas de rendimiento en el código compilado.
old_timer
14
En realidad, la respuesta corta es: el ensamblador siempre es más rápido o igual a la velocidad de C. La razón es que puedes tener ensamblaje sin C, pero no puedes tener C sin ensamblaje (en la forma binaria, que nosotros en el antiguo días llamados "código de máquina"). Dicho esto, la respuesta larga es: los compiladores de C son bastante buenos para optimizar y "pensar" en cosas en las que generalmente no piensas, por lo que realmente depende de tus habilidades, pero normalmente siempre puedes vencer al compilador de C; sigue siendo solo un software que no puede pensar y obtener ideas. También puede escribir ensamblador portátil si usa macros y es paciente.
11
Estoy totalmente en desacuerdo con que las respuestas a esta pregunta deben estar "basadas en la opinión", pueden ser bastante objetivas, no es algo así como tratar de comparar el rendimiento de los idiomas favoritos de las mascotas, para lo cual cada uno tendrá puntos fuertes y desventajas. Se trata de comprender hasta dónde nos pueden llevar los compiladores y desde qué punto es mejor hacerse cargo.
jsbueno
21
Al principio de mi carrera, escribía muchos ensambladores de C y mainframe en una compañía de software. Uno de mis compañeros era lo que yo llamaría un "purista del ensamblador" (todo tenía que ser ensamblador), así que apuesto a que podría escribir una rutina dada que corriera más rápido en C de lo que podía escribir en el ensamblador. Gané. Pero para colmo, después de ganar, le dije que quería una segunda apuesta: que podía escribir algo más rápido en ensamblador que el programa C que lo venció en la apuesta anterior. También gané eso, demostrando que la mayor parte se reduce a la habilidad y habilidad del programador más que cualquier otra cosa.
Valerie R
3
A menos que su cerebro tenga una -O3bandera, probablemente sea mejor dejar la optimización para el compilador de C :-)
paxdiablo

Respuestas:

272

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


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:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

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:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

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 popcnto bit-exploración para encontrar el primer o el último bit activado . (POSIX tiene una ffs()función, pero su semántica no coincide con x86 bsf/ 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 popcntinstrucción (si está habilitado en el momento de la compilación), pero es mucho más confiable usarlo __builtin_popcnten GNU C, o en x86 si solo está apuntar hardware con SSE4.2: _mm_popcnt_u32desde<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, ntohlpuede compilar a bswap(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.

Nils Pipenbrinck
fuente
66
¿Qué tal cosas como {x = c% d; y = c / d;}, ¿son los compiladores lo suficientemente inteligentes como para hacer que un solo div o idiv?
Jens Björnhager
44
En realidad, un buen compilador produciría el código óptimo de la primera función. No es lo mejor hacer oscurecer el código fuente con intrínsecos o ensamblar en línea sin absolutamente ningún beneficio .
slacker
65
Hola Slacker, creo que nunca antes has tenido que trabajar en código de tiempo crítico ... el ensamblaje en línea puede hacer una * gran diferencia. También para el compilador, un intrínseco es lo mismo que la aritmética normal en C. Ese es el punto en intrínseco. Le permiten usar una característica de arquitectura sin tener que lidiar con los inconvenientes.
Nils Pipenbrinck
66
@slacker En realidad, el código aquí es bastante legible: el código en línea realiza una operación única, que es inmediatamente comprensible al leer la firma del método. El código se pierde lentamente en la lectura cuando se usa una instrucción oscura. Lo que importa aquí es que tenemos un método que solo realiza una operación claramente identificable, y esa es realmente la mejor manera de producir código legible para estas funciones atómicas. Por cierto, esto no es tan oscuro un pequeño comentario como / * (a * b) >> 16 * / no puedo explicarlo de inmediato.
Dereckson
55
Para ser justos, este es un ejemplo pobre, al menos hoy. Los compiladores de C han sido capaces de hacer una multiplicación de 32x32 -> 64 incluso si el lenguaje no lo ofrece directamente: reconocen que cuando lanzas argumentos de 32 bits a 64 bits y luego los multiplica, no es necesario hacer una multiplicación completa de 64 bits, pero que un 32x32 -> 64 funcionará bien. Verifiqué y todos los clang, gcc y MSVC en su versión actual hacen esto bien . Esto no es nuevo: recuerdo haber visto el resultado del compilador y notarlo hace una década.
BeeOnRope
143

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.

Peter Cordes
fuente
3
Sí, era un sistema monocromático de un bit, específicamente eran los bloques de imágenes monocromas en un Atari ST.
lilburne
16
¿El compilador de optimización compiló el programa original o su versión?
Thorbjørn Ravn Andersen
¿En qué procesador? En 8086, esperaría que el código óptimo para una rotación de 8x8 cargara DI con 16 bits de datos usando SI, repita, add di,di / adc al,al / add di,di / adc ah,ahetc. 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.
supercat
1
Realmente no puedo pensar en ninguna plataforma en la que un compilador pueda obtener un factor o dos de código óptimo para una rotación de 8x8.
Supercat
65

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):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

Y algunos números de mi PC que ejecutan una versión de lanzamiento predeterminada * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

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.

Skizz
fuente
20
O en GCC, puede desatar las manos del compilador en la optimización de coma flotante (siempre y cuando prometa no hacer nada con infinitos o NaNs) utilizando la bandera -ffast-math. Tienen un nivel de optimización, -Ofastque 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).
David Stone
2
Sí, los flotantes no son conmutativos, el compilador debe hacer EXACTAMENTE lo que escribió, básicamente lo que dijo @DavidStone.
Alec Teal
2
¿Intentaste con las matemáticas SSE? El rendimiento fue una de las razones por las que MS abandonó x87 por completo en x86_64 y el doble de 80 bits de largo en x86
phuclv
44
@Praxeolitic: FP add es conmutativo ( 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 una loopinstrucción sean una demostración increíble de asm rápido. loopaparentemente 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. Dos fstp resultsinsns al final claramente no son óptimos. Hacer estallar el resultado extra de la pila sería mejor hacerlo con una no tienda. Como el fstp st(0)IIRC.
Peter Cordes
2
@PeterCordes: una consecuencia interesante de hacer que la suma sea conmutativa es que mientras 0 + xyx + 0 son equivalentes entre sí, ninguno de los dos es siempre equivalente a x.
supercat
58

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.

Liedman
fuente
En general, esta afirmación es cierta. El compilador hace lo mejor para DWIW, pero en algunos casos extremos, el ensamblador de codificación manual hace el trabajo cuando el rendimiento en tiempo real es imprescindible.
spoulson
1
@Liedman: "puede intentar reordenar las instrucciones más rápido que un humano". OCaml es conocido por ser rápido y, sorprendentemente, su compilador de código nativo ocamloptomite 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.
Jon Harrop
1
Los compiladores modernos hacen mucho, y llevaría demasiado tiempo hacerlo a mano, pero no son perfectos. Busque los rastreadores de errores de gcc o llvm para buscar errores de "optimización perdida". Hay muchos. Además, al escribir en asm, puede aprovechar más fácilmente las condiciones previas como "esta entrada no puede ser negativa" que sería difícil de probar para un compilador.
Peter Cordes
48

En mi trabajo, hay tres razones para conocer y usar el ensamblaje. En orden de importancia:

  1. 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.

  2. 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í:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    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.

  3. 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.

zócalo
fuente
¿No enlosas tus bucles? :-)
Jon Harrop
1
@plinth: ¿qué quieres decir con "ciclos de raspado"?
lang2
@ lang2: significa deshacerse del mayor tiempo posible que haya pasado en el bucle interno, cualquier cosa que el compilador no haya podido extraer, lo que puede incluir el uso de álgebra para levantar una multiplicación de un bucle para agregarlo en el interior, etc.
zócalo
1
El mosaico de bucles parece ser innecesario si solo está haciendo un pase sobre los datos.
James M. Lay
@ JamesM.Lay: Si solo tocas cada elemento una vez, un mejor orden transversal puede darte una ubicación espacial. (por ejemplo, use todos los bytes de una línea de caché que tocó, en lugar de recorrer columnas de una matriz usando un elemento por línea de caché)
Peter Cordes,
42

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.

Nir
fuente
44
+1, a pesar de que la última oración no pertenece realmente a esta discusión, uno asumiría que el ensamblador entra en juego solo después de que se hayan realizado todas las posibles mejoras del algoritmo, etc.
mghie
18
@Matt: el ASM escrito a mano es a menudo mucho mejor en algunas de las pequeñas CPU con las que EE trabaja y que tiene un soporte de compilador de proveedores de mala calidad.
Zan Lynx
55
"¿Solo cuando se usan algunos conjuntos de instrucciones de propósito especial"? Probablemente nunca antes haya escrito un código asm optimizado a mano. Un conocimiento moderadamente íntimo de la arquitectura en la que está trabajando le brinda una buena oportunidad para generar un mejor código (tamaño y velocidad) que su compilador. Obviamente, como comentó @mghie, siempre comienzas a codificar los mejores algos que puedes encontrar para tu problema. Incluso para compiladores muy buenos, realmente tiene que escribir su código C de una manera que lleve al compilador al mejor código compilado. De lo contrario, el código generado será subóptimo.
ysap
2
@ysap: en computadoras reales (no pequeños chips integrados con poca potencia) en el uso en el mundo real, el código "óptimo" no será más rápido porque para cualquier conjunto de datos de gran tamaño, el rendimiento estará limitado por el acceso a la memoria y las fallas de página ( y si no tiene un conjunto de datos grande, esto será rápido de cualquier manera y no tiene sentido optimizarlo), esos días trabajo principalmente en C # (ni siquiera c) y las ganancias de rendimiento del administrador de memoria de compactación ponderar los gastos generales de la recolección de basura, compactación y compilación JIT.
Nir
44
+1 por afirmar que los compiladores (especialmente JIT) pueden hacer un mejor trabajo que los humanos, si están optimizados para el hardware en el que se ejecutan.
Sebastian
38

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:

  1. 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:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    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.

  2. Ciertas operaciones de desplazamiento de bits (rotación / transporte):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    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.

Jason S
fuente
23

¿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):

El lenguaje de programación C: un lenguaje que combina la flexibilidad del lenguaje ensamblador con el poder del lenguaje ensamblador.

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).

cletus
fuente
Hubo muchas personas que usaron el ensamblador ARM como el idioma de elección en las máquinas Acorn (principios de los 90). IIRC dijeron que el pequeño conjunto de instrucciones de risc lo hizo más fácil y divertido. Pero sospecho que es porque el compilador de C llegó tarde para Acorn, y el compilador de C ++ nunca se terminó.
Andrew M
3
"... porque no hay un estándar subjetivo para C." Te refieres a objetivo .
Thomas
@ AndrewM: Sí, escribí aplicaciones de lenguaje mixto en ensamblador BASIC y ARM durante aproximadamente 10 años. Aprendí C durante ese tiempo, pero no fue muy útil porque es tan engorroso como ensamblador y más lento. Norcroft hizo algunas optimizaciones increíbles, pero creo que el conjunto de instrucciones condicionales fue un problema para los compiladores de la época.
Jon Harrop
1
@AndrewM: bueno, en realidad ARM es una especie de RISC hecho al revés. Otros ISA RISC se diseñaron a partir de lo que usaría un compilador. ARM ISA parece haber sido diseñado a partir de lo que proporciona la CPU (palanca de cambios de barril, indicadores de condición → expongamos en cada instrucción).
ninjalj
16

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.

Aaron Digulla
fuente
El Amiga no tenía 16 MB de RAM de chip, más como 512 kB a 2 MB dependiendo del conjunto de chips. Además, muchos juegos de Amiga no funcionaban con CPU más rápidos debido a técnicas como las que usted describe.
bk1e
1
@ bk1e - Amiga produjo una gran variedad de modelos diferentes de computadoras, la Amiga 500 se envió con 512K de RAM extendida a 1Meg en mi caso. amigahistory.co.uk/amiedevsys.html es una amiga con 128Meg Ram
David Waters
@ bk1e: Estoy corregido. Mi memoria puede fallarme, pero ¿no se restringió la RAM del chip al primer espacio de direcciones de 24 bits (es decir, 16 MB)? ¿Y Fast fue mapeado por encima de eso?
Aaron Digulla
@Aaron Digulla: Wikipedia tiene más información sobre las distinciones entre el chip / RAM rápido / lento: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e
@ bk1e: Mi error. La CPU de 68k tenía solo 24 carriles de direcciones, por eso tenía 16MB en mi cabeza.
Aaron Digulla
15

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?

  1. Eliminar una característica existente
  2. Reduzca el tamaño de algunas o todas las características existentes, tal vez a costa del rendimiento.
  3. Defienda el cambio a un chip más grande con un costo más alto, un mayor consumo de energía y un factor de forma más grande.

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

  • trabajando en la función del sistema operativo de bajo nivel
  • Trabajando en un compilador.
  • Trabajando en un chip extremadamente limitado, sistema embebido, etc.

Recuerde comparar su ensamblaje con el compilador generado para ver cuál es más rápido / más pequeño / mejor.

David

David Waters
fuente
44
+1 por considerar aplicaciones integradas en chips pequeños. Demasiados ingenieros de software aquí no consideran embebido o piensan que eso significa un teléfono inteligente (32 bit, MB RAM, MB flash).
Martin
1
¡Las aplicaciones integradas en el tiempo son un gran ejemplo! A menudo hay instrucciones extrañas (incluso realmente simples como las de avr sbiy cbi) que los compiladores solían (y a veces todavía lo hacen) no aprovechar al máximo, debido a su conocimiento limitado del hardware.
Felixphew
15

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 es

int c;
for(c = 0; str[c] != '\0'; c++) {}

mientras está en ensamblaje puede acelerarlo considerablemente:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

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!

BlackBear
fuente
3
¿Cómo se compara esto con los de strchr.nfshost.com/optimized_strlen_function ?
ninjalj 05 de
@ninjalj: son lo mismo :) No pensé que se pudiera hacer de esta manera en C. Creo que se puede mejorar ligeramente
BlackBear
Todavía hay una operación AND a nivel de bit antes de cada comparación en el código C. Es posible que el compilador sea lo suficientemente inteligente como para reducir eso a comparaciones de byte alto y bajo, pero no apostaría dinero. En realidad, hay un algoritmo de bucle más rápido que se basa en la propiedad que (word & 0xFEFEFEFF) & (~word + 0x80808080)es cero si todos los bytes en la palabra no son cero.
user2310967
@MichaWiedenmann cierto, debería cargar bx después de comparar los dos caracteres en ax. Gracias
BlackBear
14

Las operaciones matriciales que utilizan instrucciones SIMD son probablemente más rápidas que el código generado por el compilador.

Mehrdad Afshari
fuente
Algunos compiladores (el VectorC, si no recuerdo mal) generan código SIMD, por lo que incluso eso probablemente ya no sea un argumento para usar el código ensamblador.
OregonGhost
Los compiladores crean un código compatible con SSE, por lo que ese argumento no es cierto
vartec
55
Para muchas de esas situaciones, puede usar intrínsecos SSE en lugar de ensamblar. Esto hará que su código sea más portátil (gcc visual c ++, 64bit, 32bit, etc.) y no tendrá que hacer la asignación de registros.
Laserallan
1
Claro que sí, pero la pregunta no preguntaba dónde debería usar el ensamblado en lugar de C. Dijo que cuando el compilador de C no genera un código mejor. Asumí una fuente C que no está usando llamadas SSE directas o ensamblaje en línea.
Mehrdad Afshari
99
Sin embargo, Mehrdad tiene razón. Hacer que SSE sea correcto es bastante difícil para el compilador e incluso en situaciones obvias (para humanos, es decir) la mayoría de los compiladores no lo emplean.
Konrad Rudolph
13

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.

Mike Dunlavey
fuente
8
+1: "Podrías desviarte de las convenciones de llamadas". Los compiladores de C / C ++ tienden a ser malos al devolver múltiples valores. A menudo usan la forma sret donde la pila de llamantes asigna un bloque contiguo para una estructura y le pasa una referencia para que la persona que llama la complete. Devolver múltiples valores en los registros es varias veces más rápido.
Jon Harrop
1
@ Jon: los compiladores de C / C ++ lo hacen muy bien cuando la función se alinea (las funciones no alineadas deben ajustarse a la ABI, esto no es una limitación de C y C ++ sino el modelo de enlace)
Ben Voigt
@BenVoigt: Aquí hay un contraejemplo flyingfrogblog.blogspot.co.uk/2012/04/…
Jon Harrop
2
No veo ninguna llamada de función en línea allí.
Ben Voigt
13

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.

Jack Lloyd
fuente
2
@Dennis, por eso escribí 'Por supuesto, puedes usar intrínsecos en lugar de ensamblado, pero una vez que estás en el nivel intrínseco básicamente estás escribiendo ensamblaje, simplemente usando el compilador como un asignador de registros y (nominalmente) programador de instrucciones'.
Jack Lloyd
Además, el código SIMD basado en intrínsecos tiende a ser menos legible que el mismo código escrito en el ensamblador: gran parte del código SIMD se basa en reinterpretaciones implícitas de los datos en los vectores, lo cual es una PITA que tiene que ver con los tipos de datos que proporciona el compilador intrínseco.
cmaster - reinstalar a monica el
10

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.

Dan Byström
fuente
3
La optimización dirigida por perfil proporciona al compilador información sobre la frecuencia con la que se usa un bucle.
Zan Lynx
10

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.

mfro
fuente
Los compiladores de calidad para micros pequeños han podido evitar durante años el procesamiento de las partes superiores de los valores en los casos en que hacerlo nunca podría afectar significativamente los resultados. Las reglas de promoción sí causan problemas, pero con mayor frecuencia en los casos en que un compilador no tiene forma de saber qué casos de esquina son y no son relevantes.
supercat
9

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.

diente afilado
fuente
2
"Por lo tanto, no es necesario escribir en ensamblador para obtener el código más rápido" Bueno, no he visto a un compilador hacer lo óptimo en cualquier caso que no sea trivial. Un humano experimentado puede hacerlo mejor que el compilador en prácticamente todos los casos. Por lo tanto, es absolutamente necesario escribir en ensamblador para obtener "el código más rápido".
cmaster - reinstalar a monica el
@cmaster En mi experiencia, el resultado del compilador es bueno, aleatorio. A veces es realmente bueno y óptimo y a veces es "cómo podría haberse emitido esta basura".
Sharptooth
9

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, 1en su lugar o inc raxeliminar 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.

Maxim Masiutin
fuente
8

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

Doug T.
fuente
7

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.

RestablecerMonica Larry Osterman
fuente
6

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.

SurDin
fuente
6

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.

James Brooks
fuente
5

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.

Longpoke
fuente
1
Olvidó decir "dado mucho tiempo y esfuerzo" y "creando una pesadilla de mantenimiento". Un colega mío estaba trabajando en la optimización de una sección crítica del código del sistema operativo, y trabajó en C mucho más que el ensamblaje, ya que le permitió investigar el impacto en el rendimiento de los cambios de alto nivel dentro de un plazo razonable.
Artelius
Estoy de acuerdo. A veces usa macros y scripts para generar código de ensamblaje para ahorrar tiempo y desarrollar rápidamente. La mayoría de los ensambladores en estos días tienen macros; si no, puede hacer un preprocesador de macro (simple) usando un script Perl (bastante simple RegEx).
Esta. Precisamente. El compilador para vencer a los expertos en dominios aún no se ha inventado.
cmaster - reinstalar a monica el
4

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).

Thorbjørn Ravn Andersen
fuente
4

Uno de los fragmentos de ensamblaje más famosos es el bucle de mapeo de texturas de Michael Abrash ( expandido en detalle aquí ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

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.

MSN
fuente
Mejor aún, esos intrínsecos SSE están especificados por Intel, por lo que en realidad son bastante portátiles.
James
4

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.

Bip bip
fuente
Esto sería un poco más correcto: "Sería difícil crear un programa C no trivial donde ..." Alternativamente, podría decir: "Sería difícil encontrar un programa C del mundo real donde ..." El punto es , hay bucles triviales para los que los compiladores producen una salida óptima. Sin embargo, buena respuesta.
cmaster - reinstalar a monica el
4

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.

viejo contador de tiempo
fuente
8
GCC realiza optimizaciones extremadamente inteligentes "independientes de la plataforma". Sin embargo, no es tan bueno para utilizar conjuntos de instrucciones particulares al máximo. Para un compilador tan portátil, hace un muy buen trabajo.
Artelius
2
convenido. Su portabilidad, los idiomas que entran y los objetivos que salen son increíbles. El hecho de que sea portátil puede obstaculizar y ser realmente bueno en un idioma u objetivo. Entonces, las oportunidades para que un ser humano lo haga mejor están ahí para una optimización particular en un objetivo específico.
old_timer
+1: GCC ciertamente no es competitivo para generar código rápido, pero no estoy seguro de que sea porque es portátil. LLVM es portátil y lo he visto generar código 4 veces más rápido que los GCC.
Jon Harrop
Prefiero GCC, ya que ha sido muy sólido durante muchos años, además está disponible para casi todas las plataformas que pueden ejecutar un compilador portátil moderno. Desafortunadamente, no he podido construir LLVM (Mac OS X / PPC), por lo que probablemente no pueda cambiarlo. Una de las cosas buenas de GCC es que si escribe código que se construye en GCC, lo más probable es que se mantenga cerca de los estándares, y estará seguro de que se puede construir para casi cualquier plataforma.
4

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:

  • la memoria es lenta
  • el caché es rápido
  • intenta usar mejor el caché
  • ¿Con qué frecuencia vas a extrañar? ¿Tienes una estrategia de compensación de latencia?
  • puede ejecutar 10-100 instrucciones ALU / FPU / SSE para una sola falta de caché
  • La arquitectura de la aplicación es importante.
  • .. pero no ayuda cuando el problema no está en la arquitectura

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.

cansado
fuente