使用进位标志进行多位数加法

16
GCC拥有128位整数。使用这些整数,我可以让编译器使用mul(或只有一个操作数的imul)指令。例如:
uint64_t x,y;
unsigned __int128 z = (unsigned __int128)x*y;

产生mul。 我用它创建了一个128x128到256的函数(如果您感兴趣,请参见本问题末尾的代码)。

现在我想进行256位加法,但我没有找到编译器使用ADC的方法,除非使用汇编语言。 我可以使用汇编程序,但我希望使用内联函数提高效率。 编译器已经为128x128到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)) 

(可能不是每个输出都需要 early clobber modifier,但如果没有至少最后两个,我会得到错误的结果)。 (编辑注:只有在读取所有输入之后才会编写最后一个输出,并且可以安全地不声明为early-clobber。)

这里是一个用C语言实现同样功能的函数

void add256(int256 *x, int256 *y) {
    uint64_t t1, t2;
    t1 = x->x1; x->x1 += y->x1;
    t2 = x->x2; x->x2 += y->x2 + ((x->x1) < t1);
    t1 = x->x3; x->x3 += y->x3 + ((x->x2) < t2);
                x->x4 += y->x4 + ((x->x3) < t1);
}

为什么需要汇编?为什么编译器不能将add256函数编译成使用进位标志的形式?有没有办法强制编译器这样做(例如,我可以更改add256以实现此目的)?如果编译器不支持内联汇编怎么办(需要全部用汇编语言编写函数吗)?为什么没有针对此功能的内在函数?这是128x128到256函数:
void muldwu128(int256 *w, uint128 u, uint128 v) {
   uint128 t;
   uint64_t u0, u1, v0, v1, k, w1, w2, w3;

   u0 = u >> 64L;
   u1 = u;
   v0 = v >> 64L;
   v1 = v;

   t = (uint128)u1*v1;
   w3 = t;
   k = t >> 64L;

   t = (uint128)u0*v1 + k;
   w2 = t;
   w1 = t >> 64L;
   t = (uint128)u1*v0 + w2;
   k = t >> 64L;

   w->hi = (uint128)u0*v0 + w1 + k;
   w->lo = (t << 64L) + w3;

}

一些类型定义:
typedef          __int128  int128;
typedef unsigned __int128 uint128;

typedef union {
    struct {
        uint64_t x1;
        uint64_t x2;
         int64_t x3;
         int64_t x4;
    };
    struct {
        uint128 lo;
         int128 hi;
    };
} int256;

更新:

我的问题在很大程度上是以下问题的重复:

  1. 如何让GCC使用带进位逻辑的任意精度算术而不需要内联汇编
  2. 如何高效地进行128位加法并使用进位标志
  3. C中的多字加法

英特尔有一篇很好的文章(新指令支持大整数算术),介绍了大整数算术和三个新指令MULX、ADCX、ADOX。他们写道:

“mulx”,“adc x”和“adox”的内在定义也将集成到编译器中。这是第一个使用内在函数实现“带进位加法”类型指令的示例。内在函数的支持将使用户能够使用高级编程语言(例如C/C++)实现大整数算术。
unsigned __int64 umul128(unsigned __int64 a, unsigned __int64 b, unsigned __int64 * hi);
unsigned char _addcarry_u64(unsigned char c_in, unsigned __int64 a, unsigned __int64 b, unsigned __int64 *out);
unsigned char _addcarryx_u64(unsigned char c_in, unsigned __int64 a, unsigned __int64 b, unsigned __int64 *out);

顺便提一下,MSVC已经有了_umul128内置函数。因此,即使MSVC没有__int128,也可以使用_umul128内置函数生成mul,从而进行128位乘法。

MULX在BMI2(Haswell)中出现。自Broadwell以来,ADCXADOX指令作为ADX扩展可用。遗憾的是,自1979年8086以来就有了ADC,但没有内置函数。这将解决内联汇编问题。

(编辑注:英特尔的指令集指南确实为基线 x86-64 定义了 _addcarry_u64,但可能并非所有编译器都实现了它。然而,gcc 通常会低效地编译它和/或 _addcarryx,经常将 CF 溢出到带有 setc 的整数中,而不是更好地排序指令。)

GCC 的 __int128 代码生成使用 mulx,如果启用了 BMI2(例如使用 -mbmi2-march=haswell)。

编辑:

我尝试了 Lưu Vĩnh Phúc 建议的 Clang 的带进位加法内置函数。

void add256(int256 *x, int256 *y) {
    unsigned long long carryin=0, carryout;
    x->x1 = __builtin_addcll(x->x1, y->x1, carryin, &carryout); carryin = carryout;
    x->x2 = __builtin_addcll(x->x2, y->x2, carryin, &carryout); carryin = carryout;
    x->x3 = __builtin_addcll(x->x3, y->x3, carryin, &carryout); carryin = carryout;
    x->x4 = __builtin_addcll(x->x4, y->x4, carryin, &carryout);  
}

但是这并没有产生ADC,而且比我预想的更加复杂。

1
@Ulfalizer 是的,但仅适用于在128位加法的64位部分之间传递进位。我无法使用adc在128位部分之间传播进位。 - Jester
2
@Zboson:“带进位加法”代码生成需要编译器理解你实际上正在进行多精度算术(没有明显的符号表明,仅仅因为你命名了一个类型int1024并不意味着编译器能够理解你的意图),或者你表达出一个加法产生了两个结果,一个和与进位,并且你想在另一个加法操作中使用那个进位,例如,loworderwordA+=lowerorderwordB; highorderwordA+=highorderwordB+lastcarry(); C语言中没有符号可以表示最后的进位。... - Ira Baxter
1
@Zboson:就瘙痒而言,人们已经构建了非常好的多精度包。 (我听说GNU有一个相当不错的,但我不记得它的名字)。 显然,他们并没有受到足够的冒犯来修复编译器,但是他们的实现很难被击败。 因此,这实际上是一个问题,“是否值得明确支持这个功能,因为它很少使用?” 通常的答案是否定的,特别是如果有可行的替代方案。 - Ira Baxter
4
GMP的制作者已放弃让GCC(尽管是旧版本)发出adc指令:https://gmplib.org/manual/Assembly-Carry-Propagation.html - Iwillnotexist Idonotexist
2
很不幸,直接使用C语言的情况比你想象的更糟:你发布的add256 C代码在某些情况下无法正常工作。具体来说,考虑当x->x1 = 1,x->x2=x->x3=x->x4 = 0,y->y1=y->y2=y->y3=y->y4 = 0xFFffFFffFFffFFff时的情况。进位应该传播以使结果为全零,但t2最终变成了零,使得进一步传播变得不可能。在直接使用C语言中,唯一可能的修复方法都很丑陋且缓慢。 - user3535668
显示剩余26条评论
1个回答

4
我使用ICC 13.0.01找到了一个解决方案,其中使用了_addcarry_u64内置函数。
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);
}

产生

L__routine_start_add256_0:
add256:
        xorl      %r9d, %r9d                                    #25.9
        movq      (%rsi), %rax                                  #22.9
        addq      %rax, (%rdi)                                  #22.9
        movq      8(%rsi), %rdx                                 #23.9
        adcq      %rdx, 8(%rdi)                                 #23.9
        movq      16(%rsi), %rcx                                #24.9
        adcq      %rcx, 16(%rdi)                                #24.9
        movq      24(%rsi), %r8                                 #25.9
        adcq      %r8, 24(%rdi)                                 #25.9
        setb      %r9b                                          #25.9
        ret                                                     #26.1

我使用了-O3编译。我不知道如何在ICC中启用adx,也许需要ICC 14吗?

这正是我期望的1个addq和3个adcq

使用Clang并使用-O3 -madx时结果一团糟。

add256(uint256*, uint256*):                  # @add256(uint256*, uint256*)
movq    (%rsi), %rax
xorl    %ecx, %ecx
xorl    %edx, %edx
addb    $-1, %dl
adcq    %rax, (%rdi)
addb    $-1, %cl
movq    (%rdi), %rcx
adcxq   %rax, %rcx
setb    %al
movq    8(%rsi), %rcx
movb    %al, %dl
addb    $-1, %dl
adcq    %rcx, 8(%rdi)
addb    $-1, %al
movq    8(%rdi), %rax
adcxq   %rcx, %rax
setb    %al
movq    16(%rsi), %rcx
movb    %al, %dl
addb    $-1, %dl
adcq    %rcx, 16(%rdi)
addb    $-1, %al
movq    16(%rdi), %rax
adcxq   %rcx, %rax
setb    %al
movq    24(%rsi), %rcx
addb    $-1, %al
adcq    %rcx, 24(%rdi)
retq

如果不在Clang中启用-madx,结果并不会好多少。

编辑: 显然MSVC已经有了_addcarry_u64。我尝试过它,它和ICC一样好(1个add和3个adc)。


ADCX不在BMI2中而是在ADX中,因此当我尝试时ICC无法发出ADCX。GCC似乎无法理解内部的_addcarry_u64https://gcc.godbolt.org/。 - phuclv
@LưuVĩnhPhúc,你是对的。我不知道如何在ICC中启用ADX(-madx),这在ICC 13中无法工作。然而,我可以在Clang中启用它,但从Clang得到的结果仍然很糟糕。 - Z boson
1
顺便提一下,在GCC 5.1中添加了_addcarry_u64()。但是它有缺陷。截至5.2,它仍然有缺陷:http://coliru.stacked-crooked.com/a/28a776c89af0588c 看起来涉及跨循环迭代保存进位位的任何内容都会出现问题。 - Mysticial
1
顺便说一句,GCC 5.3修复了这个错误。但是它为那些内置函数生成的代码非常糟糕,你最好还是避免使用它们。 - Mysticial
2
说得太早了。_subborrow_u64 在 MSVC/ICC 和 GCC 之间的行为似乎不一致。MSVC 和 ICC 执行 src1 - src2。GCC 执行 src2 - src1。英特尔的内置参考指南说执行 src2 - src1。哈哈... - Mysticial
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接