C++ 使用英特尔内联汇编程序编写带进位的bigint add
我想编写一个在大整数中添加64位数字的快速代码:C++ 使用英特尔内联汇编程序编写带进位的bigint add,c++,assembly,x86,bigint,carryflag,C++,Assembly,X86,Bigint,Carryflag,我想编写一个在大整数中添加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]; 但是我不知道如何从C++代码的其余部分传递参数给汇编程序。 但上述方法不适用于carry 如果您的意思是GCC不生成使用ADC指令的代码,那是因为它的优化器已经确定有一种更优化的方法来实现添加 这
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];
<>但是我不知道如何从C++代码的其余部分传递参数给汇编程序。
但上述方法不适用于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];
}
}
无效测试(uint64\u t*a、uint64\u t*b、uint64\u t*ans、int n)
{
对于(int i=0;i
现在,事实上,当你用现代版本的GCC编译这个,你会看到一堆看起来很疯狂的代码
Godbolt编译器资源管理器非常有用,它可以对C源代码的行及其相应的汇编代码进行颜色编码(或者至少,它已经尽了最大的能力;这在优化代码中并不完美,但在这里它工作得足够好)。紫色代码是在循环的内部体中实现64位加法的代码。GCC正在发出SSE2指令来进行添加。具体来说,您可以选择(将双四字从内存未对齐地移动到XMM寄存器中),(对压缩整数四字进行加法)和(将四字从XMM寄存器移动到内存中)。粗略地说,对于非汇编专家,MOVDQU
是如何加载64位整数值的,PADDQ
进行加法,然后MOVQ
存储结果
使此输出特别嘈杂和混乱的部分原因是GCC正在展开for
循环。如果禁用循环展开(-fno tree vectorize
),则会得到,尽管它仍然使用相同的指令执行相同的操作。(嗯,大部分是这样。现在它在任何地方都使用MOVQ
,用于加载和存储,而不是使用MOVDQU
加载)
另一方面,如果明确禁止编译器使用SSE2指令(-mno-SSE2
)。现在,因为它不能使用SSE2指令,它发出基本的x86指令来进行64位加法,唯一的方法是ADD
+ADC
我怀疑这就是您希望看到的代码。显然,GCC相信将操作矢量化会导致更快的代码,因此当您使用-O2
或-O3
标志编译时,它就是这样做的。在-O1
,它总是使用ADD
+ADC
。这是指令更少并不意味着代码更快的情况之一。(或者至少,GCC不这么认为。实际代码上的基准测试可能说明不同的情况。在某些人为的场景中,开销可能很大,但在现实世界中并不重要。)
值得一提的是,Clang的行为方式与GCC非常相似
如果您的意思是此代码没有将上一次加法的结果传递到下一次加法,那么您是对的。您展示的第二段代码实现了该算法,并且 至少在针对x86-32时是这样。当以x86-64为目标时,您可以使用本机64位整数寄存器,甚至不需要“携带”,需要更少的代码。事实上,这只是32位体系结构上的“bigint”算法,这就是为什么我在前面的所有分析和编译器输出中都假设x86-32 在一篇评论中,Ped7g想知道为什么编译器似乎没有
ADD
+ADC
chain习惯用法的概念。我不完全确定他在这里指的是什么,因为他没有分享任何他尝试过的输入代码示例,但正如我所展示的,编译器确实在这里使用ADC
指令。然而,编译器不会在循环迭代中进行链进位。这在实践中很难实现,因为有太多指令清除了标志。手工编写汇编代码的人可能可以做到这一点,但编译器却不行
(请注意,c
可能应该是一个无符号整数,以鼓励某些优化。在这种情况下,它只是确保GCC在准备进行64位加法时使用XOR
指令,而不是CDQ
。虽然速度稍快,但不是很大的改进,但里程数可能会随实际代码而变化。)
(另外,令人失望的是,GCC无法在循环内部发出设置c
的无分支代码。如果输入值足够随机,分支预测将失败,最终会得到效率相对较低的代码。几乎可以肯定,有编写c源代码的方法可以说服GCC发出无分支代码,但这是一个错误。)(答案完全不同。)
我想学习如何嵌入内联(英特尔)汇编,并做得更快 好的,我们已经看到,如果您天真地导致发出一堆
ADC
指令,那么它可能不一定会更快。除非您确信关于性能的假设是正确的,否则不要手动优化
此外,内联程序集不仅难以编写、调试和维护,甚至可能会使您的代码速度变慢,因为它会抑制某些本来可以由编译器完成的优化。您需要能够证明,手工编写的程序集在性能上比编译器生成的程序集有足够的优势您还应该确认,通过改变标志或巧妙地编写C源代码,无法让编译器生成接近理想输出的代码
但是,您可以阅读各种在线教程,这些教程教您如何在中使用GCC
add eax, ebx
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];
}
}
#include <cstdint>
#include <iostream>
#define N 4
int main(int argc, char *argv[]) {
uint64_t ans[N];
const uint64_t a[N] = {UINT64_MAX, UINT64_MAX, 0, 0};
const uint64_t b[N] = {2, 1, 3, 1};
const uint64_t i = N;
asm volatile (
"xor %%eax, %%eax\n\t" // i=0 and clear CF
"mov %3, %%rdi\n\t" // N
".L_loop:\n\t"
"mov (%%rax,%1), %%rdx\n\t" // rdx = a[i]
"adc (%%rax,%2), %%rdx\n\t" // rdx += b[i] + carry
"mov %%rdx, (%%rax, %0)\n\t"// ans[i] = a[i] + b[i]
"lea 8(%%rax), %%rax\n\t" // i += 8 bytes
"dec %%rdi\n\t" // --i
"jnz .L_loop\n\t" // if (rdi == 0) goto .L_loop;
: /* Outputs (none) */
: /* Inputs */ "r" (ans), "r" (a), "r" (b), "r" (i)
: /* Clobbered */ "%rax", "%rbx", "%rdx", "%rdi", "memory"
);
// SHOULD OUTPUT 1 1 4 1
for (int i = 0; i < N; ++i)
std::cout << ans[i] << std::endl;
return 0;
}
// untested
asm volatile (
"mov $-3, %[idx]\n\t" // i=-3 (which we will scale by 8)
"mov (%[a]), %%rdx \n\t"
"add (%[b]), %%rdx \n\t" // peel the first iteration so we don't have to zero CF first, and ADD is faster on some CPUs.
"mov %%rdx, (%0) \n\t"
".L_loop:\n\t" // do{
"mov 8*4(%[a], %[idx], 8), %%rdx\n\t" // rdx = a[i + len]
"adc 8*4(%[b], %[idx], 8), %%rdx\n\t" // rdx += b[i + len] + carry
"mov %%rdx, 8*4(%[ans], %[idx], 8)\n\t" // ans[i] = rdx
"inc %[idx]\n\t"
"jnz .L_loop\n\t" // }while (++i);
: /* Outputs, actually a read-write input */ [idx] "+&r" (i)
: /* Inputs */ [ans] "r" (ans), [a] "r" (a), [b] "r" (b)
: /* Clobbered */ "rdx", "memory"
);