web-dev-qa-db-de.com

Erhalten des hohen Teils der 64-Bit-Ganzzahlmultiplikation

In C++ sagen Sie Folgendes:

uint64_t i;
uint64_t j;

dann ergibt i * j einen uint64_t, der den unteren Teil der Multiplikation zwischen i und j als Wert hat, d. h. (i * j) mod 2^64. Nun, was wäre, wenn ich den höheren Teil der Multiplikation wollte? Ich weiß, dass es eine Assembly-Anweisung gibt, bei der man 32-Bit-Ganzzahlen verwendet, aber ich kenne mich mit Assembly nicht aus und hoffte auf Hilfe.

Was ist der effizienteste Weg, um etwas zu machen:

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

Wenn Sie gcc verwenden und die Version, die Sie verwenden, 128-Bit-Nummern unterstützt (versuchen Sie es mit __uint128_t), dann ist das 128-Multiplizieren und Extrahieren der oberen 64 Bits wahrscheinlich die effizienteste Methode, um das Ergebnis zu erhalten.

Wenn Ihr Compiler keine 128-Bit-Zahlen unterstützt, ist die Antwort von Yakk richtig. Es kann jedoch für den allgemeinen Verbrauch zu kurz sein. Insbesondere muss bei einer tatsächlichen Implementierung darauf geachtet werden, dass 64-Bit-Integars nicht überlaufen.

Die einfache und tragbare Lösung, die er vorschlägt, besteht darin, a und b in zwei 32-Bit-Zahlen aufzuteilen und diese 32-Bit-Zahlen unter Verwendung der 64-Bit-Multiplikationsoperation zu multiplizieren. Wenn wir schreiben:

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;

dann ist es offensichtlich, dass:

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

und:

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

vorausgesetzt, die Berechnung wird mit 128-Bit-Arithmetik (oder höher) durchgeführt.

Dieses Problem erfordert jedoch, dass wir alle Berechnungen mit 64-Bit-Arithmetik durchführen. Daher müssen wir uns um den Überlauf sorgen.

Da a_hi, a_lo, b_hi und b_lo alle 32-Bit-Nummern ohne Vorzeichen sind, passt ihr Produkt ohne 64-Bit in eine 64-Bit-Nummer ohne Vorzeichen. Die Zwischenergebnisse der obigen Berechnung werden jedoch nicht.

Der folgende Code implementiert mulhi (a, b), wenn die Mathematik modulo 2 ^ 64 ausgeführt werden muss:

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;

Wie Yakk hervorhebt, können Sie die Berechnung des Übertrags-Bits weglassen, wenn es Ihnen nichts ausmacht, in den oberen 64 Bits um +1 zu sein.

16
craigster0

Lange Multiplikation sollte in Ordnung sein.

a*b in (hia+loa)*(hib+lob) aufteilen. Dies ergibt 4 32-Bit-Multiplikationen plus einige Verschiebungen. Tun Sie sie in 64 Bits, und machen Sie die Übertragungen manuell, und Sie erhalten den hohen Anteil.

Es ist zu beachten, dass eine Annäherung an den hohen Anteil mit weniger Multiplikationen erfolgen kann - mit einer Genauigkeit von etwa 2 ^ 33 mit 1 Multiplikation und mit 1 mit 3 Multiplikationen.

Ich glaube nicht, dass es eine tragbare Alternative gibt.

Leider sind die aktuellen Compiler nicht _optimieren @ craigster0s Nice portable version . Wenn Sie also 64-Bit-CPUs nutzen möchten, können Sie sie nur als Rückfall für Ziele verwenden Sie haben keinen #ifdef für. (Ich sehe keine generische Methode zur Optimierung; Sie benötigen einen 128-Bit-Typ oder einen Intrinsic.)


GNU C (gcc, clang oder ICC) hat unsigned __int128 auf den meisten 64-Bit-Plattformen. (Oder in älteren Versionen __uint128_t). GCC implementiert diesen Typ jedoch nicht auf 32-Bit-Plattformen.

Dies ist eine einfache und effiziente Möglichkeit, den Compiler dazu zu veranlassen, einen 64-Bit-Full-Multiply-Befehl auszugeben und die hohe Hälfte zu halten. (GCC weiß, dass ein uint64_t in eine 128-Bit-Ganzzahl umgewandelt wird, die immer noch die obere Hälfte aller Nullen aufweist, sodass Sie keine 128-Bit-Multiplikation mit drei 64-Bit-Multiplikationen erhalten.)

MSVC verfügt auch über einen __umulh intrinsic für die 64-Bit-Multiplikation mit hohen Halbwerten, aber wiederum nur auf 64-Bit-Plattformen (und insbesondere x86-64 und AArch64). Die Dokumente erwähnen auch IPF (IA-64) mit _umul128 verfügbar, aber ich habe keine MSVC für Itanium verfügbar. (Wahrscheinlich ohnehin nicht relevant.)

#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

Für x86-64, AArch64 und PowerPC64 (und andere) wird dies zu einer mul-Anweisung und einem Paar movs kompiliert, um die aufrufende Konvention zu behandeln (die nach diesen Inlines optimiert werden sollte). Von dem Godbolt-Compiler-Explorer (mit source + asm für x86-64, PowerPC64 und 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

(oder mit clang -march=haswell, um BMI2 zu aktivieren: mov rdx, rsimulx rax, rcx, rdi, um die High-Half direkt in RAX zu setzen. gcc ist dumm und verwendet noch eine zusätzliche mov.)

Für AArch64 (mit gcc unsigned __int128 oder MSVC mit __umulh):

test_var:
    umulh   x0, x0, x1
    ret

Bei einer konstanten Potenz der Kompilierzeit von 2 erhält man normalerweise die erwartete Rechtsverschiebung, um einige hohe Bits zu erfassen. Aber gcc verwendet amüsant shld (siehe den Godbolt-Link).


Leider aktuelle Compiler nicht optimieren @ craigster0s Nice portable version . Sie erhalten 8x shr r64,32, 4x imul r64,r64 und eine Reihe von add/mov Anweisungen für x86-64. Das heißt, es wird zu einer Menge 32x32 => 64-Bit-Multiplikationen kompiliert und die Ergebnisse entpackt. Wenn Sie also etwas wünschen, das 64-Bit-CPUs ausnutzt, benötigen Sie einige #ifdefs.

Eine Full-Multiply-Anweisung mul 64 besteht auf Intel-CPUs aus zwei Uops, aber immer noch mit einer Latenzzeit von nur 3 Zyklen, genau wie imul r64,r64, was nur ein 64-Bit-Ergebnis liefert. Daher ist die __int128/intrinsic-Version auf modernen x86-64-Modellen in Latenz und Durchsatz (Auswirkung auf den umgebenden Code) 5 bis 10-fach günstiger als die portable Version. Dies lässt sich anhand der auf _/http://agner.org/optimize basierenden schnellen Vermutung feststellen/ .

Überprüfen Sie den Godbolt-Compiler-Explorer unter dem obigen Link.

gcc optimiert diese Funktion jedoch vollständig, wenn sie mit 16 multipliziert: Sie erhalten eine einzige Rechtsverschiebung, die effizienter ist als mit unsigned __int128 multiply.

2
Peter Cordes

Dies ist eine Unit-getestete Version, die ich heute Abend mit dem vollständigen 128-Bit-Produkt erstellt habe. Bei der Prüfung scheint es einfacher zu sein als die meisten anderen Online-Lösungen (z. B. in der Botan-Bibliothek und andere Antworten hier), da sie die Vorteile des MIDDLE PART nicht ausnutzt, wie in den Codekommentaren erläutert.

Für den Kontext habe ich es für dieses Github-Projekt geschrieben: 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