c++ x86 c++-cli x86-64 micro-optimization

c++ - ¿Qué tan caro es convertir entre int y doble?



x86 c++-cli (2)

A menudo veo el código que convierte de ints a doblots en tots a dots y de vuelta una vez más (a veces por buenas razones, a veces no), y se me ocurrió que esto parece ser un costo "oculto" en mi programa. Supongamos que el método de conversión es el truncamiento.

Entonces, ¿qué tan caro es? Estoy seguro de que varía según el hardware, así que supongamos que es un procesador Intel nuevo (Haswell, si lo desea, aunque tomaré cualquier cosa). Algunas métricas que me interesarían (aunque una buena respuesta no tiene que tener todas):

  1. # de instrucciones generadas
  2. # de ciclos utilizados
  3. Costo relativo comparado con operaciones aritméticas básicas.

También supondría que la forma en que experimentaría de manera más aguda el impacto de una conversión lenta sería con respecto al uso de energía en lugar de la velocidad de ejecución, dada la diferencia en cuántos cálculos podemos realizar cada segundo en relación con la cantidad de datos que realmente pueden llegar. en la CPU cada segundo.


Esto es lo que pude encontrar yo mismo, para x86-64 haciendo FP FP con SSE2 (no legado x87 donde cambiar el modo de redondeo para la semántica de truncamiento de C ++ era caro):

  1. Cuando miro el ensamblado generado desde clang y gcc, parece que el reparto int double , se reduce a una sola instrucción: cvttsd2si .

    De double a int es cvtsi2sd . ( cvtsi2sdl AT&T cvtsi2sd para cvtsi2sd con tamaño de operando de 32 bits).

    Con la auto-vectorización, obtenemos cvtdq2pd .

    Entonces supongo que la pregunta es: ¿cuál es el costo de esos ?

  2. Estas instrucciones cuestan aproximadamente lo mismo que un FP addsd y un movq xmm, r64 (fp <- integer) o movq r64, xmm (integer <- fp), porque se decodifican a 2 uops que en los mismos puertos, en mainstream ( Sandybridge / Haswell / Sklake) CPU de Intel.

    El Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 dice que el costo de la instrucción cvttsd2si es de 5 latencias (consulte el Apéndice C-16). cvtsi2sd , dependiendo de su arquitectura, tiene una latencia que varía desde 1 en Silvermont hasta más o menos 7-16 en otras arquitecturas.

    Las tablas de instrucciones de Agner Fog tienen números más precisos / sensibles, como la latencia de 5 ciclos para cvtsi2sd en Silvermont (con 1 por 2 de rendimiento de reloj), o 4c de latencia en Haswell, con una por rendimiento de reloj (si evita la dependencia en el registro de destino de fusionarse con la mitad superior anterior, como gcc suele hacer con pxor xmm0,xmm0 ).

    SIMD empacado - float a empacado - int es genial; solo uop. Pero convertir al double requiere un orden aleatorio para cambiar el tamaño del elemento. SIMD float / double <-> int64_t no existe hasta AVX512, pero se puede hacer manualmente con un rango limitado.

    El manual de Intel define la latencia como: "La cantidad de ciclos de reloj que se requieren para que el núcleo de ejecución complete la ejecución de todos los μops que forman una instrucción". Pero una definición más útil es la cantidad de relojes de una entrada que está lista hasta que la salida esté lista. El rendimiento es más importante que la latencia si hay suficiente paralelismo para que la ejecución fuera de orden haga su trabajo: ¿Qué consideraciones entran en la predicción de la latencia para las operaciones en los procesadores superscalar modernos y cómo puedo calcularlas manualmente? .

  3. El mismo manual de Intel dice que los costos de instrucción de add un entero 1 latencia y los costos de un entero entero 3 (Apéndice C-27). La FP addsd y mulsd ejecuta a 2 por rendimiento de reloj, con latencia de 4 ciclos, en Skylake. Lo mismo para las versiones SIMD, y para FMA, con vectores de 128 o 256 bits.

    En Haswell, addsd / addpd tiene solo 1 rendimiento por reloj, pero una latencia de 3 ciclos gracias a una unidad dedicada para agregar FP.

Entonces, la respuesta se reduce a:

1) Es hardware optimizado, y el compilador aprovecha la maquinaria de hardware.

2) Cuesta solo un poco más de lo que lo hace la multiplicación en términos del número de ciclos en una dirección y una cantidad altamente variable en la otra (según su arquitectura). Su costo no es ni libre ni absurdo, pero probablemente merece más atención dado lo fácil que es escribir código que incurre en el costo de una manera no obvia.


Por supuesto, este tipo de pregunta depende del hardware exacto e incluso del modo.

En x86 mi i7 cuando se usa en el modo de 32 bits con opciones predeterminadas ( gcc -m32 -O3 ) la conversión de int a double es bastante rápida, lo contrario es mucho más lento porque el estándar C exige una regla absurda (truncamiento de decimales) .

Esta forma de redondeo es mala tanto para las matemáticas como para el hardware y requiere que la FPU cambie a este modo especial de redondeo, realice el truncamiento y vuelva a una forma sana de redondeo.

Si necesita velocidad, la conversión fistp > int utilizando la simple instrucción fistp es más rápida y mucho mejor para los resultados de cómputo, pero requiere un poco de ensamblaje en línea.

inline int my_int(double x) { int r; asm ("fldl %1/n" "fistpl %0/n" :"=m"(r) :"m"(x)); return r; }

es más de 6 veces más rápido que el ingenuo x = (int)y; conversión (y no tiene un sesgo hacia 0).

Sin embargo, el mismo procesador, cuando se usa en el modo de 64 bits, no tiene problemas de velocidad y el uso del código fistp hace que el código se ejecute un poco más lento.

Al parecer, los tipos de hardware se dieron por vencidos e implementaron el algoritmo de redondeo incorrecto directamente en el hardware (por lo que el código de redondeo puede ejecutarse rápidamente).