这样做有一个内在因素:_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 使用内联汇编。
adcx
请注意,由于 Broadwell 微体系结构有两个新指令:adox
您可以使用_addcarryx_u64内部函数访问它们。英特尔对这些内在函数的文档过去与编译器生成的程序集不同,但现在看来它们的文档是正确的。但是,Visual Studio 似乎仍然只生成adcx
with_addcarryx_u64
而 ICC 生成两者adcx
和adox
与这个内在。但即使 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"
);
}