2009-11-20 37 views
5

當一個C++函數接受一個std::vector參數,該通常模式是通過const引用傳遞它,如:STD的高效傳遞::矢量

int sum2(const std::vector<int> &v) 
{ 
    int s = 0; 
    for(size_t i = 0; i < v.size(); i++) s += fn(v[i]); 
    return s; 
} 

我相信,這種代碼產生雙解除引用的時候向量元素被訪問,因爲CPU應該首先解引用v來讀取指向第一個元素的指針,該指針需要再次解除引用才能讀取第一個元素。我期望在棧上傳遞矢量對象的淺拷貝會更有效率。這種淺拷貝將封裝一個指向第一個元素的指針,並且指針的大小與原始矢量所指向的內存區域相同。

int sum2(vector_ref<int> v) 
{ 
    int s = 0; 
    for(size_t i = 0; i < v.size(); i++) s += fn(v[i]); 
    return s; 
} 

類似的性能,但通過傳遞隨機訪問迭代器對可以實現更少的便利。 我的問題是:這個想法有什麼缺陷?我認爲應該有一些很好的理由,聰明人接受支付向量引用的性能成本,或者處理迭代器的不便。

編輯:基於下面的評析,請考慮這種情況,如果我只是建議vector_ref類重命名爲範圍。目的是使用具有更自然語法的隨機訪問迭代器對。

+5

你真的比較過兩種情況下生成的機器碼嗎?或者衡量表現? –

+0

您是否檢查過它在組裝中的確如此?我也不認爲迭代器對的約定來自性能考慮因素,它是設計決策(事實上,即使我沒有研究過它們的工作原理,實際上提高了迭代器範圍對象的更方便的概念)。 – UncleBens

+0

我不認爲你的意見實際上是有道理的。引用是對象的別名。因此,訪問對象的引用並不像訪問原始對象那樣昂貴。如果你已經完成了兩分鐘的盡職調查,你就不需要這篇文章,這個稍微煽動性的謾罵就是你的猜測。 –

回答

10

我相信,這個代碼導致雙反引用時向量元素被訪問

不一定。編譯器非常聰明,應該能夠eliminate common subexpressions.他們可以看到運算符[]不會更改'指向第一個元素的指針',所以他們不需要讓CPU從內存中爲每次循環迭代重新加載它。

+1

優秀的點。 –

+0

好的,所以編譯器在理論上可以將每次調用此方法的雙重去權次數減少爲1次。不會保證0更好嗎? – shojtsy

+2

根據內存訪問計算成本,而不是解除引用的數量。在你提出的'淺拷貝'中,你將在調用者而不是被調用者中進行內存訪問,所以最終的結果將是(最好)相同。事實上,如果編譯器不內聯函數,它可能會更糟糕,因爲你傳遞了一個更大的參數。 – int3

1

通過價值傳遞,除非你確定通過引用提高性能。

當您通過價值傳遞時,複製elision可能會發生,如果不是更好的性能會導致相似的結果。

戴夫寫在這裏:

http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

+1

我會強烈反對。這不僅僅是性能優化的問題。如果一個方法不改變向量的內容,那麼我總是會爭取傳遞一個向量爲const&的向量。 由於copy-elision依賴於您的方法被調用的上下文,我仍然認爲在大多數情況下,通過引用是正確的選擇。 – Sebastian

+1

如果它不改變矢量,傳值也是非常好的。從我+1。 – jalf

+1

有趣。我必須更仔細地閱讀。儘管有標題,但看起來他只關注回報,而不是參數傳遞,所以我不確定它適用於這種情況。 –

4

這個想法有什麼問題?

簡單:這是過早的優化。替代方案:接受vector<int> const&並使用迭代器或直接將迭代器傳遞給函數。

+0

當代碼比樸素版更簡單時,它是不成熟的優化嗎?爲什麼使用迭代器的時候,我們可以用迭代器的相同性能創建更自然的語法? – shojtsy

+0

因爲您可能剛剛通過原始迭代器,按值或通過const引用,並且信任編譯器來優化代碼。在很多情況下,編譯器能夠消除雙重間接。而在不能這樣做的情況下,性能差異幾乎肯定不會實際上*重要*。 – jalf

+1

一對迭代器* *已經是「你想要的淺拷貝」。此外,這是過早的優化,因爲你可能在錯誤的地方尋找提高性能。先測量,然後採取行動。 – sellibitze

1

沒有雙引用,因爲編譯器可能會將實指針作爲參數傳遞給向量,而不是指向指針的指針。你可以簡單地嘗試了這一點,並檢查您的IDE對什麼是真正回事幕後彙編視圖:

void Method(std::vector<int> const& vec) { 
int i = vec.back(); 
} 


void SomeOtherMethod() { 
    std::vector<int> vec; 
    vec.push_back(1); 
    Method(vec); 
} 

這裏會發生什麼?矢量被分配在堆棧上。第一推回被翻譯成:

push  eax // this is the constant one that has been stored in eax 
lea   ecx,[ebp-24h] // ecx is the pointer to vec on the stack 
call  std::vector<int,std::allocator<int> >::push_back 

現在我們稱之爲()的方法,通過矢量常量&:

lea   ecx,[ebp-24h] 
push  ecx 
call  Method (8274DC0h) 

不出所料,指針向量作爲引用傳遞都不過是永久解除引用的指針。現在在Method()中,向量被再次訪問:

mov   ecx,dword ptr [ebp+8] 
call  std::vector<int,std::allocator<int> >::back (8276100h) 

矢量指針直接從堆棧中取出並寫入ecx。

+2

要做到這一點,編譯器將不得不改變功能簽名,這似乎不大可能。更改簽名會中斷函數的調用者。如果您有高級鏈接時代碼生成功能,但我不確定是否有實現這種高級功能的實現。 –

+0

我檢查出來了。沒有雙引號。 – Sebastian

+2

該向量包含一個指向實際數據的指針,因此傳遞一個指向該向量的指針確實將指針傳遞給該指針。第二個解除引用發生在對back()的調用中。如果你傳遞了一個迭代器,那麼(在大多數的實現中,你會直接將指針傳遞給vector中包含的數據,從而消除了間接的級別 –

2

你說得對,這裏有一個額外的間接。如果編譯器(在鏈接時代代的幫助下)優化它,這是可想而知的(儘管這將是令人驚訝的)。

你所提出的有時稱爲切片,在某些情況下它被廣泛使用。但總的來說,我不確定這是否值得冒險。你必須非常小心地對你的切片(或其他人)進行無效評估。

請注意,如果您使用循環迭代器代替索引,那麼您只需要幾次刪除引用(要調用begin()end())而不是n次(以索引向量)。

int sum(const vector<int> &v) 
{ 
    int s = 0; 
    for (auto it = v.begin(); it != v.end(); ++it) { 
     s += fn(*it); 
    } 
    return s; 
} 

(我假設優化器將葫蘆end()通話圈外的。你能做到這一點明確的是肯定的。)

傳遞一對迭代器,而不是本身好像容器STL成語。這會給你更多的概括性,因爲容器的類型可能會有所不同,但所需的解除引用數量也是如此。

9

有什麼不對你的想法是,你已經有兩個非常好的解決方案:

  • 傳遞載體原樣,或者通過值(如編譯器會經常消除複印件),或(常量)參考,並相信編譯器消除雙重間接,或者傳遞一個迭代器對。

當然你可以爭辯說迭代器對是「不太自然的語法」,但我不同意。任何習慣了STL的人都很自然。它非常高效,並且可以準確地爲您提供使用std算法或您自己的函數來處理範圍的所需信息。

迭代器對是一種常見的C++習慣用法,讀取您的代碼的C++程序員將會毫無問題地理解它們,而他們會對您的家庭釀造矢量包裝器感到驚訝。

如果你是真的關於性能的偏執,通過這對迭代器。如果語法真的困擾你,傳遞向量並且信任編譯器。