我正在尝试生成代码(目前使用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
和其他处理链条的指令,而不是简单链add
,adc
,adc
,... adc
。
多精度原语似乎在他们打算做的事情上做得很糟糕。
所以我正在寻找的代码是我可以概括到任何长度的代码(不需要这么做,只需要我可以弄清楚如何),哪个clang以一种有效的方式生成添加它确实与它内置128位类型(不幸的是,我不能轻易概括)。我认为这应该只是一个adc
的链,但我欢迎参数和代码,它应该是其他东西。
答案 0 :(得分:23)
有一个固有的做法:_addcarry_u64。但是,只有Visual Studio和ICC(至少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微体系结构以来,有两条新说明:adcx
和adox
,您可以使用_addcarryx_u64内在函数访问它们。英特尔针对这些内在函数的文档曾经是different then the assembly produced by the compiler,但现在看来他们的文档是正确的。但是,Visual Studio仍然只显示带有adcx
的{{1}},而ICC使用此内在产生_addcarryx_u64
和adcx
。但即使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:上面的所有示例都只编译为mov
,add
和adc
指令(从clang 5.0开始,至少是-O2)。
这些示例不能用gcc生成好的代码(最多8.1,目前是godbolt上的最高版本)。
而我还没有设法使用__builtin_addcll
...