關於局部數組和全局數組的運行效率?
在看C++工程代碼的時候,對於該工程耗時最多的函數做了這樣一個優化。
為了簡化,就用A,B來表示。本來是這樣的:A類的func_a方法對一個另一個貫穿整個main生存周期的類變數B的一個數組array_b進行操作,是耗時最多的操作。優化:A在自己的類里建了一個數組array_a,在運行func_a方法前,將array_b的內容拷貝到array_a裡面,然後用func_a方法對array_a操作。
也就是說本來是A類的方法對B類的成員變數進行操作,現在是A對自己變數進行操作了,浪費了一些空間和memcpy的時間。不過似乎真的變快了。本來我打算將拷貝刪去看看優化的效果到底有多大,可惜發現不太容易刪掉,因為裡面還摻雜了許多其他複雜的初始化操作。我就自己寫了一個小程序試了一下這種優化,發現確實快了很多,甚至好幾倍以上。請問這樣做的理由是什麼呢?為什麼可以變快呢?這是我的測試代碼,B直接對A的數組進行操作,C將A的數組拷貝到自己的成員變數里再操作。結果發現C的速度快很多。
#include &
#include &
#include &
using namespace std;
const int MAX_NUM = 500;
class A
{
public:
A(int len){
m_len = len;
m_a = new int[len];
for(int i = 0;i &< len; i++) m_a[i] = i+len; } int m_len; int* m_a; }; class B { public: B(int x){m_x = x;} int addSum(int* a,int len){ int sum = 0; for(int i = 0;i &< len; ++i){ int x = MAX_NUM; do{ sum += a[i]; sum *= a[i]; sum /= a[i]; }while(x--); } return sum; } int m_x; }; class C { public: C(int *a, int len){ m_len = len; m_b = new int[len]; memcpy(m_b,a,sizeof(int) * len); } int addSum(int* a,int len){ int sum = 0; for(int i = 0;i &< m_len; ++i){ int x = MAX_NUM; do{ sum += m_b[i]; sum *= m_b[i]; sum /= m_b[i]; }while(x--); } return sum; } int m_len; int* m_b; }; int main() { time_t st; time_t ed; clock_t t1,t2; int x,y; A a(10000); B d(1); st = time(0); t1 = clock(); C c(a.m_a, a.m_len); for(int i = 0;i &< MAX_NUM; ++i) { x = c.addSum(a.m_a,a.m_len); y=i; } ed = time(0); t2 = clock(); printf("cnt = %d,%d,time = %f,time2 = %ld ",x,y,(double)(ed-st),(t2 -t1)); st = time(0); t1 = clock(); for(int i = 0;i &< MAX_NUM; ++i) { x = d.addSum(a.m_a,a.m_len); y=i; } ed = time(0); t2 = clock(); printf("cnt = %d,%d,time = %f,time2 = %ld ",x,y,(double)(ed-st),(t2 -t1)); return 0; }
我就自己寫了一個小程序試了一下這種優化,發現確實快了很多,甚至好幾倍以上。
貼代碼、測試方法、測試結果。
同 @北極 的回答,題主的case不具有普適性。
直接拿題主的代碼在我的本上編譯運行了一下,結果放這裡了:https://gist.github.com/rednaxelafx/afec16fefee7cdbf9189#file-command_prompt
$ clang++ -O3 arrcost.cc
$ ./a.out
cnt = 2133050968,499,time = 0.000000,time2 = 1545
cnt = 2133050968,499,time = 0.000000,time2 = 1415
$ clang++ --version
Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin13.4.0
Thread model: posix
差別不大嗯。
Clang/LLVM在-O3下生成的代碼也放在那個鏈接里了,請參考。對比前後兩個版本的代碼,LLVM在-O3下生成的代碼是完全一樣的。那個.ll文件可以用LLVM自帶的opt工具生成出圖形化的控制流圖,看得更清楚些。opt -dot-cfg arrcost.ll
我放了一份生成好的dot文件在https://gist.github.com/rednaxelafx/afec16fefee7cdbf9189#file-cfg-main-dot
相關傳送門:LLVM中如何獲取程序的控制流圖CFG? - 編譯器僅僅因為把數據拷貝到類里所以速度變快了?會不會數據處理的方式也不同,所以減少了cache miss?
題主你這個case不具備普遍性。
我拿到試了一下不同的編譯器,效果是不一樣的。
VS2008 Debug模式下,二者沒有明顯差別,後者稍快一些(我稍微改了一下輸出代碼):
G:VCarrcost&>Debugarrcost.exe
cnt = 8290,499,time = 29.670000
cnt = 8290,499,time = 29.641000
Release模式下,差距明顯:
G:VCarrcost&>Releasearrcost.exe
cnt = 8290,499,time = 0.030000
cnt = 8290,499,time = 18.160000
gcc 481 + Ubuntu,-O0結果:
cnt = 8290,499,time = 30.210829
cnt = 8290,499,time = 30.162914
-O2結果:
cnt = 8290,499,time = 18.638646
cnt = 8290,499,time = 18.532594
也就是說,只有VC的release模式才有這種效果。
其它版本的VC/GCC沒實驗。
原因么,反彙編了一下Release的那個,上兩個圖:
VS2008的優化中,把C_addSum優化到循環外了。也就是說,對於第一個addSum來說,計算流程是這樣的:
1. 計算出C_addSum,算一次;
2. 執行循環for(int i = 0;i &< MAX_NUM; ++i),但不做任何操作,只保存y值;3. 輸出;顯然這麼算是更快的。
對於第二個addSum來說,計算流程是:
1. 循環;2. 循環內做B_addSum;3. 輸出;顯然第一個更快,因為第一個只算一次。
VC這麼優化要說道理,也是有的,因為你輸出的x只有一次,C里的東西你只初始化了一次,在整個循環的過程中,C里的東西是不變的,你傳入的參數又沒有被使用(你傳了len但沒用),C沒有被別人引用,基於這些理由,編譯器可以認為addSum所操作的東西都是不變的,你實際上只要一次addSum的結果就夠了,所以不管你循環設置的多大,編譯器優化後始終只算一次。
對於第二個來說,循環內你引用了循環外的對象a,那麼循環內就不敢隨便優化了,誰知道a里的東西會不會改變,所以編譯器會老老實實的算上MAX_NUM次,最終輸出結果。
VC這麼做其實有點激進——我個人認為不是太妥當。A a(1《29)再試試
不管是由B來操作A的數組,還是A自己操作,在最後的函數裡面,數組的指針都是放在某個寄存器裡面的,唯一差別是一個是由函數參數傳入,另一個是通過this指針偏移之後獲得,但都是一次性操作不影響什麼性能。之後的重複性操作都是對該寄存器中的值加個偏移量來進行定址,沒有什麼本質區別。
你的測試代碼有問題。在有優化的情況下,代碼不能保證addSum能執行MAX_NUM次。
你這測試代碼,憑直覺就感受到編譯器可能會做手腳了,x y的計算都有可能被優化掉循環
占坑,從demo代碼來看,兩種方式沒有本質區別,操作的數據都是堆中的一個數組。如果事實上出現了性能的巨大差異,那多半是編譯優化搞得鬼。
有可能是同時也對 A 自己原來的成員變數進行了操作,於是如果被操作的數據都在 A 裡面的話,緩存命中率較高(spatial locality 嘛)。
不然就實在無法解釋了。可能是定址時間不一樣。a變數要獲取b的地址在獲取數組的地址。
推薦閱讀:
※PLT界在近幾年中對哪些問題的研究有重大進展?
※HotSpot JVM參數 -XX:+DoEscapeAnalysis 有什麼影響?