c – 使用intel内联汇编程序代码bigint add with carry

我想做一个快速代码,用于在大整数中添加64位数字:

uint64_t ans[n];
uint64_t a[n], b[n]; // assume initialized values....
for (int i = 0; i < n; i++)
  ans[i] = a[i] + b[i];

但以上不适用于携带.

我在这里看到另一个问题,建议使用if语句检查哪个是优雅的:

ans[0] = a[0] + b[0];
int c = ans[0] < a[0];
for (int i = 0; i < n; i++) {
  ans[i] = a[i] + b[i] + c;
  c = ans[i] < a[i];
}

但是,我想学习如何嵌入内联(intel)程序集并更快地完成它.
我确信有64位操作码,相当于:

add eax, ebx
adc ...

但我不知道如何从c代码的其余部分将参数传递给汇编程序.

最佳答案

but the above does not work with carry.

如果您的意思是GCC不生成使用ADC指令的代码,那是因为它的优化器已经确定有更优化的方式来实现添加.

这是我的代码的测试版本.我已经将数组作为传递给函数的参数提取出来,因此代码不能被省略,我们可以将研究限制在相关部分.

void Test(uint64_t* a, uint64_t* b, uint64_t* ans, int n)
{
    for (int i = 0; i < n; ++i)
    {
        ans[i] = a[i] + b[i];
    }
}

实际上,当你用现代版本的GCC和look at the disassembly编译它时,你会看到一堆看起来很疯狂的代码.

Godbolt编译器浏览器非常有用,它可以对C源代码行及其相应的汇编代码进行颜色编码(或者至少,它能够尽其所能地执行此操作;这在优化代码中并不完美,但它运行良好这里).紫色代码是在循环的内部实体中实现64位加法的. GCC发出SSE2指令进行添加.具体来说,你可以选择MOVDQU(它将双四字的未对齐移动从存储器移动到XMM寄存器中),PADDQ(它对打包的整数四字组进行添加)和MOVQ(将四字从XMM寄存器移动到存储器中) ).粗略地说,对于非汇编专家,MOVDQU是如何加载64位整数值,PADDQ进行加法,然后MOVQ存储结果.

导致此输出特别嘈杂和混乱的部分原因是GCC正在展开for循环.如果你禁用循环展开(-fno-tree-vectorize),你得到output that is easier to read,虽然它仍然使用相同的指令做同样的事情. (嗯,大多数.现在它在任何地方使用MOVQ,无论是加载还是存储,而不是使用MOVDQU加载.)

另一方面,如果你特别禁止编译器使用SSE2指令(-mno-sse2),you see output that is significantly different.现在,因为它不能使用SSE2指令,它发出基本的x86指令来进行64位加法 – 并且唯一的方法是ADD ADC.

我怀疑这是你期望看到的代码.很明显,GCC认为向量化操作会产生更快的代码,因此这是使用-O2或-O3标志进行编译时的功能.在-O1,它总是使用ADD ADC.这是较少指令并不意味着更快代码的情况之一. (或者至少,GCC并不这么认为.实际代码的基准可能会讲述一个不同的故事.在某些人为设想的场景中,开销可能很大,但在现实世界中无关紧要.)

对于它的价值,Clang的行为方式与GCC在这里非常相似.

如果你的意思是这个代码没有将前一个添加的结果带到下一个添加,那么你是对的.您展示的第二段代码实现了该算法,以及GCC does compile this using the ADC instruction.

至少,它定位于x86-32.当定位x86-64时,您可以使用本机64位整数寄存器,甚至不需要“携带”; simple ADD instructions are sufficient,要求显着减少代码.实际上,这只是32位架构上的“bigint”算法,这就是为什么我在所有前面的分析和编译器输出中假设x86-32.

在评论中,Ped7g想知道为什么编译器似乎没有ADD ADC链成语的想法.我不完全确定他在这里指的是什么,因为他没有分享他尝试的输入代码的任何例子,但正如我所示,编译器在这里使用ADC指令.但是,编译器不会跨循环迭代进行链接.这在实践中实施起来太困难了,因为许多指令都清除了标志.手写汇编代码的人可能会这样做,但编译器不会.

(注意,c应该是一个无符号整数,以鼓励某些优化.在这种情况下,它只是确保GCC在准备进行64位加法而不是CDQ时使用XOR指令.虽然稍微快一点,但并不是一个巨大的改进,但里程可能因实际代码而异.)

(另外,令人失望的是,GCC无法发出无分支代码来在循环内部设置c.使用足够随机的输入值,分支预测将失败,并且最终会得到效率相对较低的代码.几乎可以肯定的是,写入方式用来说服GCC发出无分支代码的C源代码,但这是一个完全不同的答案.)

I would like to learn how to embed inline (intel) assembly and do it faster.

好吧,我们已经看到,如果您天真地导致发出一堆ADC指令,它可能不一定会更快.除非您确信您对性能的假设是正确的,否则不要手动优化!

此外,内联汇编不仅难以编写,调试和维护,而且甚至可能使代码变慢,因为它会抑制编译器本来可以完成的某些优化.您需要能够证明您的手动编码程序集具有足够的性能,胜过编译器生成的这些考虑因素变得不那么重要.您还应该确认无法通过更改标志或巧妙地编写C源来让编译器生成接近理想输出的代码.

但是if you really wanted to,你可以阅读各种在线教程,教你如何使用GCC的内联汇编程序. This is a pretty good one;还有很多其他的.当然,还有the manual.所有这些都将解释“extended asm”如何允许您指定输入操作数和输出操作数,这将回答您的问题“如何从其余的C代码将参数传递给汇编程序”.

正如paddy和Christopher Oicles建议的那样,你应该更喜欢内在函数来内联汇编.不幸的是,没有导致ADC指令发出​​的内在函数.内联汇编是你唯一的办法,或者我已经建议编写C源代码,这样编译器就可以独立完成Right Thing™.

但是有_addcarry_u32 and _addcarry_u64 intrinsics.这些会导致发出ADCX或ADOX指令. These are “extended” versions of ADC that can produce more efficient code.它们是Broadwell微体系结构中引入的Intel ADX指令集的一部分.在我看来,Broadwell没有足够高的市场渗透率,您可以简单地发出ADCX或ADOX指令并称之为一天.许多用户仍然使用旧机器,并且尽可能支持它们符合您的利益.如果您准备针对特定架构进行调整的构建版本,它们会很棒,但我不建议将其用于一般用途.

I am sure there are 64 bit opcodes, the equivalent of: add+adc

当您的目标是64位架构时,有64位版本的ADD和ADC(以及ADCX和ADOX).这将允许您使用相同的模式实现128位“bigint”算法.

在x86-32上,基本指令集中没有这些指令的64位版本.你必须转向SSE2,就像我们看到GCC和Clang那样.

点赞