adcx和adox的测试用例

我正在测试带有进位的英特尔ADX添加和添加溢出到管道添加大整数。 我想看看预期的代码生成应该是什么样子。 从_addcarry_u64和_addcarryx_u64与MSVC和ICC ,我认为这将是一个合适的测试用例:

#include  #include  #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检查从GCC 6.1生成的代码时 ,它显示了序列化的addc-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代码应该生成管道。 所以我猜我做错了什么:

描述

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

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


我实际上准备好了第5代Core 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 ... 

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

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

我认为gcc正在发生这种情况。


clang 3.8.1的实现更直观,但它最终做了一个糟糕的工作:使用sahf和push / pop of eax进行标记保存。 在Godbolt看到它 。

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

当GCC被修复为add_carryx _…生成更好的内联代码时,请小心你的代码,因为循环变量包含一个比较(类似于子指令修改C和O标志)和一个增量(修改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来并行处理循环变量(增量和比较+分支在同一个变量上,这是另一个棘手的问题)。

Interesting Posts