![](/img/trans.png)
[英]Why does ubuntu ship with a memory allocator that is 2x slower than google tcmalloc?
[英]Why does tree vectorization make this sorting algorithm 2x slower?
如果在gcc(4.7.2)中啟用-fprofile-arcs
則此問題的排序算法會快兩倍(!)。 該問題的大量簡化的C代碼(事實證明我可以用全零來初始化數組,奇怪的性能行為仍然存在,但它使得推理更加簡單):
#include <time.h>
#include <stdio.h>
#define ELEMENTS 100000
int main() {
int a[ELEMENTS] = { 0 };
clock_t start = clock();
for (int i = 0; i < ELEMENTS; ++i) {
int lowerElementIndex = i;
for (int j = i+1; j < ELEMENTS; ++j) {
if (a[j] < a[lowerElementIndex]) {
lowerElementIndex = j;
}
}
int tmp = a[i];
a[i] = a[lowerElementIndex];
a[lowerElementIndex] = tmp;
}
clock_t end = clock();
float timeExec = (float)(end - start) / CLOCKS_PER_SEC;
printf("Time: %2.3f\n", timeExec);
printf("ignore this line %d\n", a[ELEMENTS-1]);
}
在使用優化標志很長一段時間之后,事實證明-ftree-vectorize
也會產生這種奇怪的行為,因此我們可以將-fprofile-arcs
排除在外。 在使用perf
進行分析后,我發現唯一相關的區別是:
快速案例gcc -std=c99 -O2 simp.c
(運行於3.1s)
cmpl %esi, %ecx
jge .L3
movl %ecx, %esi
movslq %edx, %rdi
.L3:
慢速gcc -std=c99 -O2 -ftree-vectorize simp.c
(運行於6.1s)
cmpl %ecx, %esi
cmovl %edx, %edi
cmovl %esi, %ecx
至於第一個片段:假設數組只包含零,我們總是跳轉到.L3
。 它可以從分支預測中大大受益。
我猜cmovl
指令不能從分支預測中受益。
問題:
以上所有猜測都是正確的嗎? 這會使算法變慢嗎?
如果是,我怎么能阻止gcc發出這條指令(當然除了瑣碎的-fno-tree-vectorization
解決方法之外),但仍然盡可能多地進行優化?
什么是-ftree-vectorization
? 文檔很模糊,我需要更多解釋來了解發生了什么。
更新:因為它出現在評論中: -ftree-vectorize
標志的奇怪性能行為保留隨機數據。 正如Yakk指出的那樣 ,對於選擇排序,實際上很難創建一個會導致很多分支錯誤預測的數據集。
既然它也出現了:我有一個Core i5 CPU。
根據Yakk的評論 ,我創建了一個測試。 下面的代碼( 在線沒有提升 )當然不再是排序算法; 我只拿出了內循環。 它唯一的目標是檢查分支預測的效果: 我們以概率p
跳過for
循環中的if
分支。
#include <algorithm>
#include <cstdio>
#include <random>
#include <boost/chrono.hpp>
using namespace std;
using namespace boost::chrono;
constexpr int ELEMENTS=1e+8;
constexpr double p = 0.50;
int main() {
printf("p = %.2f\n", p);
int* a = new int[ELEMENTS];
mt19937 mt(1759);
bernoulli_distribution rnd(p);
for (int i = 0 ; i < ELEMENTS; ++i){
a[i] = rnd(mt)? i : -i;
}
auto start = high_resolution_clock::now();
int lowerElementIndex = 0;
for (int i=0; i<ELEMENTS; ++i) {
if (a[i] < a[lowerElementIndex]) {
lowerElementIndex = i;
}
}
auto finish = high_resolution_clock::now();
printf("%ld ms\n", duration_cast<milliseconds>(finish-start).count());
printf("Ignore this line %d\n", a[lowerElementIndex]);
delete[] a;
}
感興趣的循環:
這將被稱為cmov
g++ -std=c++11 -O2 -lboost_chrono -lboost_system -lrt branch3.cpp
xorl %eax, %eax
.L30:
movl (%rbx,%rbp,4), %edx
cmpl %edx, (%rbx,%rax,4)
movslq %eax, %rdx
cmovl %rdx, %rbp
addq $1, %rax
cmpq $100000000, %rax
jne .L30
這將被稱為no cmov , Turix在他的回答中指出了-fno-if-conversion
標志。
g++ -std=c++11 -O2 -fno-if-conversion -lboost_chrono -lboost_system -lrt branch3.cpp
xorl %eax, %eax
.L29:
movl (%rbx,%rbp,4), %edx
cmpl %edx, (%rbx,%rax,4)
jge .L28
movslq %eax, %rbp
.L28:
addq $1, %rax
cmpq $100000000, %rax
jne .L29
差異並排
cmpl %edx, (%rbx,%rax,4) | cmpl %edx, (%rbx,%rax,4)
movslq %eax, %rdx | jge .L28
cmovl %rdx, %rbp | movslq %eax, %rbp
| .L28:
作為伯努利參數p
的函數的執行時間
帶有cmov
指令的代碼對p
完全不敏感。 如果p<0.26
或0.81<p
並且最多快4.38倍( p=1
),則沒有 cmov
指令的代碼是獲勝者。 當然,分支預測器的最壞情況是在p=0.5
左右,其中代碼比使用cmov
指令的代碼慢cmov
。
注意:圖表更新之前已回答問題; 這里的一些匯編代碼引用可能已經過時了。
(從我們上面的聊天中進行了改編和擴展,這足以激勵我做更多的研究。)
首先(根據我們的上述聊天),您的第一個問題的答案似乎是“是”。 在矢量“優化的”碼中,最優化(帶負)影響性能是分支predic 部件的位置 ,而在原始代碼的性能是(正)受分支預測 。 ' in the former.) (注意前者的額外' '。)
回答第3個問題:即使在你的情況下,實際上沒有進行矢量化,從步驟11(“條件執行”) 開始 ,似乎與矢量化優化相關的步驟之一是在目標循環內“平坦化”條件,喜歡循環中的這一點:
if (a[j] < a[lowerElementIndex]
lowerElementIndex = j;
顯然,即使沒有矢量化,也會發生這種情況。
這解釋了編譯器使用條件移動指令( cmovl
)的原因。 目標是完全避免分支(而不是試圖正確預測 )。 相反,兩個cmovl
指令將在前一個cmpl
的結果已知之前從管道向下發送,然后比較結果將被“轉發”以在它們的回寫之前啟用/阻止移動(即,在它們實際生效之前) )。
注意,如果循環已被矢量化,那么這可能是值得的,以便能夠有效地並行完成循環的多次迭代。
但是,在您的情況下,優化嘗試實際上是逆火,因為在展平循環中,兩個條件移動通過循環每次都通過管道發送。 這本身也可能不是那么糟糕,除了有一個RAW數據危險導致第二次移動( cmovl %esi, %ecx
)必須等到數組/內存訪問( movl (%rsp,%rsi,4), %esi
)完成,即使結果最終會被忽略。 因此花費在特定cmovl
上的巨大時間。 (我希望這是一個問題,你的處理器沒有足夠復雜的邏輯內置到其預測/轉發實現中來處理危險。)
另一方面,在非優化的情況下,正如您所知,分支預測可以幫助避免必須等待相應的數組/內存訪問的結果( movl (%rsp,%rcx,4), %ecx
指令)。 在這種情況下,當處理器正確地預測一個被采用的分支(對於一個全0的數組將是每一次,但是[偶數]在隨機數組中應該[仍然] 大致 超過 [編輯每@ Yakk的評論]一半的時間),它不必等待內存訪問完成繼續並在循環中排隊接下來的幾條指令。 因此,在正確的預測中,你得到了提升,而在不正確的預測中,結果並不比“優化”情況更差,而且更好,因為有時能夠避免在管道中使用2“浪費的” cmovl
指令。
[由於我根據您的評論錯誤地假設您的處理器,因此刪除了以下內容。] 回到你的問題,我建議查看上面的鏈接,了解更多有關矢量化的標志,但最后,我很確定忽略優化,因為你的Celeron無法使用它(在這種情況下)無論如何。
[刪除上面后添加]
重新提出你的第二個問題(“ ......我怎么能防止gcc發出這條指令... ”),你可以嘗試-fno-if-conversion
和-fno-if-conversion2
標志(不確定這些是否總能正常工作 - - 它們不再適用於我的mac),雖然我不認為你的問題通常是cmovl
指令(即,我不會總是使用那些標志),只是在這個特定的上下文中使用它(其中分支預測是鑒於@ Yakk關於排序算法的觀點,將會非常有用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.