adcx 和 adox 的测试用例

Test case for adcx and adox

我正在测试 Intel ADX add with carry and add with overflow to pipeline adds on large integers. I'd like to see what expected code generation should look like. From _addcarry_u64 and _addcarryx_u64 with MSVC and ICC,我认为这将是一个合适的测试用例:

#include <stdint.h>
#include <x86intrin.h>
#include "immintrin.h"

int main(int argc, char* argv[])
{
    #define MAX_ARRAY 100
    uint8_t c1 = 0, c2 = 0;
    uint64_t a[MAX_ARRAY]={0}, b[MAX_ARRAY]={0}, res[MAX_ARRAY];
    for(unsigned int i=0; i< MAX_ARRAY; i++){ 
        c1 = _addcarryx_u64(c1, res[i], a[i], (unsigned long long int*)&res[i]);
        c2 = _addcarryx_u64(c2, res[i], b[i], (unsigned long long int*)&res[i]);
    }
    return 0;
}

当我使用 -O3-madx 检查 generated code from GCC 6.1 时,它显示序列化 addc-O1-O2 产生相似的结果:

main:
        subq    8, %rsp
        xorl    %edi, %edi
        xorl    %esi, %esi
        leaq    -120(%rsp), %rdx
        xorl    %ecx, %ecx
        leaq    680(%rsp), %r8
.L2:
        movq    (%rdx), %rax
        addb    $-1, %sil
        adcq    %rcx, %rax
        setc    %sil
        addb    $-1, %dil
        adcq    %rcx, %rax
        setc    %dil
        movq    %rax, (%rdx)
        addq    , %rdx
        cmpq    %r8, %rdx
        jne     .L2
        xorl    %eax, %eax
        addq    8, %rsp
        ret

所以我猜测试用例没有完全达到目标,或者我做错了什么,或者我使用的东西不正确,...

如果我在 _addcarryx_u64 上正确解析英特尔文档,我相信 C 代码应该生成管道。所以我猜我做错了什么:

Description

Add unsigned 64-bit integers a and b with unsigned 8-bit carry-in c_in (carry or overflow flag), and store the unsigned 64-bit result in out, and the carry-out in dst (carry or overflow flag).

如何生成管道添加 carry/add 溢出 (adcx/adox)?


实际上我已经准备好测试第 5 代酷睿 i7(注意 adx cpu 标志):

$ cat /proc/cpuinfo | grep adx
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush
dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc
arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni
pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1
sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm
3dnowprefetch ida arat epb pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase
tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx smap xsaveopt
...

这看起来确实是一个很好的测试用例。它 assemble 可以更正工作代码,对吗?从这个意义上说,编译器支持内在函数是很有用的,即使它还不支持制作最佳代码。它让人们开始使用内在的。这是兼容性所必需的。

明年或每当编译器对 adcx/adox 的后端支持完成时,相同的代码将编译为更快的二进制文件,而无需修改源代码。

我想这就是 gcc 正在发生的事情。


clang 3.8.1 的实现更加直白,但它最终做了一个糟糕的工作:使用 sahf 和 eax 的 push/pop 保存标志。 See it on Godbolt.

我认为 asm 源代码输出中甚至存在错误,因为 mov eax, ch 不会 assemble。 (与 gcc 不同,clang/LLVM 使用内置的 assembler,并且在从 LLVM IR 到机器代码的过程中实际上并不经过 asm 的文本表示)。机器代码的反汇编在那里显示 mov eax,ebp 。我认为这也是一个错误,因为 bpl (或寄存器的其余部分)此时没有有用的值。可能它想要 mov al, chmovzx eax, ch.

当 GCC 将修复为 add_carryx_... 生成更好的内联代码时,请小心您的代码,因为循环变体包含比较(修改 C 和 O 标志,类似于 sub 指令) 和增量(像添加指令一样修改 C 和 O 标志)。

  for(unsigned int i=0; i< MAX_ARRAY; i++){ 
        c1 = _addcarryx_u64(c1, res[i], a[i], (unsigned long long int*)&res[i]);
        c2 = _addcarryx_u64(c2, res[i], b[i], (unsigned long long int*)&res[i]);
    }

出于这个原因,您代码中的 c1 和 c2 将始终被可怜地处理(在每次循环迭代时保存和恢复到临时寄存器中)。出于充分的理由,由 gcc 生成的结果代码仍然看起来像您提供的程序集。

从运行时间的角度来看,res[i]是2条add_carryx指令之间的直接依赖关系,这2条指令并不是真正独立的,不会受益于可能的处理器中的架构并行性。

我理解代码只是一个示例,但可能不是修改 gcc 时使用的最佳示例。

大整数运算中3个数的相加是一道难题;矢量化有帮助,然后你最好使用 addcarryx 来并行处理循环变体(增量和比较+同一变量上的分支,又是一个棘手的问题)。