我想做一个快速代码,用于在大整数中添加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];
}
但是,我想学习如何嵌入内联(英特尔)程序集并更快地完成它。 我确信有64位操作码,相当于:
add eax, ebx
adc ...
但我不知道如何从其余的c ++代码中将参数传递给汇编程序。
答案 0 :(得分:2)
我不能完全确定这是否是您想要的东西,并且我的组装技能绝对不是最好的(例如,缺少后缀),但这可以使用ADC
并且可以解决您的问题。 / p>
请注意省略了C ++ for循环;我们需要循环访问asm,因为我们需要CF
才能在迭代中生存。 (GCC6有标志输出约束,但没有标志输入;没有办法要求编译器将FLAGS从一个asm语句传递到另一个asm语句,即使有语法,gcc可能也会用setc / cmp效率低下。)< / p>
#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;
}
为了避免设置carry flag (CF)
,我需要倒数至0,以避免执行CMP
。 DEC
未设置carry flag
,因此它可能是此应用程序的理想竞争者。 但是,我不知道如何使用 %rdi
从数组的开头索引比inc %rax
所需的额外指令和注册要快。
volatile
和"memory"
遮盖符是必需的,因为我们只要求编译器提供指针输入,而不告诉它我们实际读写哪个内存。
在某些较旧的CPU(尤其是Core2 / Nehalem)上,adc
之后的inc
将导致partial-flag stall。参见Problems with ADC/SBB and INC/DEC in tight loops on some CPUs。但是在现代CPU上,这是有效的。
编辑:
正如@PeterCordes所指出的那样,我的inc %rax
和lea的8倍缩放效率极低(现在考虑到这是愚蠢的)。现在,它就是lea 8(%rax), %rax
。
编者注:我们可以通过使用数组末尾的负索引来保存另一条指令,并使用inc / jnz
朝0计数。
(这会将数组大小硬编码为4。您可以通过将数组长度要求为立即数,并以-i
作为输入,或者要求提供指向末尾的指针,来使其更加灵活。 )
// 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"
);
万一GCC复制了此代码,循环标签可能应该使用%%=
,或使用1:
这样的带编号的本地标签
像以前使用的那样,使用缩放索引寻址模式并不比常规索引寻址模式(2个寄存器)昂贵。理想情况下,我们将对adc
或商店使用单寄存器寻址模式,也许可以通过减去输入上的指针来索引相对于ans
的其他两个数组。
但是我们需要一个单独的LEA来增加8,因为我们仍然需要避免破坏CF。尽管如此,在Haswell及更高版本上,索引商店仍无法在端口7上使用AGU,而Sandybridge / Ivybridge则将它们分层为2 oups。因此,对于英特尔SnB系列而言,在这里避免建立索引存储将是一件好事,因为我们每次迭代需要2倍负载+ 1倍存储。参见Micro fusion and addressing modes
早期的Intel CPU(Core2 / Nehalem)在上述循环中将出现部分标志停顿,因此上述问题与它们无关。
AMD CPU可能适合上述循环。 Agner Fog's optimization and microarch guides不要提及任何严重的问题。
不过,对于AMD或Intel来说,展开一点也不会有伤害。