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 popcnt
o 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 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 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.