Explication d'une méthode rapide pour arrondir un double à un int 32 bits

169

En lisant le code source de Lua , j'ai remarqué que Lua utilise a macropour arrondir a doubleà 32 bits int. J'ai extrait le macro, et cela ressemble à ceci:

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];}

Ici ENDIANLOCse définit comme endianness , 0pour little endian, 1pour big endian. Lua gère soigneusement l'endianness. treprésente le type entier, comme intou unsigned int.

J'ai fait un peu de recherche et il existe un format plus simple macroqui utilise la même pensée:

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

Ou dans un style C ++:

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

Cette astuce peut fonctionner sur n'importe quelle machine utilisant IEEE 754 (ce qui signifie à peu près toutes les machines aujourd'hui). Cela fonctionne pour les nombres positifs et négatifs, et l'arrondi suit la règle du banquier . (Ce n'est pas surprenant, car il suit IEEE 754.)

J'ai écrit un petit programme pour le tester:

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

Et il produit -12345679, comme prévu.

Je voudrais entrer dans les détails sur le fonctionnement de cette astuce macro. Le nombre magique 6755399441055744.0est en fait 2^51 + 2^52, ou 1.5 * 2^52, et 1.5en binaire peut être représenté par 1.1. Lorsqu'un entier 32 bits est ajouté à ce nombre magique, eh bien, je suis perdu d'ici. Comment fonctionne cette astuce?

PS: C'est dans le code source de Lua, Llimits.h .

MISE À JOUR :

  1. Comme le souligne @Mysticial, cette méthode ne se limite pas à 32 bits int, elle peut également être étendue à 64 bits inttant que le nombre est compris entre 2 ^ 52. (Le macrobesoin de quelques modifications.)
  2. Certains matériaux indiquent que cette méthode ne peut pas être utilisée dans Direct3D .
  3. Lorsque vous travaillez avec l'assembleur Microsoft pour x86, il existe une macroécriture encore plus rapide assembly(elle est également extraite de la source Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Il existe un nombre magique similaire pour un nombre à précision unique: 1.5 * 2 ^23

Yu Hao
la source
3
"rapide" par rapport à quoi?
Cory Nelson
3
@CoryNelson Fast comparé à un casting simple. Cette méthode, lorsqu'elle est mise en œuvre correctement (avec les intrinsèques SSE) est littéralement cent fois plus rapide qu'un cast. (qui invoque un appel de fonction méchant à un code de conversion plutôt coûteux)
Mysticial
2
Droite - je peux voir que c'est plus rapide que ftoi. Mais si vous parlez SSE, pourquoi ne pas simplement utiliser l'instruction unique CVTTSD2SI?
Cory Nelson
3
@tmyklebu De nombreux cas d'utilisation double -> int64sont en effet dans la 2^52fourchette. Celles-ci sont particulièrement courantes lors de l'exécution de convolutions entières à l'aide de FFT à virgule flottante.
Mysticial
7
@MSalters Pas nécessairement vrai. Un casting doit être conforme aux spécifications du langage - y compris la gestion appropriée des cas de dépassement de capacité et de NAN. (ou ce que le compilateur spécifie dans le cas IB ou UB) Ces vérifications ont tendance à être très coûteuses. L'astuce mentionnée dans cette question ignore complètement ces cas de coin. Donc, si vous voulez la vitesse et que votre application ne se soucie pas (ou ne rencontre jamais) de tels cas de coin, alors ce hack est parfaitement approprié.
Mysticial

Réponses:

161

A doubleest représenté comme ceci:

double représentation

et il peut être vu comme deux entiers de 32 bits; maintenant, la intprise dans toutes les versions de votre code (en supposant que ce soit un 32 bits int) est celle de droite sur la figure, donc ce que vous faites à la fin est juste de prendre les 32 bits les plus bas de la mantisse.


Maintenant, au nombre magique; comme vous l'avez correctement dit, 6755399441055744 est 2 ^ 51 + 2 ^ 52; l'ajout d'un tel nombre oblige le doubleà aller dans la "gamme douce" entre 2 ^ 52 et 2 ^ 53, ce qui, comme l'explique Wikipedia ici , a une propriété intéressante:

Entre 2 52 = 4,503,599,627,370,496 et 2 53 = 9,007,199,254,740,992, les nombres représentables sont exactement les entiers

Cela découle du fait que la mantisse a une largeur de 52 bits.

L'autre fait intéressant à propos de l'ajout de 2 51 +2 52 est qu'il n'affecte la mantisse que dans les deux bits les plus élevés - qui sont de toute façon ignorés, puisque nous ne prenons que ses 32 bits les plus bas.


Dernier point mais non le moindre: le signe.

La virgule flottante IEEE 754 utilise une représentation de grandeur et de signe, tandis que les entiers sur les machines "normales" utilisent l'arithmétique du complément à 2; comment est-ce géré ici?

Nous n'avons parlé que d'entiers positifs; supposons maintenant que nous ayons affaire à un nombre négatif dans la plage représentable par un 32 bits int, donc inférieur (en valeur absolue) à (-2 ^ 31 + 1); appelle-le -a. Un tel nombre est évidemment rendu positif en ajoutant le nombre magique, et la valeur résultante est 2 52 +2 51 + (- a).

Maintenant, qu'obtient-on si nous interprétons la mantisse dans la représentation complémentaire de 2? Il doit être le résultat de la somme du complément à 2 de (2 52 +2 51 ) et (-a). Encore une fois, le premier terme affecte uniquement les deux bits supérieurs, ce qui reste dans les bits 0 ~ 50 est la représentation complémentaire de 2 de (-a) (encore une fois, moins les deux bits supérieurs).

Puisque la réduction d'un nombre de complément à 2 à une largeur plus petite se fait simplement en coupant les bits supplémentaires sur la gauche, prendre les 32 bits inférieurs nous donne correctement (-a) en 32 bits, l'arithmétique du complément à 2.

Matteo Italia
la source
"" "L'autre fait intéressant à propos de l'ajout de 2 ^ 51 + 2 ^ 52 est qu'il n'affecte la mantisse que dans les deux bits les plus élevés - qui sont de toute façon ignorés, puisque nous ne prenons que ses 32 bits les plus bas" "" Qu'est-ce que c'est? L'ajout de cela peut changer toute la mantisse!
YvesgereY
@John: bien sûr, tout l'intérêt de les ajouter est de forcer la valeur à être dans cette plage, ce qui peut évidemment entraîner un décalage de la mantisse (entre les autres choses) par rapport à la valeur d'origine. Ce que je disais ici, c'est qu'une fois que vous êtes dans cette plage, les seuls bits qui diffèrent de l'entier de 53 bits correspondant sont les bits 51 et 52, qui sont de toute façon ignorés.
Matteo Italia du
2
Pour ceux qui souhaitent se convertir, int64_tvous pouvez le faire en décalant la mantisse vers la gauche puis vers la droite de 13 bits. Cela effacera l'exposant et les deux bits du nombre «magique», mais gardera et propagera le signe à l'entier signé 64 bits entier. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda
Dois-je bien comprendre que 2 ^ 51 n'est nécessaire que pour gérer les valeurs négatives?
Kentzo il y a