web-dev-qa-db-ja.com

64ビット整数乗算の上位部分を取得する

C++では、次のように言います。

uint64_t i;
uint64_t j;

次にi * juint64_tは、値としてijの間の乗算の下位部分、つまり(i * j) mod 2^64。では、乗算の上位部分が必要な場合はどうなりますか? 32ビット整数を使用するときにそのようなことを行うアセンブリ命令が存在することは知っていますが、私はアセンブリにまったく精通していないので、助けを期待していました。

次のようなものを作る最も効率的な方法は何ですか?

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

Gccを使用していて、使用しているバージョンが128ビット数をサポートしている場合(__uint128_tを使用してみてください)、128乗算を実行して上位64ビットを抽出するのが、結果を取得する最も効率的な方法です。

コンパイラが128ビットの数値をサポートしていない場合、Yakkの答えは正しいです。ただし、一般消費には短すぎる場合があります。特に、実際の実装では64ビット整数のオーバーフローに注意する必要があります。

彼が提案するシンプルでポータブルなソリューションは、aとbのそれぞれを2つの32ビット数に分割し、64ビット乗算演算を使用してそれらの32ビット数を乗算することです。私たちが書いた場合:

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;

その後、それは明らかです:

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

そして:

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

ただし、計算は128ビット(またはそれ以上)の演算を使用して実行されます。

ただし、この問題では64ビット演算を使用してすべての計算を実行する必要があるため、オーバーフローを心配する必要があります。

A_hi、a_lo、b_hi、およびb_loはすべて符号なし32ビット数であるため、それらの積はオーバーフローなしで符号なし64ビット数に収まります。ただし、上記の計算の中間結果にはなりません。

次のコードは、2 ^ 64を法として数学を実行する必要がある場合に、mulhi(a、b)を実装します。

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;

Yakkが指摘しているように、上位64ビットで+1ずれても構わない場合は、キャリービットの計算を省略できます。

18
craigster0

これは私が今夜に思いついたユニットテスト済みバージョンで、完全な128ビット製品を提供します。検査では、コードコメントで説明されているように、MIDDLE PARTがオーバーフローしないという利点を利用しているため、オンラインの他のほとんどのソリューション(Botanライブラリやその他のここでの回答など)よりも簡単なようです。

コンテキストについては、この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
4
catid

長い乗算は良いパフォーマンスであるはずです。

スプリット a*bから(hia+loa)*(hib+lob)。これにより、4つの32ビット乗算といくつかのシフトが得られます。それらを64ビットで実行し、キャリーを手動で実行すると、高い部分が得られます。

高い部分の近似はより少ない乗算で実行できることに注意してください-1乗算で2 ^ 33以内で、3乗算で1以内で正確です。

ポータブルな代替手段はないと思います。

64ビットISAのGCCを使用したTL:DR:(a * (unsigned __int128)b) >> 64は、単一の完全乗算または上位半分乗算命令にうまくコンパイルされます。インラインasmをいじる必要はありません。


残念ながら現在のコンパイラしないでください @ craigster0のNiceポータブルバージョンを最適化して、64ビットCPUを利用したい場合、#ifdefがないターゲットのフォールバックとして以外は使用できません。 (それを最適化する一般的な方法はわかりません。128ビット型または組み込み関数が必要です。)


GNU C(gcc、clang、またはICC) ほとんどの64ビットプラットフォームでunsigned __int128 があります。 (または以前のバージョンでは__uint128_t)。ただし、GCCは32ビットプラットフォームでこのタイプを実装していません。

これは、コンパイラーに64ビットの全乗算命令を発行させ、上位半分を維持させる簡単で効率的な方法です。 (GCCは、128ビット整数にキャストされたuint64_tが依然として上半分がすべてゼロであることを知っているため、3つの64ビット乗算を使用して128ビット乗算を取得することはありません。)

MSVCには__umulh組み込み もあり、64ビットの上位半分の乗算が可能ですが、64ビットプラットフォーム(特にx86-64およびAArch64)でのみ使用できます。ドキュメントにはIPFについても記載されています(IA-64)_umul128を利用できますが、ItaniumのMSVCを利用できません(おそらく関係ありません)。

#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

x86-64、AArch64、およびPowerPC64(およびその他)の場合、これは1つのmul命令といくつかのmovsは呼び出し規約に対処します(これはこのインラインの後で最適化されるはずです)。 From Godboltコンパイラエクスプローラ (x86-64、PowerPC64、AArch64のソース+ asmを使用):

     # 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

(またはclang -march=haswellを使用してBMI2を有効にします:mov rdx, rsi/mulx rax, rcx, rdiを使用して上半分を直接RAXに入れます。gccは無意味であり、追加のmovを使用します。)

AArch64の場合(gcc unsigned __int128またはMSVCと__umulhを使用):

test_var:
    umulh   x0, x0, x1
    ret

コンパイル時の定数乗数が2の乗数であると、通常、予想どおりの右シフトでいくつかの上位ビットを取得します。しかし、gccは面白いことにshldを使用しています(Godboltリンクを参照)。


残念ながら現在のコンパイラしないでください @ craigster0のNiceポータブルバージョンを最適化します。 8x shr r64,32、4x imul r64,r64、およびx86-64のadd/mov命令の束を取得します。つまり、多くの32x32 => 64ビットの乗算にコンパイルされ、結果がアンパックされます。したがって、64ビットCPUを利用するものが必要な場合は、#ifdefsが必要です。

完全乗算mul 64命令はIntel CPUでは2 uopsですが、64ビットの結果しか生成しないimul r64,r64と同じように、3サイクルのレイテンシしかありません。つまり、__int128 /組み込みバージョンは、最新のx86-64でのレイテンシとスループット(周囲のコードへの影響)がポータブルバージョンよりも5〜10倍安価です http:/ /agner.org/optimize/

上記のリンクのGodboltコンパイラエクスプローラーで確認してください。

ただし、gccは、16を乗算するときにこの関数を完全に最適化します。unsigned __int128乗算よりも効率的な単一の右シフトを取得します。

1
Peter Cordes