2

我正在测试带有进位的英特尔 ADX添加和带有溢出的添加到管道添加大整数。我想看看预期的代码生成应该是什么样子。从_addcarry_u64 和 _addcarryx_u64 与 MSVC 和 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;
}

当我使用and检查从 GCC 6.1 生成的代码时,它显示 serialized 。并产生类似的结果:-O3-madxaddc-O1-O2

main:
        subq    $688, %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    $8, %rdx
        cmpq    %r8, %rdx
        jne     .L2
        xorl    %eax, %eax
        addq    $688, %rsp
        ret

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

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

描述

将无符号 64 位整数 a 和 b 与无符号 8 位进位 c_in(进位或溢出标志)相加,并将无符号 64 位结果存储在 out 中,并将进位结果存储在 dst(进位或溢出标志)中。

如何生成带有进位添加/带有溢出(adcx/ adox)的管道?


我实际上已经准备好要测试的第 5 代 Core i7(注意adxcpu 标志):

$ 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
...
4

2 回答 2

1

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

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

我认为这就是 gcc 的情况。


clang 3.8.1 的实现更加字面化,但它最终做得很糟糕:使用 sahf 和 eax 的 push/pop 保存标志。 在 Godbolt 上看到它

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

于 2016-09-04T21:55:56.727 回答
0

当 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 并行处理循环变体(在同一变量上进行增量和比较+分支,这是另一个棘手的问题)。

于 2017-10-24T20:04:34.210 回答