27

複数の機械語で構成される 2 つの数値を追加するコード (現在は clang++-3.8 を使用) を作成しようとしています。今のところ単純化するために、128 ビットの数値のみを追加していますが、これを一般化できるようにしたいと考えています。

最初のいくつかの typedef:

typedef unsigned long long unsigned_word;
typedef __uint128_t unsigned_128;

そして「結果」タイプ:

struct Result
{
  unsigned_word lo;
  unsigned_word hi;
};

最初の関数 はf、符号なしワードの 2 つのペアを取り、結果を返します。中間ステップとして、これらの 64 ビット ワードの両方を 128 ビット ワードに入れてから、次のように追加します。

Result f (unsigned_word lo1, unsigned_word hi1, unsigned_word lo2, unsigned_word hi2)
{
  Result x;
  unsigned_128 n1 = lo1 + (static_cast<unsigned_128>(hi1) << 64);
  unsigned_128 n2 = lo2 + (static_cast<unsigned_128>(hi2) << 64);
  unsigned_128 r1 = n1 + n2;
  x.lo = r1 & ((static_cast<unsigned_128>(1) << 64) - 1);
  x.hi = r1 >> 64;
  return x;
}

これは実際には次のように非常にうまくインライン化されます。

movq    8(%rsp), %rsi
movq    (%rsp), %rbx
addq    24(%rsp), %rsi
adcq    16(%rsp), %rbx

代わりに、次のように、clang 多精度プリミティブを使用して、より単純な関数を作成しました。

static Result g (unsigned_word lo1, unsigned_word hi1, unsigned_word lo2, unsigned_word hi2)
{
  Result x;
  unsigned_word carryout;
  x.lo = __builtin_addcll(lo1, lo2, 0, &carryout);
  x.hi = __builtin_addcll(hi1, hi2, carryout, &x.carry);
  return x;
}

これにより、次のアセンブリが生成されます。

movq    24(%rsp), %rsi
movq    (%rsp), %rbx
addq    16(%rsp), %rbx
addq    8(%rsp), %rsi
adcq    $0, %rbx

この場合、追加の追加があります。通常addのローワードを実行してからハイワードを実行する代わりにadc、単にaddハイワードを実行し、次にaddローワードを実行adcし、ハイワードをゼロの引数で再度実行します。

これはそれほど悪くはないように見えるかもしれませんが、より大きなワード (192 ビット、256 ビットなど) でこれを試すとor、単純なadd, adc, adc.. . adc.

多精度プリミティブは、意図したとおりにひどい仕事をしているようです。

したがって、私が探しているのは、任意の長さに一般化できるコードです (それを行う必要はありません。方法を理解するだけで十分です)。これは 128 ビット型で組み込まれています (残念ながら、これを簡単に一般化することはできません)。これは単に一連のadcs であるべきだと思いますが、別のものであるべきであるという引数とコードを歓迎します。

4

3 に答える 3

23

これを行う組み込み関数があります: _addcarry_u64。ただし、Visual StudioICC (少なくとも VS 2013 と 2015、ICC 13 と ICC 15) のみがこれを効率的に実行します。Clang 3.7 および GCC 5.2 は、この組み込み関数を使用して効率的なコードを生成しません。

さらに、Clang には、これを実行すると思われる組み込みの がありますが、__builtin_addcll効率的なコードも生成されません。

Visual Studio がこれを行う理由は、64 ビット モードでのインライン アセンブリが許可されていないため、コンパイラが組み込み関数を使用してこれを行う方法を提供する必要があるためです (ただし、Microsoft はこれを実装するのに時間がかかりました)。

したがって、Visual Studio では_addcarry_u64. ICC を使用する _addcarry_u64か、インライン アセンブリを使用します。Clang と GCC では、インライン アセンブリを使用します。

Broadwell マイクロアーキテクチャ以降、2 つの新しい命令があることに注意してください:adcxおよび_addcarryx_u64adox組み込み関数でアクセスできます。これらの組み込み関数に関する Intel のドキュメントは、以前はコンパイラによって生成されたアセンブリとは異なっていましたが、現在はドキュメントが正しいようです。ただし、Visual Studio は依然として with のみを生成するように見えますが、ICC はとwith this 組み込みの両方を生成します。しかし、ICC は両方の命令を生成しますが、最適なコード (ICC 15) を生成しないため、インライン アセンブリが必要です。adcx_addcarryx_u64adcxadox

個人的には、これを行うために C/C++ の非標準機能 (インライン アセンブリや組み込み関数など) が必要であるという事実は C/C++ の弱点だと思いますが、他の人は同意しないかもしれません。このadc命令は、1979 年から x86 命令セットに含まれています。C/C++ コンパイラが必要なタイミングを最適に判断できることに息をのむことはありませんadc。など__int128の組み込み型を使用できますが、組み込みではないより大きな型が必要な場合は、インライン アセンブリや組み込み関数などの非標準の C/C++ 機能を使用する必要があります。


これを行うためのインライン アセンブリ コードに関しては、キャリー フラグを使用したマルチワード加算で、レジスタ内の 8 つの 64 ビット整数に対する 256 ビット加算のソリューションを既に投稿しました。

これが再投稿されたコードです。

#define ADD256(X1, X2, X3, X4, Y1, Y2, Y3, Y4) \
 __asm__ __volatile__ ( \
 "addq %[v1], %[u1] \n" \
 "adcq %[v2], %[u2] \n" \
 "adcq %[v3], %[u3] \n" \
 "adcq %[v4], %[u4] \n" \
 : [u1] "+&r" (X1), [u2] "+&r" (X2), [u3] "+&r" (X3), [u4] "+&r" (X4) \
 : [v1]  "r" (Y1), [v2]  "r" (Y2), [v3]  "r" (Y3), [v4]  "r" (Y4)) 

メモリから値を明示的にロードしたい場合は、次のようなことができます

//uint64_t dst[4] = {1,1,1,1};
//uint64_t src[4] = {1,2,3,4};
asm (
     "movq (%[in]), %%rax\n"
     "addq %%rax, %[out]\n"
     "movq 8(%[in]), %%rax\n"
     "adcq %%rax, 8%[out]\n"
     "movq 16(%[in]), %%rax\n"
     "adcq %%rax, 16%[out]\n"
     "movq 24(%[in]), %%rax\n"
     "adcq %%rax, 24%[out]\n"
     : [out] "=m" (dst)
     : [in]"r" (src)
     : "%rax"
     );

これにより、ICC の次の関数とほぼ同じアセンブリが生成されます

void add256(uint256 *x, uint256 *y) {
    unsigned char c = 0;
    c = _addcarry_u64(c, x->x1, y->x1, &x->x1);
    c = _addcarry_u64(c, x->x2, y->x2, &x->x2);
    c = _addcarry_u64(c, x->x3, y->x3, &x->x3);
        _addcarry_u64(c, x->x4, y->x4, &x->x4);
}

私は GCC インライン アセンブリ (または一般的なインライン アセンブリ - 通常は NASM などのアセンブラを使用) の経験が限られているため、より優れたインライン アセンブリ ソリューションがあるかもしれません。


だから私が探しているのは、任意の長さに一般化できるコードです

この質問に答えるために、テンプレート メタ プログラミングを使用した別のソリューションを紹介します。これと同じトリックをループ展開に使用しました。これにより、ICC で最適なコードが生成されます。Clang または GCC が_addcarry_u64効率的に実装される場合、これは適切な一般的な解決策になります。

#include <x86intrin.h>
#include <inttypes.h>

#define LEN 4  // N = N*64-bit add e.g. 4=256-bit add, 3=192-bit add, ...

static unsigned char c = 0;

template<int START, int N>
struct Repeat {
    static void add (uint64_t *x, uint64_t *y) {
        c = _addcarry_u64(c, x[START], y[START], &x[START]);
        Repeat<START+1, N>::add(x,y);
    }
};

template<int N>
    struct Repeat<LEN, N> {
    static void add (uint64_t *x, uint64_t *y) {}
};


void sum_unroll(uint64_t *x, uint64_t *y) {
    Repeat<0,LEN>::add(x,y);
}

ICCからの組立

xorl      %r10d, %r10d                                  #12.13
movzbl    c(%rip), %eax                                 #12.13
cmpl      %eax, %r10d                                   #12.13
movq      (%rsi), %rdx                                  #12.13
adcq      %rdx, (%rdi)                                  #12.13
movq      8(%rsi), %rcx                                 #12.13
adcq      %rcx, 8(%rdi)                                 #12.13
movq      16(%rsi), %r8                                 #12.13
adcq      %r8, 16(%rdi)                                 #12.13
movq      24(%rsi), %r9                                 #12.13
adcq      %r9, 24(%rdi)                                 #12.13
setb      %r10b

メタ プログラミングはアセンブラの基本的な機能であるため、C と C++ (テンプレート メタ プログラミング ハックを除く) にも解決策がありません (D 言語には解決策があります)。


上記で使用した、メモリを参照するインライン アセンブリは、関数でいくつかの問題を引き起こしていました。これは、よりうまく機能するように見える新しいバージョンです

void foo(uint64_t *dst, uint64_t *src)
{
    __asm (
        "movq (%[in]), %%rax\n"
        "addq %%rax, (%[out])\n"
        "movq 8(%[in]), %%rax\n"
        "adcq %%rax, 8(%[out])\n"
        "movq 16(%[in]), %%rax\n"
        "addq %%rax, 16(%[out])\n"
        "movq 24(%[in]), %%rax\n"
        "adcq %%rax, 24(%[out])\n"
        :
        : [in] "r" (src), [out] "r" (dst)
        : "%rax"
    );
}
于 2015-11-15T11:27:55.060 に答える
1

clang 5.0 以降では、__uint128_t-addition を使用し、シフトすることでキャリー ビットを取得して、良い結果を得ることができます。

inline uint64_t add_with_carry(uint64_t &a, const uint64_t &b, const uint64_t &c)
{
    __uint128_t s = __uint128_t(a) + b + c;
    a = s;
    return s >> 64;
}

多くの場合、clang は依然として奇妙な操作を行いますが (エイリアシングの可能性があるためだと思いますか?)、通常は 1 つの変数を一時変数にコピーすると役立ちます。

使用例

template<int size> struct LongInt
{
    uint64_t data[size];
};

手動使用:

void test(LongInt<3> &a, const LongInt<3> &b_)
{
    const LongInt<3> b = b_; // need to copy b_ into local temporary
    uint64_t c0 = add_with_carry(a.data[0], b.data[0], 0);
    uint64_t c1 = add_with_carry(a.data[1], b.data[1], c0);
    uint64_t c2 = add_with_carry(a.data[2], b.data[2], c1);
}

一般的な解決策:

template<int size>
void addTo(LongInt<size> &a, const LongInt<size> b)
{
    __uint128_t c = __uint128_t(a.data[0]) + b.data[0];
    for(int i=1; i<size; ++i)
    {
        c = __uint128_t(a.data[i]) + b.data[i] + (c >> 64);
        a.data[i] = c;
    }
}

Godbolt Linkmov : 上記の例はすべて、、addおよび命令のみにコンパイルされていadcます (clang 5.0 以降、少なくとも -O2)。

例は、gcc (現時点では Godbolt の最高バージョンである 8.1 まで) では適切なコードを生成しません。そして、私はまだ使用できるものを手に入れることができませんでした__builtin_addcll...

于 2018-05-09T22:54:17.387 に答える