我不完全确定这是否是您要找的东西,而且我的组装技能绝对不是最好的(例如缺少后缀),但这使用ADC
并且应该可以解决您的问题。
注意 C++ for 循环的省略;我们需要在 asm 中循环,因为我们需要CF
在迭代中生存下来。(GCC6 有标志输出约束,但没有标志输入;没有办法要求编译器将 FLAGS 从一个 asm 语句传递到另一个,即使有语法,gcc 也可能使用 setc/cmp 效率低下。)
#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
和clobber 是必要的"memory"
,因为我们只向编译器询问指针输入,而不告诉它我们实际读取和写入的内存。
在某些较旧的 CPU 上,尤其是 Core2 / Nehalem,adc
之后inc
会导致部分标志停止。请参阅某些 CPU 上紧密循环中的 ADC/SBB 和 INC/DEC 问题。但在现代 CPU 上,这是有效的。
编辑:正如@PeterCordes所指出的,我的inc %rax
和使用 lea 缩放 8 的效率非常低(现在我想起来很愚蠢)。现在,简直了lea 8(%rax), %rax
。
编者注:我们可以通过使用数组末尾的负索引来保存另一条指令,使用 . 向上计数到 0 inc / jnz
。
(这会将数组大小硬编码为 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
理想情况下,我们会为 the或 store使用单寄存器寻址模式,可能ans
通过减去输入上的指针来相对于 索引其他两个数组。
但是我们需要一个单独的 LEA 来增加 8,因为我们仍然需要避免破坏 CF。尽管如此,在 Haswell 和更高版本上,索引商店不能在端口 7 上使用 AGU,而 Sandybridge/Ivybridge 它们未层压到 2 微秒。因此,对于英特尔 SnB 系列,在此处避免索引存储会很好,因为每次迭代我们需要 2x 加载 + 1x 存储。请参阅微融合和寻址模式
早期的 Intel CPU (Core2 / Nehalem) 在上述循环中会出现部分标志停顿,因此上述问题与它们无关。
AMD CPU 可能对上述循环没问题。 Agner Fog 的优化和微架构指南没有提到任何严重的问题。
不过,对于 AMD 或英特尔来说,展开一点不会有什么坏处。