2014-10-04 90 views
10

我在標準化循環緩衝區時偶然發現了這個問題。任何人都可以解釋一個std :: vector在這個例子中如何超越一個普通數組?std :: vector如何比普通數組更快?

#include <iostream> 
#include <vector> 

struct uint_pair { 
    unsigned int a, b; 
    uint_pair (unsigned int x = 0, unsigned int y = 0) : a(x), b(y) {} 
}; 

struct container { 
    unsigned int pos; 

#ifdef USE_VECTOR 
    std::vector<uint_pair> data; 
    container() : pos(0) { data.resize(16); } 
#else 
    uint_pair data[16]; 
    container() : pos(0) {} 
#endif 

    void add(uint_pair val) { 
     data[++pos % 16] = val; 
    } 
}; 

int main() { 
    container c; 
    for (unsigned int i = 0; i < 1000000000; i++) c.add(uint_pair{i, i}); 
    std::cout << c.data[0].a << " " << c.data[0].b << std::endl; 
} 

這是我使用的GCC(鏗鏘類似)得到的結果:

g++ -o bench -std=c++0x -Os main.cpp -D'USE_VECTOR' 
real 0m8.757s 
user 0m8.750s 
sys  0m0.002s 

g++ -o bench -std=c++0x -Os main.cpp 
real 0m9.215s 
user 0m9.209s 
sys  0m0.002s 
+1

可能只是分配排隊與高速緩存的其他數據的方式。附:你想調整大小而不是保留。 – 2014-10-04 04:17:49

+0

@MarkRansom謝謝,更新了代碼。結果仍然成立。 – amarcus 2014-10-04 04:21:47

+0

GCC 4.8帶來更大的差異。我看到0.6s的矢量和1.8s的陣列。優化級別很重要,-O3獲得矢量的0.9s。 – Adam 2014-10-04 04:27:49

回答

8

這裏是你如何能消除差異。而不是你add的,使用這樣的功能:

void set(unsigned int x, unsigned int y) { 
    ++pos; 
    data[pos % 16].a = x; 
    data[pos % 16].b = y; 
} 

這樣調用:

for (unsigned int i = 0; i < 1000000000; i++) c.set(i, i); 

這不完全一樣的東西是你的,但它避免了在語義上創建一個臨時對象。它看起來像是在使用矢量時,編譯器能夠更好地優化臨時性。

$ g++-4.8 -o bench -std=c++11 -Os main.cpp -DUSE_VECTOR 
$ time ./bench 
999999999 999999999 

real 0m0.635s 
user 0m0.630s 
sys 0m0.002s 

$ g++-4.8 -o bench -std=c++11 -Os main.cpp 
$ time ./bench 
999999999 999999999 

real 0m0.644s 
user 0m0.639s 
sys 0m0.002s 

在我的機器的setadd方法均產生具有矢量相同的性能。只有數組顯示不同。爲了進一步證明優化,如果使用-O0進行編譯,那麼數組方法稍快一些(但速度比使用-Os慢10倍以上)。

這並沒有解釋爲什麼編譯器將這兩種方式區別對待。畢竟,矢量是由數組支持的。此外,std::array的行爲與您的C風格陣列相同。

+0

有趣的是,性能方面,'std :: array'更像是使用C風格的數組,而不是使用'std :: vector'。 – 5gon12eder 2014-10-04 04:54:10

+0

@ 5gon12eder正確,它只是一個圍繞C風格數組的STL類包裝。我也嘗試過,在這種情況下,它的行爲就像C風格的數組。 – Adam 2014-10-04 04:55:28

+0

在我的機器上,我觀察到有些不同的結果。 std :: vector循環總是有5條指令。該數組需要7個OP代碼,但只有4個代碼與您的代碼相同,所以它比'std :: vector'更快(也受時序結果支持)。 'std :: array'總是產生與C風格數組相同的彙編代碼。 [GCC 4.9.1 20140903(預發佈)在x86_64 GNU/Linux] – 5gon12eder 2014-10-04 05:14:49

2

一個問題是在結構中放置「pos」成員。

對於c數組,請記住它連續存儲在與「pos」成員相鄰的內存中。當數據被推入c數組時,必須發佈額外的指令來抵消「pos」成員之後的結構。但是,寫入向量不會造成這種限制,因爲它的內存位於其他地方。

要擠出更多性能,請確保最熱門的數據位於緩存行的前端。

編輯:

要獲得的c-陣列一樣快執行作爲矢量,該C-陣列必須在8個字節邊界的64位機器上進行分配。因此,像:

uint_pair* data; 
unsigned int pos; 

container() : pos(0) { 
    std::size_t bufSize = sizeof(uint_pair) * 17; 
    void* p = new char[bufSize]; 
    p = std::align(8, sizeof(uint_pair), p, bufSize); 
    data = reinterpret_cast<uint_pair*>(p); 
} 

有了一個稍微修改add函數:

void add(unsigned int x, unsigned int y) { 
    auto& ref = data[pos++ % 16]; 
    ref.a = x; 
    ref.b = y; 
} 

的C-陣列現在時間:

real 0m0.735s 
user 0m0.730s 
sys  0m0.002s 

而且的std ::向量:

real 0m0.743s 
user 0m0.736s 
sys  0m0.004s 

標準庫實現rs正在爲你全力以赴:)

+0

您的聲明是問題是內存對齊,但你不顯示。你使用了類似的'add'函數,我證明了這種改變會消除性能差異。所以對齊更改根本沒有任何效果(換句話說,編譯器已經處理了這個問題)。 – Adam 2014-10-04 07:23:08

+0

你是對的,內置於​​結構中的數組與通過指針訪問的數組之間存在差異。但這並不能解釋整個性能差異(請參閱我對原始問題的評論)。我也想看看您的緩存聲明的一些證據。數據總計少於20個整數。所有的方法都應該在緩存中。 – Adam 2014-10-04 07:26:01

+0

我們必須得到不同的結果。使用你的「set」或我改變的「add」,堆分配的c-array和std :: vector之間的性能差異是**不等於**,c-陣列。 順便說一下,使用正確對齊的堆分配可以完全消除這種差異,順便說一句,這是編譯器不會爲您做的。因此,修改的「添加」以及對齊的堆分配都是必需的。 – d3coy 2014-10-04 16:00:19

0

看來C++ 11編譯器由於operator =(右值引用)而生成更好的向量代碼。 首先,在C++ 03編譯器中,普通數組比矢量快兩倍。其次,如果你使用Adam建議的void set(unsigned int x,unsigned int y),那麼它們沒有什麼不同。對於矢量

.L49: 
leal (%rdi,%rax), %esi 
andl $15, %esi 
leaq (%rdx,%rsi,8), %rsi 
movl %eax, (%rsi) 
movl %eax, 4(%rsi) 
incq %rax 
cmpq $1000000000, %rax 
jne .L49 

彙編代碼普通數組

.L3: 
movl 12(%rsp), %edx 
incl %edx 
movl %edx, 12(%rsp) 
andl $15, %edx 
leaq 12(%rsp,%rdx,8), %rdx 
movl %eax, 4(%rdx) 
movl %eax, 8(%rdx) 
incl %eax 
cmpl $1000000000, %eax 
jne .L3 
+0

我不相信一個動作在踢。首先'uint_pair'聲明瞭一個構造函數,所以它沒有默認的移動構造函數。第二:'add'函數中'operator ='的參數是一個左值。第三:即使定義了一個移動ctor,兩個未簽名的成員仍然必須被複制。 – DarioP 2014-10-08 09:29:48