簡體   English   中英

為什么樹矢量化使這種排序算法慢2倍?

[英]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指令不能從分支預測中受益。


問題:

  1. 以上所有猜測都是正確的嗎? 這會使算法變慢嗎?

  2. 如果是,我怎么能阻止gcc發出這條指令(當然除了瑣碎的-fno-tree-vectorization解決方法之外),但仍然盡可能多地進行優化?

  3. 什么是-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 cmovTurix在他的回答中指出了-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.260.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM