22

直到今天,我一直認爲體面編譯器會自動將結構傳遞值轉換爲傳遞引用,如果結構足夠大,後者會更快。據我所知,這似乎是一個不費吹灰之力的優化。然而,爲了滿足我對這是否真的發生的好奇心,我在C++和D中創建了一個簡單的測試用例,並查看了GCC和Digital Mars D的輸出。兩者都堅持按值傳遞32字節的結構,有問題的函數是加起來的成員和返回的值,沒有修改傳入的結構。C++版本如下。爲什麼不通過引用傳遞結構通用優化?

#include "iostream.h" 

struct S { 
    int i, j, k, l, m, n, o, p; 
}; 

int foo(S s) { 
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p; 
} 

int main() { 
    S s; 
    int bar = foo(s); 
    cout << bar; 
} 

我的問題是,爲什麼赫克不會像這樣由編譯器優化,以傳遞通過引用,而不是實際推動所有這些int小號到堆棧?注:使用的編譯器開關:GCC -O2(-O3內聯foo()。),DMD -O -inline -release。

編輯:顯然,在通常情況下,傳值與傳遞引用的語義不會相同,例如,如果涉及到複製構造函數或原始結構在被調用方中被修改。然而,在很多現實場景中,語義在可觀察行爲方面將是相同的。這些是我所問的情況。

回答

22

不要忘記,在C/C++中編譯器需要能夠僅基於函數聲明來編譯對函數的調用。

鑑於調用者可能僅使用該信息,編譯器無法編譯該函數以利用您正在討論的優化。調用者不知道該函數將不會修改任何內容,因此它不能通過ref。由於一些呼叫者可能因缺乏詳細信息而傳遞價值,所以函數必須按照價值傳遞進行編譯,並且每個人都需要按價值傳遞。

請注意,即使您將該參數標記爲'const',編譯器仍然無法執行優化,因爲該函數可能在說謊並丟棄了常量(這是允許的並且定義良好,只要傳入的對象實際上不是const)。

我認爲,對於靜態函數(或匿名命名空間中的那些函數),編譯器可能會進行您正在討論的優化,因爲該函數沒有外部鏈接。只要函數的地址沒有傳遞給其他例程或存儲在指針中,就不應該從其他代碼中調用。在這種情況下,編譯器可以充分了解所有調用者,所以我認爲它可以進行優化。

我不確定是否有這樣做(實際上,如果有的話,我會感到驚訝,因爲它可能無法經常應用)。

當然,作爲程序員(使用C++時),只要有可能,就可以使用const&參數強制編譯器執行此優化。我知道你在問爲什麼編譯器不能自動執行,但我認爲這是最好的。

+0

當進行鏈接時間優化,也就是鏈接時間代碼生成或整個程序編譯時,編譯器不需要僅基於聲明來編譯該調用。它充分了解發生了什麼。爲了編譯對大小和速度敏感的嵌入式應用程序,鏈接時間代碼生成是唯一的方法。 – 2015-02-03 16:16:22

10

一個答案是,編譯器需要檢測被調用的方法不會以任何方式修改結構的內容。如果確實如此,那麼通過引用傳遞的效果將不同於傳遞值。

+2

你可以通過寫入時複製語義結構等。 – 2012-01-22 19:57:29

+0

是的,我想你可以讓你的語言的ABI聲明結構是通過引用傳遞的,但如果被調用者試圖修改它們,它就會變成副本。聽起來有點混亂 - 你需要使用所有棘手的最壞情況混疊分析來確定它們保證不被觸摸。只是爲了在語言中有明確的規則更容易 - 如果你不想傳遞一個結構來減慢你的速度,請給它一個引用或指針。 – Edmund 2012-01-24 05:00:55

11

問題是你要求編譯器做出關於用戶代碼意圖的決定。也許我希望我的超級大結構被值傳遞,以便我可以在複製構造函數中做些什麼。相信我,在那裏有人有他們有效需要在副本構造函數中調用這樣的場景。通過引用切換將繞過複製構造函數。

這是一個編譯器生成的決定將是一個壞主意。原因在於它無法推斷代碼的流向。你不能看電話,並知道它會做什麼。你必須a)知道代碼和b)猜測編譯器優化。

4

確實如果某些語言的編譯器可以訪問被調用的函數,並且他們可以假定被調用的函數不會改變,那麼編譯器可以這樣做。這有時被稱爲全局優化,看起來很可能某些C或C++編譯器實際上會優化這種情況 - 更可能是通過將代碼嵌入這樣一個簡單的函數中。

3

有很多理由傳遞價值,並讓編譯器優化你的意圖可能會破壞你的代碼。

例如,被調用函數以任何方式修改結構。如果你打算將結果傳回給調用者,那麼你可以傳遞一個指針/引用或者自己返回它。

您要求編譯器執行的操作是更改代碼的行爲,這將被視爲編譯器錯誤。

如果你想進行優化並通過引用傳遞,那麼通過一切手段修改某人現有的函數/方法定義來接受引用;這並不是那麼難。你可能會驚訝於你沒有意識到的破裂。

2

按值更改爲引用將更改函數的簽名。如果函數不是靜態的,則會導致其他編譯單元發生鏈接錯誤,這些編譯單元不知道您所做的優化。
的確,實現這種優化的唯一方法是通過某種後鏈接全局優化階段。這些非常難以完成,但一些編譯器在某種程度上做了它們。

1

嗯,簡單的答案是結構在內存中的位置是不同的,因此你傳遞的數據是不同的。我認爲更復雜的答案是線程化。

您的編譯器需要檢測a)foo不會修改結構; b)foo不對結構元素的物理位置進行任何計算;和c)調用者或調用者產生的另一個線程在foo運行完成之前不會修改該結構。

在你的例子中,可以想象編譯器可以做這些事情 - 但保存的內存是不重要的,可能不值得猜測。如果你用一個有200萬個元素的結構運行同一個程序會發生什麼?

2

我認爲這絕對是您可以實現的一種優化(在一些假設下,請參見最後一段),但我不清楚它是否有利可圖。而不是將參數推入堆棧(或根據調用約定將參數傳遞到寄存器),您可以推送一個指針,通過它讀取值。這種額外的間接會花費週期。它也需要傳入的參數在內存中(所以你可以指向它)而不是在寄存器中。如果傳遞的記錄具有許多字段並且接收記錄的功能只讀取其中的一部分,那將是有益的。間接浪費的額外週期將不得不彌補通過推送不需要的字段而浪費的週期。

您可能會對LLVM中實際實施的反向優化argument promotion感到驚訝。這將內部函數的參數參數轉換爲一個值參數(或一個聚集爲標量),只有少量字段只能從中讀取。這對於通過引用幾乎可以傳遞所有內容的語言特別有用。如果按照dead argument elimination進行操作,則不必傳遞未觸及的字段。

它承擔提的是,這種變化的函數被調用只能在被優化功能的工作方式優化是內部的模塊被編譯(您可以通過使用C聲明一個函數static,並與C++模板得到這個)。優化器不僅要解決功能問題,還要解決所有的調用點。這使得這種優化的範圍相當有限,除非您在鏈接時進行優化。另外,當涉及拷貝構造函數時(如其他海報所提到的),優化永遠不會被調用,因爲它可能會改變程序的語義,優秀的優化器永遠不應該這樣做。

2

傳遞引用只是傳遞地址/指針的語法糖。所以函數必須隱式地引用一個指針來讀取參數的值。解引用指針可能會更昂貴(如果在循環中),那麼結構拷貝用於按值複製。

更重要的是,像其他人一樣,傳遞引用具有不同於傳遞值的語義。 const參考做不是表示參考值不會改變。其他函數調用可能會更改引用的值。

1

編譯器將需要確保傳遞進來(如調用代碼命名)的結構沒有被修改

double x; // using non structs, oh-well 

void Foo(double d) 
{ 
     x += d; // ok 
     x += d; // Oops 
} 

void main() 
{ 
    x = 1; 
    Foo(x); 
}