2013-10-18 21 views
19

爲什麼函數在C++文件中的位置會影響其性能?特別是在下面給出的例子中,我們有兩個相同的函數,它們具有不同的一致的性能特徵。人們如何去研究這一點並確定性能如此不同的原因?爲什麼函數在C++文件中的位置會影響其性能

這個例子非常簡單,我們有兩個函數:a和b。每一個都在緊密循環中運行多次並進行優化(-O3 -march=corei7-avx)並計時。下面是代碼:

#include <cstdint> 
#include <iostream> 
#include <numeric> 

#include <boost/timer/timer.hpp> 

bool array[] = {true, false, true, false, false, true}; 

uint32_t __attribute__((noinline)) a() { 
    asm(""); 
    return std::accumulate(std::begin(array), std::end(array), 0); 
} 

uint32_t __attribute__((noinline)) b() { 
    asm(""); 
    return std::accumulate(std::begin(array), std::end(array), 0); 
} 

const size_t WARM_ITERS = 1ull << 10; 
const size_t MAX_ITERS = 1ull << 30; 

void test(const char* name, uint32_t (*fn)()) 
{ 
    std::cout << name << ": "; 
    for (size_t i = 0; i < WARM_ITERS; i++) { 
     fn(); 
     asm(""); 
    } 
    boost::timer::auto_cpu_timer t; 
    for (size_t i = 0; i < MAX_ITERS; i++) { 
     fn(); 
     asm(""); 
    } 
} 

int main(int argc, char **argv) 
{ 
    test("a", a); 
    test("b", b); 
    return 0; 
} 

一些顯着的特徵:

  • 函數a和b是相同的。它們執行相同的累加操作並編譯成相同的彙編指令。
  • 每個測試迭代都有一個預熱期,然後才能開始嘗試並消除升溫高速緩存的任何問題。

當這是編譯和運行我們得到呈現出比B顯著慢以下的輸出:

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
a: 7.412747s wall, 7.400000s user + 0.000000s system = 7.400000s CPU (99.8%) 
b: 5.729706s wall, 5.740000s user + 0.000000s system = 5.740000s CPU (100.2%) 

如果我們顛倒了兩個測試(即調用test(b)然後test(a))一個仍然較慢比b:

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
b: 5.733968s wall, 5.730000s user + 0.000000s system = 5.730000s CPU (99.9%) 
a: 7.414538s wall, 7.410000s user + 0.000000s system = 7.410000s CPU (99.9%) 

如果我們現在反轉的功能定位在C++文件(b移動的定義之上的)結果倒置,並且變得比乙快!

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
a: 5.729604s wall, 5.720000s user + 0.000000s system = 5.720000s CPU (99.8%) 
b: 7.411549s wall, 7.420000s user + 0.000000s system = 7.420000s CPU (100.1%) 

所以基本上無論哪個函數在C++文件的頂部都會變慢。

一些問題的答案,你可能有:

  • 編譯的代碼是a和b是相同的。反彙編已被檢查。 (對於那些感興趣的人:http://pastebin.com/2QziqRXR
  • 該代碼使用gcc 4.8,在Ubuntu 13.04,Ubuntu 13.10和Ubuntu 12.04.03上編譯爲gcc 4.8.1。
  • 在Intel Sandy Bridge i7-2600和Intel Xeon X5482 cpus上觀察到的效果。

爲什麼會發生這種情況?有什麼工具可以調查這樣的事情?

+0

它們有可能在不同的頁面上結束,這會導致額外的工作?我覺得奇怪的是CPU時間在系統測量中,而不是用戶。這意味着用戶代碼的運行不是花費時間,而是代表進程的一些操作系統級別的事情。 –

+0

作爲一個在黑暗中完整的鏡頭,我會建議乙會議更熱,因爲會議運行的第一個結果...(編輯:哦,你倒...) –

+0

@DaveS我相信時間全部在用戶空間域中。循環加熱循環(測量之前)應該充分加熱緩存和分支預測。 – Shane

回答

6

它在我看來像是一個緩存別名問題。

測試用例相當巧妙,正確定時之前加載到一切緩存。看起來一切都在高速緩存配合 - 雖然模擬的,我已經看的valgrind的cachegrind工具的輸出驗證了這一點,正如人們所期望在這樣一個小的測試情況下,有沒有顯著高速緩存未命中:

valgrind --tool=cachegrind --I1=32768,8,64 --D1=32768,8,64 /tmp/so 
==11130== Cachegrind, a cache and branch-prediction profiler 
==11130== Copyright (C) 2002-2012, and GNU GPL'd, by Nicholas Nethercote et al. 
==11130== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info 
==11130== Command: /tmp/so 
==11130== 
--11130-- warning: L3 cache found, using its data for the LL simulation. 
a: 6.692648s wall, 6.670000s user + 0.000000s system = 6.670000s CPU (99.7%) 
b: 7.306552s wall, 7.280000s user + 0.000000s system = 7.280000s CPU (99.6%) 
==11130== 
==11130== I refs:  2,484,996,374 
==11130== I1 misses:   1,843 
==11130== LLi misses:   1,694 
==11130== I1 miss rate:   0.00% 
==11130== LLi miss rate:   0.00% 
==11130== 
==11130== D refs:  537,530,151 (470,253,428 rd + 67,276,723 wr) 
==11130== D1 misses:   14,477 ( 12,433 rd +  2,044 wr) 
==11130== LLd misses:   8,336 (  6,817 rd +  1,519 wr) 
==11130== D1 miss rate:   0.0% (  0.0%  +  0.0% ) 
==11130== LLd miss rate:   0.0% (  0.0%  +  0.0% ) 
==11130== 
==11130== LL refs:    16,320 ( 14,276 rd +  2,044 wr) 
==11130== LL misses:   10,030 (  8,511 rd +  1,519 wr) 
==11130== LL miss rate:   0.0% (  0.0%  +  0.0% ) 

我選擇了一個32字節,8字節的聯合緩存,64字節緩存行大小與常見的英特爾CPU相匹配,並且反覆觀察到a和b函數之間的差異。

有32K,128路關聯高速緩存使用相同的緩存行大小雖然,差運行的虛機上的所有,但消失:

valgrind --tool=cachegrind --I1=32768,128,64 --D1=32768,128,64 /tmp/so 
==11135== Cachegrind, a cache and branch-prediction profiler 
==11135== Copyright (C) 2002-2012, and GNU GPL'd, by Nicholas Nethercote et al. 
==11135== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info 
==11135== Command: /tmp/so 
==11135== 
--11135-- warning: L3 cache found, using its data for the LL simulation. 
a: 6.754838s wall, 6.730000s user + 0.010000s system = 6.740000s CPU (99.8%) 
b: 6.827246s wall, 6.800000s user + 0.000000s system = 6.800000s CPU (99.6%) 
==11135== 
==11135== I refs:  2,484,996,642 
==11135== I1 misses:   1,816 
==11135== LLi misses:   1,718 
==11135== I1 miss rate:   0.00% 
==11135== LLi miss rate:   0.00% 
==11135== 
==11135== D refs:  537,530,207 (470,253,470 rd + 67,276,737 wr) 
==11135== D1 misses:   14,297 ( 12,276 rd +  2,021 wr) 
==11135== LLd misses:   8,336 (  6,817 rd +  1,519 wr) 
==11135== D1 miss rate:   0.0% (  0.0%  +  0.0% ) 
==11135== LLd miss rate:   0.0% (  0.0%  +  0.0% ) 
==11135== 
==11135== LL refs:    16,113 ( 14,092 rd +  2,021 wr) 
==11135== LL misses:   10,054 (  8,535 rd +  1,519 wr) 
==11135== LL miss rate:   0.0% (  0.0%  +  0.0% ) 

因爲在8路高速緩存,有較少的空間潛在的混疊函數可能會隱藏,你會得到相當於更多散列衝突的尋址。與具有不同的高速緩存相關性,在這種情況下,你的運氣了與事情變得放置在目標文件中,所以雖然不是高速緩存未命中機器,你也不必做任何工作,解決其高速緩存行你真的需要。

編輯:更高速緩存相關性:http://en.wikipedia.org/wiki/CPU_cache#Associativity


另一個編輯:我已經與硬件事件通過perf工具監控證實了這一點。

我修改了源根據是否有存在一個命令行參數來調用僅()或b()。時間與原始測試用例中的時間相同。

sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so 
a: 6.317755s wall, 6.300000s user + 0.000000s system = 6.300000s CPU (99.7%) 
sudo perf report 

4K dTLB-loads 
97 dTLB-load-misses 
4K dTLB-stores 
7 dTLB-store-misses 
479 iTLB-loads 
142 iTLB-load-misses    

sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so foobar 
b: 4.854249s wall, 4.840000s user + 0.000000s system = 4.840000s CPU (99.7%) 
sudo perf report 

3K dTLB-loads 
87 dTLB-load-misses 
3K dTLB-stores 
19 dTLB-store-misses 
259 iTLB-loads 
93 iTLB-load-misses 

顯示是b少TLB動作,所以高速緩存中沒有被驅逐。鑑於兩者之間的功能是相同的,只能通過別名來解釋。

0

您正在致電abtest。由於編譯器沒有理由重新排序你的兩個函數ab(最初)從test更遠。你也在使用模板,所以實際的代碼生成比它在C++源代碼中看起來要大得多。

因此它很可能是爲b指令存儲器進入指令緩存與testa還被遠沒有進入高速緩存,因此需要更長的時間,從低了下去緩存或CPU主內存抓取在一起b

因此它可能是因爲較長的取指令週期爲ab,比ba運行,即使實際的代碼是一樣的,它僅僅是漸行漸遠。

某些CPU架構(諸如臂皮質-A系列),該計數高速緩存未命中的數目支持性能計數器。像perf這樣的工具可以在設置爲使用適當的性能計數器時捕獲這些數據。

相關問題