27

我正在尝试生成添加两个由多个机器字组成的数字的代码(当前使用 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采用两对无符号字并返回结果,作为中间步骤,在添加它们之前将这两个 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 位)时,您很快就会得到一团糟的ors 和其他处理进位链的指令,而不是简单的add, adc, adc, ..链。 ... adc_

多精度原语似乎在他们打算做的事情上做得很糟糕。

所以我正在寻找的是我可以推广到任何长度的代码(不需要这样做,就足够了,所以我可以弄清楚如何去做),clang 以一种与它一样有效的方式产生添加它内置 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 使用内联汇编。

adcx请注意,由于 Broadwell 微体系结构有两个新指令:adox您可以使用_addcarryx_u64内部函数访问它们。英特尔对这些内在函数的文档过去与编译器生成的程序集不同,但现在看来它们的文档是正确的。但是,Visual Studio 似乎仍然只生成adcxwith_addcarryx_u64而 ICC 生成两者adcxadox与这个内在。但即使 ICC 生成了两条指令,它也不会生成最佳代码 (ICC 15),因此仍然需要内联汇编。

就个人而言,我认为需要 C/C++ 的非标准特性(例如内联汇编或内在函数)来执行此操作是 C/C++ 的一个弱点,但其他人可能不同意。该adc指令自 1979 年以来一直在 x86 指令集中。我不会屏住呼吸 C/C++ 编译器能够以最佳方式找出你想要的时间adc。当然,它们可以具有内置类型,__int128但是当您想要一个非内置的更大类型时,您必须使用一些非标准的 C/C++ 功能,例如内联汇编或内在函数。


就执行此操作的内联汇编代码而言,我已经发布了一个解决方案,用于在寄存器中使用进位标志在多字加法中对八个 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 回答
2

在 Clang 6 上,两者都__builtin_addcl产生__builtin_add_overflow 相同的最佳反汇编

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, &carryout);
  return x;
}

Result h(unsigned_word lo1, unsigned_word hi1, unsigned_word lo2, unsigned_word hi2)
{
  Result x;
  unsigned_word carryout;
  carryout = __builtin_add_overflow(lo1, lo2, &x.lo);
  carryout = __builtin_add_overflow(hi1, carryout, &hi1);
  __builtin_add_overflow(hi1, hi2, &x.hi);
  return x;
}

两者的组装:

add rdi, rdx
adc rsi, rcx
mov rax, rdi
mov rdx, rsi
ret
于 2018-05-09T23:06:24.827 回答
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 仍然会执行奇怪的操作(我假设是因为可能存在别名?),但通常将一个变量复制到一个临时变量中会有所帮助。

使用示例

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 Link:以上所有示例都仅编译为mov,addadc指令(从 clang 5.0 开始,至少 -O2)。

这些示例不能使用 gcc 生成好的代码(最高 8.1,目前是 Godbolt 上的最高版本)。而且我还没有设法得到任何可用的东西__builtin_addcll......

于 2018-05-09T22:54:17.387 回答