¿Cuál es la forma más rápida de convertir float a int en x86?

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

  • Flotador 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 comstackdor lo haga.

Depende de si desea una conversión truncada o redondeada y con qué precisión. De forma predeterminada, C realizará una conversión de truncamiento 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 para su uso (como conocer el estado de redondeo FPU). Como la respuesta a su problema es bastante compleja y depende de algunas variables que no ha expresado, recomiendo este artículo sobre el tema:

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

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

Un truco comúnmente utilizado para el código x86 / x87 es forzar a la parte mantisa del flotante a representar 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 tanto, requiere que la unidad x87 se establezca en doble precisión y no se puede adaptar para una conversión 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 se establece más cerca. Nota: Portable en el sentido de que comstack y funciona. Las plataformas que no sean x86 usualmente no se benefician mucho de esta técnica, si es que lo hacen.

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 es compatible con SSE3 (incluso Pentium 5 es, JBB), puede permitir que el comstackdor use su instrucción FISTTP (es decir, -msse3 para gcc). Parece hacer algo así como 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, lo que provoca lentitud). Viene como parte de SSE3 pero en realidad es (el único) refinamiento del lado X87.

Otros CPUs entonces X86 probablemente harían la conversión bien, de todos modos. 🙂

Procesadores con soporte SSE3

Hay una instrucción para convertir un punto flotante a un int en el ensamblaje: use la instrucción FISTP. Muestra el valor de la stack de coma flotante, lo convierte en un número entero y luego lo almacena en la dirección especificada. No creo que haya una manera más rápida (a menos que uses conjuntos de instrucciones extendidas como MMX o SSE, con los que no estoy familiarizado).

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

La base de código Lua tiene el siguiente fragmento para hacer esto (verifique src / luaconf.h desde http://www.lua.org). Si encuentras (SO encuentra) una forma más rápida, estoy seguro de que estarían 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 

Si realmente te importa la velocidad de esto, asegúrate de que tu comstackdor esté generando la instrucción FIST. En MSVC puede hacer esto con / QIfist, consulte esta descripción general de MSDN

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

Como MS nos saca del ensamblaje en línea en X64 y nos obliga a usar intrínsecos, busqué cuál usar. MSDN doc da _mm_cvtsd_si64x con un ejemplo.

El ejemplo funciona, pero es terriblemente ineficiente, con una carga desalineada de 2 dobles, donde solo necesitamos una carga, por lo que eliminamos el requisito de alineación adicional. Entonces se producen muchas cargas innecesarias y recargas, pero se pueden eliminar de la siguiente manera:

  #include  #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 establecer sin ensamblaje en línea, por ejemplo

  _control87(_RC_NEAR,_MCW_RC); 

donde redondear al más cercano es por defecto (de todos modos).

La cuestión de si establecer el modo de redondeo en cada llamada o asumir que se restaurará (libs de terceros) tendrá que ser respondida por la experiencia, supongo. Deberá 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 

Supongo que se requiere truncamiento, igual que si se escribe i = (int)f en “C”.

Si tiene SSE3, puede 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), puede usar casi tan rápido:

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

En las computadoras más antiguas, hay una opción para configurar el modo de redondeo de forma manual y realizar la conversión utilizando la instrucción fistp ordinaria. Probablemente esto solo funcionará para matrices de flotadores, de lo contrario se debe tener cuidado de no utilizar ninguna construcción que haga que el comstackdor cambie el modo de redondeo (como el fundido). 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 ensamblaje en línea solo funciona con los comstackdores de Visual Studio de Microsoft (y tal vez con Borland), tendría que ser reescrito al ensamblado de GNU para poder comstackr con gcc. Sin embargo, la solución SSE2 con intrínsecos debería ser bastante portátil.

Otros modos de redondeo son posibles por diferentes intrínsecos de SSE2 o por ajuste manual de la palabra de control de FPU a un modo de redondeo diferente.

En general, puede confiar en que el comstackdor sea eficiente y correcto. Por lo general, no se gana nada ejecutando sus propias funciones para algo que ya existe en el comstackdor.