Pregunta

¿Cuál es la forma más rápida que conoces de convertir un número de punto flotante a un int en una CPU x86?Preferiblemente en C o ensamblaje (que puede estar alineado en C) para cualquier combinación de lo siguiente:

  • Flotante de 32/64/80 bits -> Entero de 32/64 bits

Estoy buscando alguna técnica que sea más rápida que simplemente dejar que el compilador lo haga.

¿Fue útil?

Solución

Depende de si desea una conversión truncada o redondeada y con qué precisión.De forma predeterminada, C realizará una conversión truncada cuando pase de float a int.Hay instrucciones de FPU que lo hacen, pero no es una conversión ANSI C y existen importantes advertencias al usarlo (como conocer el estado de redondeo de FPU).Dado que la respuesta a tu problema es bastante compleja y depende de algunas variables que no has expresado, te recomiendo este artículo sobre el tema:

http://www.stereopsis.com/FPU.html

Otros consejos

La conversión empaquetada usando SSE es, con diferencia, el método más rápido, ya que puede convertir varios valores en la misma instrucción. ffmpeg tiene mucho ensamblaje para esto (principalmente para convertir la salida decodificada de audio a muestras enteras);compruébalo para ver algunos ejemplos.

Un truco comúnmente utilizado para código x86/x87 simple es forzar que la parte mantisa del flotante represente el int.Sigue la versión de 32 bits.

La versión de 64 bits es analógica.La versión de Lua publicada anteriormente es más rápida, pero se basa en el truncamiento de doble a un resultado de 32 bits, por lo que requiere que la unidad x87 esté configurada con doble precisión y no se puede adaptar para la conversión int de doble a 64 bits.

Lo bueno de este código es que es completamente portátil para todas las plataformas que cumplen con IEEE 754; la única suposición es que el modo de redondeo de punto flotante está configurado en el más cercano.Nota:Portátil en el sentido de que compila y funciona.Las plataformas distintas a x86 normalmente no se benefician mucho de esta técnica, en todo caso.

static const float Snapper=3<<22;

union UFloatInt {
 int i;
 float f;
};

/** by Vlad Kaipetsky
portable assuming FP24 set to nearest rounding mode
efficient on x86 platform
*/
inline int toInt( float fval )
{
  Assert( fabs(fval)<=0x003fffff ); // only 23 bit values handled
  UFloatInt &fi = *(UFloatInt *)&fval;
  fi.f += Snapper;
  return ( (fi.i)&0x007fffff ) - 0x00400000;
}

Si puede garantizar que la CPU que ejecuta su código sea compatible con SSE3 (incluso Pentium 5 lo es, JBB), puede permitir que el compilador use su instrucción FISTTP (es decir,-msse3 para gcc).Parece hacer lo que siempre debería haberse hecho:

http://software.intel.com/en-us/articles/how-to-implement-the-fisttp-streaming-simd-extensions-3-instruction/

Tenga en cuenta que FISTTP es diferente de FISTP (que tiene sus problemas y provoca la lentitud).Viene como parte de SSE3 pero en realidad es (el único) refinamiento del lado X87.

De todos modos, otras CPU que no sean X86 probablemente harían la conversión bien.:)

Procesadores con soporte SSE3

Hay una instrucción para convertir un punto flotante en un int en ensamblador:Utilice la instrucción FISTP.Extrae el valor de la pila de punto flotante, lo convierte en un número entero y luego lo almacena en la dirección especificada.No creo que haya una forma más rápida (a menos que utilice conjuntos de instrucciones extendidos como MMX o SSE, con los que no estoy familiarizado).

Otra instrucción, FIST, deja el valor en la pila FP, pero no estoy seguro de que funcione con destinos de cuatro palabras.

La base del código Lua tiene el siguiente fragmento para hacer esto (consulte src/luaconf.h desde www.lua.org).Si encuentra (SO encuentra) una forma más rápida, estoy seguro de que estarán encantados.

Oh, lua_Number significa doble.:)

/*
@@ lua_number2int is a macro to convert lua_Number to int.
@@ lua_number2integer is a macro to convert lua_Number to lua_Integer.
** CHANGE them if you know a faster way to convert a lua_Number to
** int (with any rounding method and without throwing errors) in your
** system. In Pentium machines, a naive typecast from double to int
** in C is extremely slow, so any alternative is worth trying.
*/

/* On a Pentium, resort to a trick */
#if defined(LUA_NUMBER_DOUBLE) && !defined(LUA_ANSI) && !defined(__SSE2__) && \
    (defined(__i386) || defined (_M_IX86) || defined(__i386__))

/* On a Microsoft compiler, use assembler */
#if defined(_MSC_VER)

#define lua_number2int(i,d)   __asm fld d   __asm fistp i
#define lua_number2integer(i,n)     lua_number2int(i, n)

/* the next trick should work on any Pentium, but sometimes clashes
   with a DirectX idiosyncrasy */
#else

union luai_Cast { double l_d; long l_l; };
#define lua_number2int(i,d) \
  { volatile union luai_Cast u; u.l_d = (d) + 6755399441055744.0; (i) = u.l_l; }
#define lua_number2integer(i,n)     lua_number2int(i, n)

#endif

/* this option always works, but may be slow */
#else
#define lua_number2int(i,d) ((i)=(int)(d))
#define lua_number2integer(i,d) ((i)=(lua_Integer)(d))

#endif

Supongo que se requiere truncamiento, igual que si se escribe i = (int)f Cª".

Si tienes SSE3, puedes usar:

int convert(float x)
{
    int n;
    __asm {
        fld x
        fisttp n // the extra 't' means truncate
    }
    return n;
}

Alternativamente, con SSE2 (o en x64 donde el ensamblaje en línea podría no estar disponible), puedes usar casi tan rápido:

#include <xmmintrin.h>
int convert(float x)
{
    return _mm_cvtt_ss2si(_mm_load_ss(&x)); // extra 't' means truncate
}

En computadoras más antiguas existe una opción para configurar el modo de redondeo manualmente y realizar la conversión usando el método normal. fistp instrucción.Esto probablemente solo funcionará para matrices de flotantes; de lo contrario, se debe tener cuidado de no utilizar ninguna construcción que haga que el compilador cambie el modo de redondeo (como la conversión).Se hace así:

void Set_Trunc()
{
    // cw is a 16-bit register [_ _ _ ic rc1 rc0 pc1 pc0 iem _ pm um om zm dm im]
    __asm {
        push ax // use stack to store the control word
        fnstcw word ptr [esp]
        fwait // needed to make sure the control word is there
        mov ax, word ptr [esp] // or pop ax ...
        or ax, 0xc00 // set both rc bits (alternately "or ah, 0xc")
        mov word ptr [esp], ax // ... and push ax
        fldcw word ptr [esp]
        pop ax
    }
}

void convertArray(int *dest, const float *src, int n)
{
    Set_Trunc();
    __asm {
        mov eax, src
        mov edx, dest
        mov ecx, n // load loop variables

        cmp ecx, 0
        je bottom // handle zero-length arrays

    top:
        fld dword ptr [eax]
        fistp dword ptr [edx]
        loop top // decrement ecx, jump to top
    bottom:
    }
}

Tenga en cuenta que el ensamblado en línea solo funciona con los compiladores Visual Studio de Microsoft (y tal vez con Borland), tendría que reescribirse en el ensamblado GNU para poder compilarlo con gcc.Sin embargo, la solución SSE2 con elementos intrínsecos debería ser bastante portátil.

Otros modos de redondeo son posibles mediante diferentes intrínsecos de SSE2 o configurando manualmente la palabra de control de la FPU en un modo de redondeo diferente.

Si realmente le importa la velocidad de esto, asegúrese de que su compilador esté generando la instrucción FIST.En MSVC puedes hacer esto con /QIfist, vea esta descripción general de MSDN

También puede considerar el uso de elementos intrínsecos de SSE para que hagan el trabajo por usted; consulte este artículo de Intel: http://softwarecommunity.intel.com/articles/eng/2076.htm

Dado que MS nos excluye del ensamblaje en línea en X64 y nos obliga a usar elementos intrínsecos, busqué cuál usar. documento de MSDN da _mm_cvtsd_si64x con un ejemplo.

El ejemplo funciona, pero es terriblemente ineficiente, ya que utiliza una carga no alineada de 2 dobles, donde solo necesitamos una carga, eliminando así el requisito de alineación adicional.Entonces se producen muchas cargas y recargas innecesarias, pero se pueden eliminar de la siguiente manera:

 #include <intrin.h>
 #pragma intrinsic(_mm_cvtsd_si64x)
 long long _inline double2int(const double &d)
 {
     return _mm_cvtsd_si64x(*(__m128d*)&d);
 }

Resultado:

        i=double2int(d);
000000013F651085  cvtsd2si    rax,mmword ptr [rsp+38h]  
000000013F65108C  mov         qword ptr [rsp+28h],rax  

El modo de redondeo se puede configurar sin montaje en línea, p.

    _control87(_RC_NEAR,_MCW_RC);

donde redondear al más cercano es el valor predeterminado (de todos modos).

Supongo que la pregunta de si se debe configurar el modo de redondeo en cada llamada o asumir que se restaurará (libras de terceros) tendrá que ser respondida por la experiencia.tendrás que incluir float.h para _control87() y constantes relacionadas.

Y no, esto no funcionará en 32 bits, así que sigue usando la instrucción FISTP:

_asm fld d
_asm fistp i

Generalmente, puede confiar en que el compilador será eficiente y correcto.Por lo general, no se gana nada implementando sus propias funciones para algo que ya existe en el compilador.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top