web-dev-qa-db-fra.com

Obtenir la partie haute de la multiplication d’entiers 64 bits

En C++, dites ceci:

uint64_t i;
uint64_t j;

alors i * j donnera un uint64_t qui a pour valeur la partie inférieure de la multiplication entre i et j, c.-à-d. (i * j) mod 2^64. Maintenant, que se passe-t-il si je voulais la partie supérieure de la multiplication? Je sais qu’il existe une instruction de l’Assemblée pour faire quelque chose comme cela lorsqu’on utilise des entiers 32 bits, mais je ne connais pas du tout Assembly, j’espérais donc de l’aide.

Quel est le moyen le plus efficace de créer quelque chose comme:

uint64_t k = mulhi(i, j);
19
Matteo Monti

Si vous utilisez gcc et que la version que vous utilisez prend en charge les nombres à 128 bits (essayez d'utiliser __uint128_t), effectuez la multiplication à 128 et l'extraction des 64 bits supérieurs constitue probablement le moyen le plus efficace d'obtenir le résultat.

Si votre compilateur ne prend pas en charge les nombres 128 bits, la réponse de Yakk est correcte. Cependant, il peut être trop bref pour la consommation générale. En particulier, une implémentation réelle doit faire attention au débordement des entiers 64 bits.

La solution simple et portable qu'il propose consiste à diviser chacun de a et b en 2 nombres de 32 bits, puis de multiplier ces nombres de 32 bits en utilisant l'opération de multiplication de 64 bits. Si nous écrivons:

uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;

alors il est évident que:

a = (a_hi << 32) + a_lo;
b = (b_hi << 32) + b_lo;

et:

a * b = ((a_hi << 32) + a_lo) * ((b_hi << 32) + b_lo)
      = ((a_hi * b_hi) << 64) +
        ((a_hi * b_lo) << 32) +
        ((b_hi * a_lo) << 32) +
          a_lo * b_lo

à condition que le calcul soit effectué en utilisant une arithmétique de 128 bits (ou plus).

Mais ce problème nécessite que nous effectuions tous les calculs en utilisant l’arithmétique 64 bits, nous devons donc nous préoccuper des débordements.

Puisque a_hi, a_lo, b_hi et b_lo sont tous des nombres 32 bits non signés, leur produit tiendra dans un nombre non signé de 64 bits sans débordement. Cependant, les résultats intermédiaires du calcul ci-dessus ne le seront pas.

Le code suivant implémentera mulhi (a, b) lorsque les calculs mathématiques doivent être effectués modulo 2 ^ 64:

uint64_t    a_lo = (uint32_t)a;
uint64_t    a_hi = a >> 32;
uint64_t    b_lo = (uint32_t)b;
uint64_t    b_hi = b >> 32;

uint64_t    a_x_b_hi =  a_hi * b_hi;
uint64_t    a_x_b_mid = a_hi * b_lo;
uint64_t    b_x_a_mid = b_hi * a_lo;
uint64_t    a_x_b_lo =  a_lo * b_lo;

uint64_t    carry_bit = ((uint64_t)(uint32_t)a_x_b_mid +
                         (uint64_t)(uint32_t)b_x_a_mid +
                         (a_x_b_lo >> 32) ) >> 32;

uint64_t    multhi = a_x_b_hi +
                     (a_x_b_mid >> 32) + (b_x_a_mid >> 32) +
                     carry_bit;

return multhi;

Comme le fait remarquer Yakk, si cela ne vous dérange pas d'être inactif de +1 dans les 64 bits supérieurs, vous pouvez omettre le calcul du bit de retenue.

16
craigster0

Une multiplication longue devrait être une bonne performance.

Divisez a*b en (hia+loa)*(hib+lob). Cela donne 4 multiplications 32 bits plus quelques décalages. Faites-les en 64 bits et faites les retenues manuellement, et vous obtiendrez la partie haute.

Notez qu’une approximation de la partie haute peut être réalisée avec moins de multiplications - exactes dans les 2 ^ 33 ou plus avec 1 multiplication, et dans les limites de 1 avec 3 multiplications.

Je ne pense pas qu'il existe une alternative portable.

Malheureusement, les compilateurs actuels non optimisent la version portable de Craigster0 , donc si vous voulez tirer parti des processeurs 64 bits, vous ne pouvez pas l'utiliser, sauf comme solution de secours pour les cibles. vous n'avez pas de #ifdef pour. (Je ne vois pas de moyen générique pour l'optimiser; vous avez besoin d'un type 128 bits ou intrinsèque.)


GNU C (gcc, clang ou ICC) a unsigned __int128 sur la plupart des plates-formes 64 bits. (Ou dans les anciennes versions, __uint128_t). Cependant, GCC n'implémente pas ce type sur les plates-formes 32 bits.

C'est un moyen simple et efficace de faire en sorte que le compilateur émette une instruction de multiplication complète 64 bits tout en conservant la moitié haute. (GCC sait qu'un uint64_t converti en un entier de 128 bits a toujours la moitié supérieure tout le zéro, vous ne pouvez donc pas obtenir une multiplication de 128 bits avec trois multiplications de 64 bits.)

MSVC a également un __umulh intrinsèque pour la multiplication moitié-moitié 64 bits, mais là encore, il n’est disponible que sur les plates-formes 64 bits (et plus précisément x86-64 et AArch64. Les documents mentionnent également IPF (IA-64) ayant _umul128 disponible, mais je n'ai pas MSVC pour Itanium disponible (probablement pas pertinent de toute façon.)

#define HAVE_FAST_mul64 1

#ifdef __SIZEOF_INT128__     // GNU C
 static inline
 uint64_t mulhi64(uint64_t a, uint64_t b) {
     unsigned __int128 prod =  a * (unsigned __int128)b;
     return prod >> 64;
 }

#Elif defined(_M_X64) || defined(_M_ARM64)     // MSVC
   // MSVC for x86-64 or AArch64
   // possibly also  || defined(_M_IA64) || defined(_WIN64)
   // but the docs only guarantee x86-64!  Don't use *just* _WIN64; it doesn't include AArch64 Android / Linux

  // https://docs.Microsoft.com/en-gb/cpp/intrinsics/umulh
  #include <intrin.h>
  #define mulhi64 __umulh

#Elif defined(_M_IA64) // || defined(_M_ARM)       // MSVC again
  // https://docs.Microsoft.com/en-gb/cpp/intrinsics/umul128
  // incorrectly say that _umul128 is available for ARM
  // which would be weird because there's no single insn on AArch32
  #include <intrin.h>
  static inline
  uint64_t mulhi64(uint64_t a, uint64_t b) {
     unsigned __int64 HighProduct;
     (void)_umul128(a, b, &HighProduct);
     return HighProduct;
  }

#else

# undef HAVE_FAST_mul64
  uint64_t mulhi64(uint64_t a, uint64_t b);  // non-inline prototype
  // or you might want to define @craigster0's version here so it can inline.
#endif

Pour x86-64, AArch64 et PowerPC64 (et autres), cette opération est compilée en une instruction mulet un couple movname__s pour traiter la convention d'appel (qui doit être optimisée après cette entrée dans les lignes). Depuis l'explorateur du compilateur Godbolt (avec source + asm pour x86-64, PowerPC64 et AArch64):

     # x86-64 gcc7.3.  clang and ICC are the same.  (x86-64 System V calling convention)
     # MSVC makes basically the same function, but with different regs for x64 __fastcall
    mov     rax, rsi
    mul     rdi              # RDX:RAX = RAX * RDI
    mov     rax, rdx
    ret

(ou avec clang -march=haswell pour permettre à BMI2: mov rdx, rsi/mulx rax, rcx, rdi de placer directement la moitié haute dans RAX. gcc est stupide et utilise encore un movsupplémentaire.)

Pour AArch64 (avec gcc unsigned __int128 ou MSVC avec __umulh):

test_var:
    umulh   x0, x0, x1
    ret

Avec une puissance constante au moment de la compilation de 2 multiplicateur, nous obtenons généralement le décalage à droite attendu pour capturer quelques bits élevés. Mais gcc utilise avec amusement shld(voir le lien Godbolt).


Malheureusement, les compilateurs actuels non optimisent la version portable de @ craigster0 . Vous obtenez 8x shr r64,32, 4x imul r64,r64 et un tas d'instructions addname __/movpour x86-64. c’est-à-dire qu’il compile en un grand nombre de multiplications et décompressions de résultats 32x32 => 64 bits. Donc, si vous voulez quelque chose qui tire parti des processeurs 64 bits, vous avez besoin de #ifdefs.

Une instruction à multiplication mul 64 complète est égale à 2 uops sur les processeurs Intel, mais il ne reste qu'une latence de 3 cycles, identique à imul r64,r64 qui ne produit qu'un résultat 64 bits. Ainsi, la version __int128/intrinsic est 5 à 10 fois moins chère en temps de latence et de débit (impact sur le code environnant) sur la version moderne x86-64 que la version portable, à partir d'une estimation rapide basée sur http://agner.org/optimize/ .

Découvrez-le sur l'explorateur du compilateur Godbolt sur le lien ci-dessus.

gcc optimise pleinement cette fonction lors de la multiplication par 16: vous obtenez un seul décalage à droite, plus efficace qu'avec la multiplication unsigned __int128.

2
Peter Cordes

C'est une version testée par l'unité que j'ai proposée ce soir et qui fournit le produit 128 bits complet. Lors de l'inspection, cela semble être plus simple que la plupart des autres solutions en ligne (dans la bibliothèque Botan et d'autres réponses ici, par exemple), car elle tire parti de la manière dont la partie centrale ne déborde pas, comme expliqué dans les commentaires de code.

Pour le contexte, je l'ai écrit pour ce projet github: https://github.com/catid/fp61

//------------------------------------------------------------------------------
// Portability Macros

// Compiler-specific force inline keyword
#ifdef _MSC_VER
# define FP61_FORCE_INLINE inline __forceinline
#else
# define FP61_FORCE_INLINE inline __attribute__((always_inline))
#endif


//------------------------------------------------------------------------------
// Portable 64x64->128 Multiply
// CAT_MUL128: r{hi,lo} = x * y

// Returns low part of product, and high part is set in r_hi
FP61_FORCE_INLINE uint64_t Emulate64x64to128(
    uint64_t& r_hi,
    const uint64_t x,
    const uint64_t y)
{
    const uint64_t x0 = (uint32_t)x, x1 = x >> 32;
    const uint64_t y0 = (uint32_t)y, y1 = y >> 32;
    const uint64_t p11 = x1 * y1, p01 = x0 * y1;
    const uint64_t p10 = x1 * y0, p00 = x0 * y0;
    /*
        This is implementing schoolbook multiplication:

                x1 x0
        X       y1 y0
        -------------
                   00  LOW PART
        -------------
                00
             10 10     MIDDLE PART
        +       01
        -------------
             01 
        + 11 11        HIGH PART
        -------------
    */

    // 64-bit product + two 32-bit values
    const uint64_t middle = p10 + (p00 >> 32) + (uint32_t)p01;

    /*
        Proof that 64-bit products can accumulate two more 32-bit values
        without overflowing:

        Max 32-bit value is 2^32 - 1.
        PSum = (2^32-1) * (2^32-1) + (2^32-1) + (2^32-1)
             = 2^64 - 2^32 - 2^32 + 1 + 2^32 - 1 + 2^32 - 1
             = 2^64 - 1
        Therefore it cannot overflow regardless of input.
    */

    // 64-bit product + two 32-bit values
    r_hi = p11 + (middle >> 32) + (p01 >> 32);

    // Add LOW PART and lower half of MIDDLE PART
    return (middle << 32) | (uint32_t)p00;
}

#if defined(_MSC_VER) && defined(_WIN64)
// Visual Studio 64-bit

# include <intrin.h>
# pragma intrinsic(_umul128)
# define CAT_MUL128(r_hi, r_lo, x, y) \
    r_lo = _umul128(x, y, &(r_hi));

#Elif defined(__SIZEOF_INT128__)
// Compiler supporting 128-bit values (GCC/Clang)

# define CAT_MUL128(r_hi, r_lo, x, y)                   \
    {                                                   \
        unsigned __int128 w = (unsigned __int128)x * y; \
        r_lo = (uint64_t)w;                             \
        r_hi = (uint64_t)(w >> 64);                     \
    }

#else
// Emulate 64x64->128-bit multiply with 64x64->64 operations

# define CAT_MUL128(r_hi, r_lo, x, y) \
    r_lo = Emulate64x64to128(r_hi, x, y);

#endif // End CAT_MUL128
0
catid