Un método rápido para redondear un doble a un int de 32 bits explicado

169

Al leer el código fuente de Lua , noté que Lua usa a macropara redondear doublea 32 bits int. Extraje el macro, y se ve así:

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

Aquí ENDIANLOCse define como endianness , 0para little endian, 1para big endian. Lua maneja con cuidado el endianness. trepresenta el tipo entero, como into unsigned int.

Investigué un poco y hay un formato más simple macroque usa el mismo pensamiento:

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

O en un estilo C ++:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

Este truco puede funcionar en cualquier máquina que use IEEE 754 (lo que significa que prácticamente todas las máquinas actuales). Funciona tanto para números positivos como negativos, y el redondeo sigue la Regla del banquero . (Esto no es sorprendente, ya que sigue a IEEE 754.)

Escribí un pequeño programa para probarlo:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

Y genera -12345679, como se esperaba.

Me gustaría entrar en detalles sobre cómo funciona este truco macro. El número mágico 6755399441055744.0es en realidad 2^51 + 2^52, o 1.5 * 2^52, y 1.5en binario se puede representar como 1.1. Cuando se agrega cualquier número entero de 32 bits a este número mágico, bueno, estoy perdido desde aquí. ¿Cómo funciona este truco?

PD: Esto está en el código fuente de Lua, Llimits.h .

ACTUALIZACIÓN :

  1. Como señala @Mysticial, este método no se limita a 32 bits int, también se puede ampliar a 64 bits intsiempre que el número esté en el rango de 2 ^ 52. ( macroNecesita algunas modificaciones).
  2. Algunos materiales dicen que este método no se puede usar en Direct3D .
  3. Al trabajar con el ensamblador de Microsoft para x86, hay una macroescritura aún más rápida assembly(esto también se extrae de la fuente Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Hay un número mágico similar para un número de precisión simple: 1.5 * 2 ^23

Yu Hao
fuente
3
"rápido" en comparación con qué?
Cory Nelson
3
@CoryNelson Rápido en comparación con un elenco simple. Este método, cuando se implementa correctamente (con intrínsecos SSE) es literalmente cien veces más rápido que un lanzamiento. (que invoca una llamada de función desagradable a un código de conversión bastante costoso)
Mysticial
2
Correcto, puedo ver que es más rápido que ftoi. Pero si estás hablando de SSE, ¿por qué no usar solo la instrucción individual CVTTSD2SI?
Cory Nelson
3
@tmyklebu Muchos de los casos de uso que se double -> int64encuentran están dentro del 2^52rango. Estos son particularmente comunes cuando se realizan convoluciones enteras utilizando FFT de punto flotante.
Mysticial
77
@MSalters No necesariamente es cierto. Un reparto debe cumplir con las especificaciones del lenguaje, incluido el manejo adecuado de los casos de desbordamiento y NAN. (o lo que especifique el compilador en el caso IB o UB) Estas comprobaciones tienden a ser muy caras. El truco mencionado en esta pregunta ignora por completo estos casos de esquina. Entonces, si desea la velocidad y a su aplicación no le importa (o nunca encuentra) tales casos de esquina, entonces este truco es perfectamente apropiado.
Mysticial

Respuestas:

161

A doublese representa así:

doble representación

y puede verse como dos enteros de 32 bits; ahora, el inttomado en todas las versiones de su código (suponiendo que sea de 32 bits int) es el que está a la derecha en la figura, por lo que lo que está haciendo al final es tomar los 32 bits más bajos de mantisa.


Ahora, al número mágico; como usted dijo correctamente, 6755399441055744 es 2 ^ 51 + 2 ^ 52; agregar tal número obliga doublea entrar en el "rango dulce" entre 2 ^ 52 y 2 ^ 53, que, como explica Wikipedia aquí , tiene una propiedad interesante:

Entre 2 52 = 4,503,599,627,370,496 y 2 53 = 9,007,199,254,740,992 los números representables son exactamente los enteros

Esto se deduce del hecho de que la mantisa tiene 52 bits de ancho.

El otro hecho interesante sobre la adición de 2 51 +2 52 es que afecta a la mantisa solo en los dos bits más altos, que de todos modos se descartan, ya que solo estamos tomando sus 32 bits más bajos.


Por último, pero no menos importante: el signo.

El punto flotante IEEE 754 usa una representación de magnitud y signo, mientras que los enteros en máquinas "normales" usan aritmética de complemento a 2; ¿Cómo se maneja esto aquí?

Hablamos solo de enteros positivos; ahora supongamos que estamos tratando con un número negativo en el rango representable por un bit de 32 bits int, por lo que es menor (en valor absoluto) que (-2 ^ 31 + 1); llámalo -a. Tal número obviamente se hace positivo al sumar el número mágico, y el valor resultante es 2 52 +2 51 + (- a).

Ahora, ¿qué obtenemos si interpretamos la mantisa en la representación del complemento a 2? Debe ser el resultado de la suma del complemento a 2 de (2 52 +2 51 ) y (-a). Nuevamente, el primer término afecta solo a los dos bits superiores, lo que queda en los bits 0 ~ 50 es la representación del complemento a 2 de (-a) (nuevamente, menos los dos bits superiores).

Dado que la reducción del número de complemento de 2 a un ancho menor se realiza cortando los bits adicionales a la izquierda, tomar los 32 bits inferiores nos da correctamente (-a) en 32 bits, aritmética de complemento de 2.

Matteo Italia
fuente
"" "El otro hecho interesante sobre la adición de 2 ^ 51 + 2 ^ 52 es que afecta a la mantisa solo en los dos bits más altos, que se descartan de todos modos, ya que estamos tomando solo sus 32 bits más bajos" "" ¿Qué es eso? ¡Agregar esto puede cambiar toda la mantisa!
YvesgereY
@John: por supuesto, el objetivo de agregarlos es forzar que el valor esté en ese rango, lo que obviamente puede resultar en cambiar la mantisa (entre las otras cosas) con respecto al valor original. Lo que estaba diciendo aquí es que, una vez que está en ese rango, los únicos bits que difieren del entero de 53 bits correspondiente son los bits 51 y 52, que de todos modos se descartan.
Matteo Italia
2
Para aquellos a quienes les gustaría convertirse int64_t, pueden hacerlo desplazando la mantisa a la izquierda y luego a la derecha 13 bits. Esto borrará el exponente y los dos bits del número 'mágico', pero mantendrá y propagará el signo a todo el entero con signo de 64 bits. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda