2016-09-16 90 views
2

我試圖用g ++ 5.4(-ftree-vectorize)使用自動矢量化。我注意到下面的代碼中的數組版本導致編譯器錯過了內部循環中的向量化機會,導致與指針版本相比顯着的性能差異。在這種情況下有什麼可以幫助編譯器嗎?gcc中的數組vs指針自動矢量化

void floydwarshall(float* mat, size_t n) { 
#if USE_POINTER 
    for (int k = 0; k < n; ++k) { 
     for (int i = 0; i < n; ++i) { 
      auto v = mat[i*n + k]; 
      for (int j = 0; j < n; ++j) { 
       auto val = v + mat[k*n+j]; 
       if (mat[i*n + j] > val) { 
        mat[i*n + j] = val; 
       } 
      } 
     } 
    } 
#else // USE_ARRAY 
    typedef float (*array)[n]; 
    array m = reinterpret_cast<array>(mat); 
    for (int k = 0; k < n; ++k) { 
     for (int i = 0; i < n; ++i) { 
      auto v = m[i][k]; 
      for (int j = 0; j < n; ++j) { 
       auto val = v + m[k][j]; 
       if (m[i][j] > val) { 
        m[i][j] = val; 
       } 
      } 
     } 
    } 
#endif 
} 
+1

避免分支'mat [i * n + j] =(mat [i * n + j]> val)?val:mat [i * n + j];'(無條件寫入)更容易矢量化。 –

+0

我把你的代碼分成兩個獨立的函數(同時查看)和[把它們放在Godbolt編譯器資源管理器](https://godbolt.org/g/i0ByaJ)上。它們都使用gcc5自動矢量化。4'-O3 -march = haswell',在內部循環中使用vcmpltps/vmaskmovps是Marc指出的原因。指針版本在內部循環中有兩個來自內存的整數加載,並且數組版本的內部循環綁定在內存中('cmp r11,QWORD PTR [rbp-72]'),所以它們都有問題。你使用了哪些目標選項?只需要基線SSE2? –

回答

2

兩個版本矢量化,使用g ++ 5.4 -O3 -march=haswell,使用vcmpltps /在對馬克指出的原因內環vmaskmovps。

如果您不讓編譯器使用AVX指令,那將會花費更多時間。但是如果我只使用-O3(所以只有SSE2可用,因爲它是x86-64的基準),所以我沒有看到任何版本的矢量化。所以你原來的問題是基於我無法複製的結果。

將if()更改爲三元運算符(因此代碼始終存儲到數組中)可讓編譯器加載/ MINPS /無條件地存儲。如果您的矩陣不適合緩存,這是很多內存流量;也許你可以用不同的方式安排你的循環?或者可能不是,因爲需要m[i][k],並且我認爲這很重要。

如果更新非常少,並且髒數據的回寫會導致內存瓶頸,那麼如果沒有向量元素被修改,甚至可能需要分支以避免存儲。


這裏是一個數組版本,以及矢量化,即使只有SSE2。我添加了代碼以告訴編譯器輸入已對齊,並且大小是8的倍數(每個AVX向量的浮點數)。如果你的真實代碼無法做出這些假設,那就把這部分拿出來。它使得矢量化部分更容易找到,因爲它並沒有被埋在標量的介紹/清除代碼中。 (使用-O2 -ftree-vectorize沒有完全展開的清理代碼這種方式,但-O3一樣。)

我注意到,沒有AVX,GCC仍然使用未對齊的加載,但對準商店。如果m[i][j]對齊,也許它不知道尺寸是8的倍數應該使m[k][j]對齊?這可能是指針版本和數組版本之間的區別。

code on the Godbolt compiler explorer

void floydwarshall_array_unconditional(float* mat, size_t n) { 

    // try to tell gcc that it doesn't need scalar intro/outro code 
    // The vectorized inner loop isn't particularly different without these, but it means less wading through scalar cleanup code (and less bloat if you can use this in your real code). 

    // works with gcc6, doesn't work with gcc5.4 
    mat = (float*)__builtin_assume_aligned(mat, 32); 
    n /= 8; 
    n *= 8;   // code is simpler if matrix size is always a multiple of 8 (floats per AVX vector) 

    typedef float (*array)[n]; 
    array m = reinterpret_cast<array>(mat); 

    for (size_t k = 0; k < n; ++k) { 
     for (size_t i = 0; i < n; ++i) { 
      auto v = m[i][k]; 
      for (size_t j = 0; j < n; ++j) { 
       auto val = v + m[k][j]; 
       m[i][j] = (m[i][j]>val) ? val : m[i][j]; // Marc's suggested change: enables vectorization with unconditional stores. 
      } 
     } 
    } 
} 

gcc5.4不管理,以避免周圍的矢量部分標前奏/清除代碼,但gcc6.2做。這兩個編譯器版本的向量化部分基本相同。

## The inner-most loop (with gcc6.2 -march=haswell -O3) 
.L5: 
    vaddps ymm0, ymm1, YMMWORD PTR [rsi+rax] 
    vminps ymm0, ymm0, YMMWORD PTR [rdx+rax]  #### Note use of minps and unconditional store, enabled by using the ternary operator instead of if(). 
    add  r14, 1 
    vmovaps YMMWORD PTR [rdx+rax], ymm0 
    add  rax, 32 
    cmp  r14, r13 
    jb  .L5 

外,下一個循環做一些整數計數器檢查(使用一些setcc的東西),並執行vmovss xmm1, DWORD PTR [rax+r10*4]和一個單獨的vbroadcastss ymm1, xmm1。據推測,標量清理是跳到不需要廣播的,而gcc並不知道整體而言,即使在不需要廣播部分的情況下,使用VBROADCASTSS作爲負載也會更便宜。