2016-09-04 29 views
2

我在測試Intel ADX添加進位並添加溢出到流水線添加大整數。我想看看預期的代碼生成應該是什麼樣子。從_addcarry_u64 and _addcarryx_u64 with MSVC and ICC,我認爲這將是一個合適的測試案例:adcx和adox的測試用例

#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代碼應該生成管道。所以我猜我做錯了什麼:

說明

帶符號的8位進位無符號添加64位整數a和b C_IN (攜帶或溢出標誌),並將無符號的64位結果輸出, 和dst(進位或溢出標誌)的執行結果。

怎樣才能生成pipeline'd添加進位/添加溢出(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 
... 
+0

我認爲這些內在函數大多存在,因爲MSVC不允許在64位模式下進行內聯彙編。有了GCC,在這種情況下,您需要使用內聯彙編。事實上,使用GCC已經存在數十年的'adc'的最好方法是內聯彙編。將內聯彙編作爲選項很不錯,但它太糟糕了,比如PITA在GCC中使用它。 –

回答

1

這看起來像一個很好的測試案例。它彙編糾正工作代碼,對吧?對於編譯器而言,即使它尚不支持製作最佳代碼,它也可以支持這種內在意義。它可以讓人們開始使用本質。這對於兼容性是必需的。

明年或當編譯器對adcx/adox的後端支持完成時,相同的代碼將編譯爲速度更快的二進制文件,而不進行源修改。

我認爲這就是gcc的情況。


鐺3.8.1的實現更字面,但它結束了做一個可怕的工作:與SAHF和推/ EAX的流行標誌節能。 See it on Godbolt

我想在asm源碼輸出中還有一個bug,因爲mov eax, ch不會組裝。 (與gcc不同的是,clang/LLVM使用內置的彙編程序,實際上並沒有通過從LLVM IR到機器代碼的方式的文本表示形式)。機器代碼的反彙編在那裏顯示mov eax,ebp。我認爲這也是一個錯誤,因爲bpl(或其他註冊表)在這一點上沒有有用的價值。可能它想要mov al, chmovzx eax, ch

+0

更新:clang3.9和4.0在該來源崩潰,clang5.0合理編譯它。 (僅使用adcx,但具有足夠的展開以通過分別保存/恢復每個鏈的進位來啓用ILP。) –

0

當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在你的代碼將永遠pitifuly處理(保存,並在每次循環迭代在臨時寄存器中恢復)。並且由gcc生成的結果代碼仍然看起來像您提供的程序集,原因很充分。

從運行時的角度來看,res [i]是2條add_carryx指令之間的直接依賴關係,2條指令並不真正獨立,並且不會從處理器中可能的體系結構並行中受益。

我知道代碼僅僅是一個例子,但是當gcc被修改時,它可能不是最好的例子。

在大整數算術中增加3個數字是一個棘手的問題;矢量化可以幫助你,然後你最好使用addcarryx來並行處理循環變量(增量和比較+相同變量的分支,又一個棘手的問題)。

+0

clang5.0展開足夠有用的循環。 (https://godbolt.org/g/2NTfVs)實際上這是一個有趣的測試,讓第二個進位鏈依賴於第一個進位鏈。但請注意,它只是一個單向依賴項:'res [] + = a []'鏈可以在res [] + = b []'鏈之前運行,這就是clang所做的。 (然後當它們仍然在寄存器中時,重用這4個'res []'值)。 –

+0

好處是這需要循環展開以避免每次迭代都進行保存/恢復(除非循環沒有標記,使用'lea'和'jrcxz ''或'loop',[但那些不幸的是除了在AMD上效率不高](https://stackoverflow.com/questions/35742570/why-is-the-loop-instruction-slow-couldnt-intel-have-實現效率) –

+0

感謝鏈接godbolts。查看由不同編譯器生成的不同代碼,adcx被用作adc,並且adox不被使用。你說得對,展開幾次迭代,2依賴鏈可以交錯,並且pushf/popf可以用於在循環變體時間保存/恢復兩個標誌..... – Pierre