使用clang的携带代码生成良好的添加

时间:2015-11-13 10:36:45

标签: c++ assembly optimization clang adx

我正在尝试生成代码(目前使用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

在这种情况下,还有一个额外的添加。不是在lo-words上做一个普通的add,而是在hi-words上adc,而只是add是hi-words,然后是add s lo-words,然后再次使用零参数对hi-word进行adc

这可能看起来不是太糟糕,但是当你用更大的单词(比如192bit,256bit)尝试这个时,你很快会得到一堆or和其他处理链条的指令,而不是简单链addadcadc,... adc

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

所以我正在寻找的代码是我可以概括到任何长度的代码(不需要这么做,只需要我可以弄清楚如何),哪个clang以一种有效的方式生成添加它确实与它内置128位类型(不幸的是,我不能轻易概括)。我认为这应该只是一个adc的链,但我欢迎参数和代码,它应该是其他东西。

2 个答案:

答案 0 :(得分: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微体系结构以来,有两条新说明:adcxadox,您可以使用_addcarryx_u64内在函数访问它们。英特尔针对这些内在函数的文档曾经是different then the assembly produced by the compiler,但现在看来他们的文档是正确的。但是,Visual Studio仍然只显示带有adcx的{​​{1}},而ICC使用此内在产生_addcarryx_u64adcx。但即使ICC产生两个指令,它也不能生成最优的代码(ICC 15),因此仍然需要内联汇编。

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

就内联汇编代码而言,我已经为multi-word addition using the carry flag的寄存器中的8个64位整数发布了256位加法的解决方案。

这是重新发布的代码。

__int128

如果要显式加载内存中的值,可以执行类似这样的操作

#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)) 

与ICC中的以下功能产生近乎完全相同的装配

//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"
     );

我对GCC内联汇编(或一般的内联汇编 - 我通常使用NASM等汇编程序)的经验有限,所以也许有更好的内联汇编解决方案。

  

所以我正在寻找的是可以概括为任何长度的代码

这里回答这个问题是使用模板元编程的另一种解决方案。 I used this same trick for loop unrolling。这会产生ICC的最佳代码。如果Clang或GCC有效地实施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); } ,这将是一个很好的通用解决方案。

_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);
}

元编程是汇编程序的一个基本特性,因此它太糟糕了C和C ++(除了通过模板元编程黑客攻击)也没有解决方案(D语言确实如此)。

我上面使用的内联汇编引用了内存导致函数中出现一些问题。这是一个似乎更好的新版本

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

答案 1 :(得分:1)

从clang 5.0开始,可以使用__uint128_t获得良好的结果 - 添加并通过移位来获取进位:

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:上面的所有示例都只编译为movaddadc指令(从clang 5.0开始,至少是-O2)。

这些示例不能用gcc生成好的代码(最多8.1,目前是godbolt上的最高版本)。 而我还没有设法使用__builtin_addcll ...