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

时间:2016-12-09 00:29:58

标签: c++ assembly

我想做一个快速代码,用于在大整数中添加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 ++代码中将参数传递给汇编程序。

1 个答案:

答案 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,以避免执行CMPDEC未设置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来说,展开一点也不会有伤害。