簡體   English   中英

C ++性能std :: array vs std :: vector

[英]C++ performance std::array vs std::vector

晚上好。

我知道C風格的數組或std :: array並不比矢量快。 我一直使用矢量(我使用它們很好)。 但是,我有一些情況,使用std :: array比使用std :: vector更好,我不知道為什么(用clang 7.0和gcc 8.2測試)。

讓我分享一個簡單的代碼:

#include <vector>
#include <array>

// some size constant
const size_t N = 100;

// some vectors and arrays
using vec = std::vector<double>;
using arr = std::array<double,3>;
// arrays are constructed faster here due to known size, but it is irrelevant
const vec v1 {1.0,-1.0,1.0};
const vec v2 {1.0,2.0,1.0};
const arr a1 {1.0,-1.0,1.0};
const arr a2 {1.0,2.0,1.0};

// vector to store combinations of vectors or arrays
std::vector<double> glob(N,0.0);

到現在為止還挺好。 初始化變量的上述代碼不包含在基准測試中。 現在,讓我們編寫一個函數來組合v1v2a1a2元素( double ):

// some combination
auto comb(const double m, const double f)
{
  return m + f;
}

基准功能:

void assemble_vec()
{
    for (size_t i=0; i<N-2; ++i)
    {
        glob[i] += comb(v1[0],v2[0]);
        glob[i+1] += comb(v1[1],v2[1]);
        glob[i+2] += comb(v1[2],v2[2]);
    }  
}

void assemble_arr()
{
    for (size_t i=0; i<N-2; ++i)
    {
        glob[i] += comb(a1[0],a2[0]);
        glob[i+1] += comb(a1[1],a2[1]);
        glob[i+2] += comb(a1[2],a2[2]);
    }  
}

我用clang 7.0和gcc 8.2試過這個。 在這兩種情況下,陣列版本的速度幾乎是矢量版本的兩倍。

有誰知道為什么? 謝謝!

GCC(可能還有Clang)正在優化數組,但不是向量

數組必然比向量慢的基本假設是不正確的。 因為向量要求將其數據存儲在已分配的內存中(默認分配器使用動態內存),所以需要使用的值必須存儲在堆內存中,並在執行此程序時重復訪問。 相反,數組使用的值可以完全優化,並在程序的程序集中直接引用。

下面是GCC在打開優化后吐出為assemble_vecassemble_arr函數的assemble_vec集:

[-snip-]
//==============
//Vector Version
//==============
assemble_vec():
        mov     rax, QWORD PTR glob[rip]
        mov     rcx, QWORD PTR v2[rip]
        mov     rdx, QWORD PTR v1[rip]
        movsd   xmm1, QWORD PTR [rax+8]
        movsd   xmm0, QWORD PTR [rax]
        lea     rsi, [rax+784]
.L23:
        movsd   xmm2, QWORD PTR [rcx]
        addsd   xmm2, QWORD PTR [rdx]
        add     rax, 8
        addsd   xmm0, xmm2
        movsd   QWORD PTR [rax-8], xmm0
        movsd   xmm0, QWORD PTR [rcx+8]
        addsd   xmm0, QWORD PTR [rdx+8]
        addsd   xmm0, xmm1
        movsd   QWORD PTR [rax], xmm0
        movsd   xmm1, QWORD PTR [rcx+16]
        addsd   xmm1, QWORD PTR [rdx+16]
        addsd   xmm1, QWORD PTR [rax+8]
        movsd   QWORD PTR [rax+8], xmm1
        cmp     rax, rsi
        jne     .L23
        ret

//=============
//Array Version
//=============
assemble_arr():
        mov     rax, QWORD PTR glob[rip]
        movsd   xmm2, QWORD PTR .LC1[rip]
        movsd   xmm3, QWORD PTR .LC2[rip]
        movsd   xmm1, QWORD PTR [rax+8]
        movsd   xmm0, QWORD PTR [rax]
        lea     rdx, [rax+784]
.L26:
        addsd   xmm1, xmm3
        addsd   xmm0, xmm2
        add     rax, 8
        movsd   QWORD PTR [rax-8], xmm0
        movapd  xmm0, xmm1
        movsd   QWORD PTR [rax], xmm1
        movsd   xmm1, QWORD PTR [rax+8]
        addsd   xmm1, xmm2
        movsd   QWORD PTR [rax+8], xmm1
        cmp     rax, rdx
        jne     .L26
        ret
[-snip-]

這些代碼部分之間存在一些差異,但關鍵區別在於分別在.L23.L26標簽之后,對於矢量版本,與陣列版本相比,數字通過效率較低的操作碼加在一起。正在使用(更多)SSE指令。 與陣列版本相比,矢量版本還涉及更多的內存查找。 這些因素相互結合將導致代碼對std::array版本的代碼執行速度比std::vector版本快。

C ++別名規則不允許編譯器證明glob[i] += stuff不會修改const vec v1 {1.0,-1.0,1.0};其中一個元素const vec v1 {1.0,-1.0,1.0}; v2

std::vector上的const意味着可以假設“控制塊”指針在構造之后不被修改,但是內存仍然是動態分配的,所有編譯器都知道它在靜態存儲中實際上有一個const double *

std::vector實現中沒有任何內容允許編譯器排除指向該存儲的其他一些non-const指針。 例如, glob的控制塊中的double *data

C ++沒有為庫實現者提供一種方法,為編譯器提供不同std::vector s的存儲不重疊的信息。 他們不能使用__restrict (甚至在支持該擴展的編譯器上),因為這可能會破壞帶有vector元素地址的程序。 有關restrict請參閱C99文檔


但是使用const arr a1 {1.0,-1.0,1.0}; a2 ,雙打本身可以進入只讀靜態存儲,編譯器知道這一點。 因此它可以評估comb(a1[0],a2[0]); 等等在編譯時 在@Xirema的答案中,您可以看到asm輸出加載常量.LC1.LC2 (只有兩個常數,因為這兩個a1[0]+a2[0]a1[2]+a2[2]1.0+1.0 。循環體使用xmm2作為源操作數為addsd兩次,另一恆定一次)。


但是,在運行時循環外,編譯器仍然無法執行求和操作嗎?

不,再次因為潛在的混疊。 它不知道存儲到glob[i+0..3]中的存儲不會修改v1[0..2]的內容,因此每次通過循環后它都會從v1和v2重新加載到glob

(但它不必重新加載vector<>控制塊指針,因為基於類型的嚴格別名規則讓它假設存儲double不會修改double* 。)

編譯器可以檢查glob.data() + 0 .. N-3沒有與v1/v1.data() + 0 .. 2任何一個重疊,並為該情況制作了不同版本的循環,將三個comb()結果提升出循環。

這是一些有用的優化,一些編譯器在自動矢量化時會做,如果它們不能證明缺少別名 ; 在你的情況下,gcc不會檢查重疊,這顯然是一個錯過的優化,因為它會使函數運行得更快。 但問題是編譯器是否可以合理地猜測在運行時檢查重疊是否值得發出asm,並且具有相同循環的2個不同版本。 通過配置文件引導優化,它會知道循環很熱(運行多次迭代),並且值得花費額外的時間。 但如果沒有這個,編譯器可能不希望冒太多風險。

ICC19(英特爾的編譯器)實際上確實在這里做了類似的事情,但它很奇怪:如果你看一下assemble_vec的開頭( 在Godbolt編譯器瀏覽器上 ),它從glob加載數據指針,然后加上8並再次減去指針,產生一個常數8 然后它在運行時分支8 > 784 (未采用)然后-8 < 784 (采用)。 看起來這應該是重疊檢查,但它可能使用相同的指針兩次而不是v1和v2? 784 = 8*100 - 16 = sizeof(double)*N - 16

無論如何,它最終運行了..B2.19循環,它提升了所有3個comb()計算,有趣的是一次循環2次迭代,4個標量加載並存儲到glob[i+0..4] ,並且6 addsd (標量雙)添加指令。

在函數體的其他地方,有一個矢量化版本,它使用3x addpd (打包雙addpd ),只存儲/重新加載部分重疊的128位向量。 這將導致存儲轉發停頓,但是無序執行可能能夠隱藏它。 它非常奇怪,它在運行時分支計算,每次都會產生相同的結果,並且從不使用該循環。 聞起來像個臭蟲。


如果glob[]是一個靜態數組 ,你仍然遇到了問題。 因為編譯器無法知道v1/v2.data()沒有指向那個靜態數組。

我想如果你通過double *__restrict g = &glob[0];訪問它double *__restrict g = &glob[0]; ,根本不存在問題。 這將保證編譯器g[i] += ...不會影響您通過其他指針訪問的任何值,如v1[0]

實際上,這不能為gcc,clang或ICC -O3提升comb() 但它確實適用於MSVC。 (我已經讀過MSVC沒有進行基於類型的嚴格別名優化,但是它沒有在循環中重新加載glob.data()所以它以某種方式弄清楚存儲一個double不會修改指針。但MSVC會與其他C ++實現不同,定義*(int*)my_float的行為以進行類型懲罰。)

為了測試, 我把它放在Godbolt上

//__attribute__((noinline))
void assemble_vec()
{
     double *__restrict g = &glob[0];   // Helps MSVC, but not gcc/clang/ICC
    // std::vector<double> &g = glob;   // actually hurts ICC it seems?
    // #define g  glob                  // so use this as the alternative to __restrict
    for (size_t i=0; i<N-2; ++i)
    {
        g[i] += comb(v1[0],v2[0]);
        g[i+1] += comb(v1[1],v2[1]);
        g[i+2] += comb(v1[2],v2[2]);
    }  
}

我們從循環外的MSVC得到這個

    movsd   xmm2, QWORD PTR [rcx]       # v2[0]
    movsd   xmm3, QWORD PTR [rcx+8]
    movsd   xmm4, QWORD PTR [rcx+16]
    addsd   xmm2, QWORD PTR [rax]       # += v1[0]
    addsd   xmm3, QWORD PTR [rax+8]
    addsd   xmm4, QWORD PTR [rax+16]
    mov     eax, 98                             ; 00000062H

然后我們得到一個有效的循環。

所以這是對gcc / clang / ICC的錯過優化。

我認為關鍵是你使用的存儲空間太小(六個雙倍),這使得編譯器在std::array情況下,通過在寄存器中放置值來完全消除RAM存儲。 如果更優化,編譯器可以將堆棧變量存儲到寄存器。 這減少了一半的內存訪問(只寫入glob仍然存在)。 std::vector的情況下,由於使用了動態內存,編譯器無法執行這樣的優化。 嘗試為a1, a2, v1, v2使用明顯更大的尺寸

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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