adcx 和 adox 的测试用例
Posted
技术标签:
【中文标题】adcx 和 adox 的测试用例【英文标题】:Test case for adcx and adox 【发布时间】:2016-09-04 19:58:23 【问题描述】:我正在测试Intel ADX 带进位加法和带溢出加法到管道加法上的大整数。我想看看预期的代码生成应该是什么样子。来自_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 $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(注意 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
...
【问题讨论】:
我认为这些内在函数主要存在,因为 MSVC 不允许在 64 位模式下进行内联汇编。在这种情况下,您应该使用 GCC 使用内联汇编。事实上,使用 GCC 已经存在了几十年的adc
的最佳方式是内联汇编。将内联汇编作为一个选项很好,但它太糟糕了,比如 PITA 在 GCC 中使用。
【参考方案1】:
这看起来确实是一个很好的测试用例。它组装以纠正工作代码,对吗?从这个意义上说,编译器支持内在函数很有用,即使它还不支持制作最佳代码。它让人们开始使用内在函数。这是兼容性所必需的。
明年或每当编译器对 adcx/adox 的后端支持完成时,相同的代码将编译为更快的二进制文件,而无需修改源代码。
我认为这就是 gcc 的情况。
clang 3.8.1 的实现更加字面化,但它最终做得很糟糕:使用 sahf 和 eax 的 push/pop 保存标志。 See it on Godbolt.
我认为 asm 源输出中甚至存在错误,因为 mov eax, ch
不会汇编。 (与 gcc 不同,clang/LLVM 使用内置汇编程序,并且在从 LLVM IR 到机器代码的过程中实际上并没有通过 asm 的文本表示)。机器代码的反汇编显示 mov eax,ebp
那里。我认为这也是一个错误,因为bpl
(或寄存器的其余部分)在那时没有有用的价值。可能它想要mov al, ch
或movzx eax, ch
。
【讨论】:
更新:clang3.9 和 4.0 在该源上崩溃,clang5.0 可以合理编译。 (仅使用 adcx,但有足够的展开以通过分别保存/恢复每个链的进位来启用 ILP。)【参考方案2】:当 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 并行处理循环变体(在同一变量上进行增量和比较+分支,这是另一个棘手的问题)。
【讨论】:
clang5.0 展开循环足够有用。 (godbolt.org/g/2NTfVs) 让第二个进位链依赖于第一个实际上是一个有趣的测试。但请注意,这只是一种单向依赖:res[] += a[]
链可以运行在 res[] += b[]
链之前,这就是 clang 所做的。 (然后在它们仍在寄存器中时重用这 4 个 res[]
值。)
好点,这需要循环展开以避免保存/恢复每次迭代的进位(除非你在没有标志的情况下循环,使用lea
和jrcxz
,或loop
,but those are unfortunately not as efficient except on AMD
感谢上帝螺栓的链接。查看不同编译器生成的不同代码,使用 adcx 就好像它是 adc,而没有使用 adox。你是对的,通过展开几次迭代,两个依赖链可以交错,并且 pushf/popf 可以用于在循环变体时保存/恢复两个标志.....
popf
非常慢(例如,Haswell 上的吞吐量 = 18c)。 IDK 为什么它在 ring3 中那么慢,它不能更新 IF,只有 DF 和条件代码。但是LAHF / SAHF是单uop,是保存/恢复CF的更好选择。不过,编译器使用 setc
/ cmp
的方式可能至少一样好。
位 OF 位于 eflag 寄存器中的位置 11,LAHF/SAHF 无法访问。那么它必须是像 SETC al, SETO bl 这样的东西来保存 2 个标志。恢复两个标志的最短方法是什么? SHL bl,7 ;或 al, bl; RCR al,1(从位 7 ^ 0 设置 OF 和从位 0 设置 CF)以上是关于adcx 和 adox 的测试用例的主要内容,如果未能解决你的问题,请参考以下文章