簡體   English   中英

C ++:結構訪問速度比基本變量慢?

[英]C++: Structs slower to access than basic variables?

我找到了一些像這樣“優化”的代碼:

void somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
}

並且評論說它會使變量訪問速度更快,但我一直認為結構元素訪問速度和它畢竟不是結構一樣快。

有人能清除我的頭腦嗎?

經驗法則:它並不慢,除非探查者說它是。 讓編譯器擔心微觀優化(他們對他們非常聰明;畢竟,他們已經做了多年)並專注於更大的圖景。

通常的優化規則(Michael A. Jackson)適用:1。不要這樣做。 2.(僅限專家:)不要這樣做。

話雖如此,讓我們假設它是最內層的循環,占用了性能關鍵型應用程序的80%的時間。 即便如此,我懷疑你會看到任何不同。 讓我們使用這段代碼:

struct Xyz {
    float x, y, z;
};

float f(Xyz param){
    return param.x + param.y + param.z;
}

float g(Xyz param){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    return x + y + z;
}

通過LLVM運行它顯示:只有沒有優化,兩者按預期運行( g將結構成員復制到本地,然后對這些進行求和; f直接從param獲取的值)。 使用標准優化級別,兩者都會產生相同的代碼(提取值一次,然后對它們求和)。

對於簡短的代碼,這種“優化”實際上是有害的,因為它不必要地復制浮動。 對於在幾個地方使用成員的較長代碼,如果你主動告訴你的編譯器是愚蠢的,那么它可能會有點幫助。 使用65(而不是2)添加成員/本地的快速測試證實了這一點:在沒有優化的情況下, f重復加載結構成員,而g重用已經提取的本地成員。 優化版本再次相同,並且僅提取成員一次。 (令人驚訝的是,即使啟用LTO,也沒有強度降低將增加轉化為乘法,但這只表明所使用的LLVM版本無論如何都沒有過於激烈地優化 - 因此它應該在其他編譯器中也能正常工作。)

所以,最重要的是:除非你知道你的代碼必須由編譯器編譯,這個編譯器非常愚蠢和/或古老,以至於它不會優化任何東西,你現在已經證明了編譯器會使兩種方式都相同而且可以因此,以表演的名義廢除了這種可讀性和釀造性犯罪。 (如有必要,請為您的特定編譯器重復實驗。)

我不是編譯大師,所以請耐心等待。 我猜測代碼的原始作者假設通過將結構中的值復制到局部變量,編譯器已將這些變量“放置”到某些平台(例如x86)上可用的浮點寄存器中。 如果沒有足夠的寄存器,它們將被放入堆棧中。

話雖這么說,除非這個代碼處於密集計算/循環的中間,否則我會努力清晰而不是速度。 很少有人會注意到時間上的一些指令差異。

您必須查看特定實現的已編譯代碼才能確定,但​​原則上沒有理由為什么您的首選代碼(使用struct成員)必須比您顯示的代碼慢(復制到變量中)然后使用變量)。

someFunc按值獲取結構,因此它有自己的該結構的本地副本。 編譯器完全可以自由地對結構成員應用完全相同的優化,因為它將應用於float變量。 它們都是自動變量,在這兩種情況下,“as-if”規則允許它們存儲在寄存器中而不是存儲器中,前提是函數產生正確的可觀察行為。

這是當然除非您使用指向結構的指針並使用它,在這種情況下,值需要在指針指向的正確順序的某處寫入內存。 這開始限制優化,並且由於如果傳遞指向自動變量的指針,編譯器不能再認為變量名是對該存儲器的唯一引用,因此它的內容的唯一方式,這引入了其他限制。可以修改。 對同一對象進行多次引用稱為“別名”,並且有時會阻止在對象以某種方式被稱為別名時可以進行的優化。

然后,如果這是一個問題,並且函數中的其余代碼以某種方式確實使用了指向結構的指針,那么當然你可以在狡猾的基礎上將值復制到正確性的POV中的變量。 因此,聲稱的優化並不像在這種情況下看起來那么簡單。

現在,可能有特定的編譯器(或特定的優化級別)無法應用於結構化它們被允許應用的所有優化,但是對浮點變量應用等效優化。 如果是這樣,則評論是正確的,這就是為什么你必須檢查以確定。 例如,可以比較發出的代碼:

float somefunc(SomeStruct param){
    float x = param.x; // param.x and x are both floats. supposedly this makes it faster access
    float y = param.y;
    float z = param.z;
    for (int i = 0; i < 10; ++i) {
        x += (y +i) * z;
    }
    return x;
}

有了這個:

float somefunc(SomeStruct param){
    for (int i = 0; i < 10; ++i) {
        param.x += (param.y +i) * param.z;
    }
    return param.x;
}

可能還存在優化級別,其中額外的變量使代碼變得更糟。 我不確定我是否非常信任代碼評論,這些評論說“據說這可以讓它更快地訪問”,聽起來像作者並不清楚它為什么重要。 “顯然它可以更快地訪問 - 我不知道為什么,但是確認這一點的測試並證明它在我們程序的上下文中產生明顯的差異,在以下位置的源代碼控制中”更像是它;-)

在未經優化的代碼中:

  • 函數參數(不通過引用傳遞)在堆棧上
  • 局部變量也在堆棧上

對匯編語言中的局部變量和函數參數的未優化訪問看起來或多或少如下:

mov %eax, %ebp+ compile-time-constant

其中%ebp是一個幀指針(一個函數的'this'指針)。

如果訪問參數或局部變量,則沒有區別。

您從結構中訪問元素的事實與程序集/機器的觀點完全沒有區別。 結構是用C語言構造的結構,使程序員的生活更輕松。

所以,最后,我的答案是:不,這樣做絕對沒有任何好處。

在使用指針時,有充分和有效的理由進行這種優化,因為消耗所有輸入首先使編譯器免於可能的別名問題,從而阻止它產生最佳代碼(盡管現在也有限制)。

對於非指針類型,理論上存在開銷,因為每個成員都是通過struct的this指針訪問的。 理論上,這可能在內環中是顯而易見的,否則在理論上將是一個小的開銷。
然而,在實踐中,現代編譯器幾乎總是(除非存在復雜的繼承層次結構)產生完全相同的二進制代碼。

我問過自己與兩年前的問題完全相同的問題,並使用gcc 4.4進行了非常廣泛的測試用例。 我的發現是,除非你真的試圖在編譯器的兩腿之間故意扔掉,否則生成的代碼絕對沒有區別。

編譯器可以制作更快的代碼來復制float-to-float。 但是當x將被使用時,它將被轉換為內部FPU表示。

當您指定要操作的“簡單”變量(不是結構/類)時,系統只需要去那個地方並獲取它想要的數據。

但是當你引用結構或類中的變量(如AB ,系統需要計算B在該區域內的位置A (因為可能在其之前聲明了其他變量),並且該計算需要比上面描述的更簡單的訪問。

Piotr給出了真正的答案。 這個只是為了好玩。

我測試了它。 這段代碼:

float somefunc(SomeStruct param, float &sum){
    float x = param.x;
    float y = param.y;
    float z = param.z;
    float xyz = x * y * z;
    sum = x + y + z;
    return xyz;
}

這段代碼:

float somefunc(SomeStruct param, float &sum){
    float xyz = param.x * param.y * param.z;
    sum = param.x + param.y + param.z;
    return xyz;
}

使用g++ -O2編譯時生成相同的匯編代碼。 但是,它們會在關閉優化的情況下生成不同的代碼。 這是區別:

<   movl    -32(%rbp), %eax
<   movl    %eax, -4(%rbp)
<   movl    -28(%rbp), %eax
<   movl    %eax, -8(%rbp)
<   movl    -24(%rbp), %eax
<   movl    %eax, -12(%rbp)
<   movss   -4(%rbp), %xmm0
<   mulss   -8(%rbp), %xmm0
<   mulss   -12(%rbp), %xmm0
<   movss   %xmm0, -16(%rbp)
<   movss   -4(%rbp), %xmm0
<   addss   -8(%rbp), %xmm0
<   addss   -12(%rbp), %xmm0
---
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   mulss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   mulss   %xmm1, %xmm0
>   movss   %xmm0, -4(%rbp)
>   movss   -32(%rbp), %xmm1
>   movss   -28(%rbp), %xmm0
>   addss   %xmm1, %xmm0
>   movss   -24(%rbp), %xmm1
>   addss   %xmm1, %xmm0

標記為<的行對應於具有“優化”變量的版本。 在我看來,“優化”版本甚至比沒有額外變量的版本更慢。 但是,這是可以預期的,因為x,y和z在堆棧上分配,就像param一樣。 分配更多堆棧變量以復制現有堆棧變量有什么意義?

如果執行“優​​化”的人更了解語言,他可能會將這些變量聲明為register ,但即使這樣,“優化”版本也會稍微變慢和變長,至少在G ++ / x86-64上。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM