Question

Quel est le moyen le plus rapide de convertir un nombre à virgule flottante en entier sur un processeur x86? De préférence en C ou en assembleur (qui peut être inséré en C) pour toute combinaison des éléments suivants:

  • float 32/64/80 bits - > Entier 32/64 bits

Je cherche une technique plus rapide que de laisser le compilateur le faire.

Était-ce utile?

La solution

Cela dépend si vous voulez une conversion tronquée ou une arrondie et à quelle précision. Par défaut, C effectuera une conversion tronquée lorsque vous passerez de float à int. Il existe des instructions FPU qui le font, mais il ne s'agit pas d'une conversion ANSI C et son utilisation comporte des mises en garde importantes (comme connaître l'état d'arrondi de la FPU). Comme la réponse à votre problème est assez complexe et dépend de certaines variables que vous n’avez pas exprimées, je vous recommande cet article:

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

Autres conseils

La conversion compacte à l'aide de SSE est de loin la méthode la plus rapide, car vous pouvez convertir plusieurs valeurs dans la même instruction. ffmpeg a beaucoup d'assemblage à cet effet (principalement pour convertir la sortie audio décodée en échantillons entiers); vérifiez-le pour quelques exemples.

Un truc couramment utilisé pour le code x86 / x87 brut consiste à forcer la partie mantisse du flottant à représenter l'int. La version 32 bits suit.

La version 64 bits est analogique. La version de Lua publiée ci-dessus est plus rapide, mais repose sur la troncature de double sur un résultat 32 bits. Elle nécessite donc que l'unité x87 soit réglée sur la double précision et ne peut pas être adaptée pour une conversion double à 64 bits int.

L’avantage de ce code est qu’il est entièrement portable pour toutes les plates-formes conformes à la norme IEEE 754; la seule hypothèse retenue est que le mode d’arrondi en virgule flottante est réglé sur la valeur la plus proche. Remarque: Portable dans le sens où il compile et fonctionne. Les plates-formes autres que x86 ne bénéficient généralement pas beaucoup de cette technique, voire pas du tout.

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 vous pouvez garantir que le processeur qui exécute votre code est compatible SSE3 (même le Pentium 5 le est, JBB), vous pouvez autoriser le compilateur à utiliser son instruction FISTTP (c'est-à-dire -msse3 pour gcc). Il semble faire la chose comme il aurait toujours dû être fait:

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

Notez que FISTTP est différent de FISTP (qui a ses problèmes, causant la lenteur). Il fait partie de SSE3 mais est en réalité (le seul) raffinement côté X87.

De toute façon, les processeurs X86 pourraient très bien effectuer la conversion. :)

Processeurs avec prise en charge de SSE3

Il existe une instruction pour convertir un nombre à virgule flottante en un entier dans l'assemblage: utilisez l'instruction FISTP. Il extrait la valeur de la pile à virgule flottante, la convertit en un entier, puis la stocke à l'adresse spécifiée. Je ne pense pas qu'il y aurait un moyen plus rapide (à moins que vous utilisiez des jeux d'instructions étendus comme MMX ou SSE, que je ne connais pas bien).

Une autre instruction, FIST, laisse la valeur sur la pile FP, mais je ne suis pas sûr que cela fonctionne avec des destinations de la taille d'un mot.

La base de code Lua a pour cela l’extrait suivant (consultez src / luaconf.h sur www.lua.org). Si vous trouvez (donc trouve) un moyen plus rapide, je suis sûr qu'ils seraient ravis.

Oh, lua_Number signifie double. :)

/*
@@ 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

Je suppose que la troncature est nécessaire, comme si on écrivait i = (int) f dans "C".

Si vous avez SSE3, vous pouvez utiliser:

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

Alternativement, avec SSE2 (ou en x64 où l'assemblage en ligne risque de ne pas être disponible), vous pouvez utiliser presque aussi rapidement:

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

Sur les ordinateurs plus anciens, il est possible de définir le mode d'arrondi manuellement et d'effectuer la conversion à l'aide de l'instruction fistp ordinaire. Cela ne fonctionnera probablement que pour les tableaux de floats, sinon il faudra veiller à ne pas utiliser de constructions susceptibles de faire changer le mode d'arrondi du compilateur (tel que le casting). C'est fait comme ça:

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:
    }
}

Notez que l’assemblage inline ne fonctionne qu'avec les compilateurs Visual Studio de Microsoft (et peut-être Borland), il devrait être réécrit dans l’assemblage GNU pour pouvoir être compilé avec gcc. La solution SSE2 avec éléments intrinsèques devrait toutefois être assez portable.

D'autres modes d'arrondi sont possibles avec différentes valeurs intrinsèques SSE2 ou en réglant manuellement le mot de contrôle de la FPU sur un mode d'arrondi différent.

Si vous vous souciez vraiment de la vitesse de cette opération, assurez-vous que votre compilateur génère l'instruction FIST. En MSVC, vous pouvez le faire avec / QIfist, voir cet aperçu MSDN .

Vous pouvez également envisager d’utiliser les composants intrinsèques de SSE pour effectuer le travail à votre place. Consultez cet article d’Intel: http://softwarecommunity.intel.com/articles/eng/2076.htm

Etant donné que MS nous a quitté l’assemblage en ligne en X64 et nous a obligés à utiliser des éléments intrinsèques, j’ai cherché lesquels. Le document MSDN donne _mm_cvtsd_si64x avec un exemple.

L'exemple fonctionne, mais il est terriblement inefficace. Il utilise une charge non alignée de 2 doubles, où nous n'avons besoin que d'une seule charge, éliminant ainsi l'exigence supplémentaire d'alignement. Ensuite, de nombreuses charges et recharges inutiles sont produites, mais elles peuvent être éliminées comme suit:

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

Résultat:

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

Le mode d'arrondi peut être défini sans assemblage en ligne, par exemple

.
    _control87(_RC_NEAR,_MCW_RC);

où arrondir au plus proche est la valeur par défaut (en tout cas).

La question de savoir s'il faut définir le mode d'arrondi à chaque appel ou présumer qu'il sera restauré (bibliothèques tierces) devra être résolue par l'expérience, j'imagine. Vous devrez inclure float.h pour _control87 () et les constantes associées.

Et non, cela ne fonctionnera pas en 32 bits, continuez donc à utiliser l'instruction FISTP:

_asm fld d
_asm fistp i

En général, vous pouvez faire confiance au compilateur pour qu’il soit efficace et correct. Il n’ya généralement rien à gagner à faire rouler ses propres fonctions pour quelque chose qui existe déjà dans le compilateur.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top