簡體   English   中英

C編譯器和循環優化

[英]C compilers and loop optimisation

我對編譯器實際優化的方式沒有很多經驗,不同級別之間有什么區別(例如-O2與-cc3對於gcc)。 因此,我不確定以下兩個語句對於任意編譯器是否相同:

for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}

variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}

從處理時間的角度來看,只計算variable1和variable2的乘積,因為它們在循環中不會改變,這是有意義的。 然而,這需要額外的內存,而且我不確定優化器對此開銷的影響有多大。如果您從紙張/書籍中獲得等式並希望將其轉換為計算機可讀的內容,則第一個表達式是最容易閱讀的。但第二個可能是最快的 - 特別是對於在循環內有很多未改變的變量的更復雜的方程式(我有一些非常令人討厭的非線性微分方程,我希望在代碼中是人類可讀的)。 如果我將變量聲明為常量,是否有任何變化? 我希望我的問題對任意編譯器都有意義,因為我同時使用gcc,Intel和Portland編譯器。

對於任意編譯器來說,很難充分回答這個問題。 使用此代碼可以做什么不僅取決於編譯器,還取決於目標體系結構。 我將嘗試解釋具有良好功能的生產編譯器可以對此代碼執行的操作。

從處理時間的角度來看,只計算variable1和variable2的乘積,因為它們在循環中不會改變,這是有意義的。

你是對的。 正如Cat先生所指出的那樣,這被稱為共同的子表達式消除 因此,編譯器可以生成代碼僅計算表達式一次(或者如果已知兩個操作數的值一次是常量,則甚至在編譯時計算它)。

如果可以確定函數沒有副作用,那么合適的編譯器也可以對函數執行子表達式消除。 例如,GCC可以在函數體可用時分析函數,但也有pureconst屬性可用於專門標記應該進行此優化的函數 (請參閱函數屬性 )。

鑒於沒有副作用,編譯器能夠確定它(在您的示例中,沒有任何阻礙),其中兩個片段在這方面是相同的(我已經檢查過clang :-))。

然而,這需要額外的內存,而且我不確定優化器對此開銷有多大影響。

實際上,這不需要任何額外的內存。 乘法在處理器寄存器中完成,結果也存儲在寄存器中。 這是一個消除大量代碼並使用單個寄存器來存儲結果的問題,這總是很好的(並且當涉及到寄存器分配時 ,尤其是在循環中,這肯定會使生活更輕松)。 因此,如果可以進行此優化,則無需額外費用即可完成。

第一個表達式是最容易閱讀的..

GCC和Clang都將執行此優化。 我不確定其他編譯器,所以你必須自己檢查。 但很難想象任何好的編譯器都沒有進行子表達式消除。

如果我將變量聲明為常量,是否有任何變化?

有可能。 這稱為常量表達式 - 僅包含常量的表達式。 可以在編譯期間而不是在運行時評估常量表達式。 因此,例如,如果多個A,B和C,其中A和B都是常量,編譯器將針對該預先計算的值預先計算A*B表達式多個C. 如果編譯器可以在編譯時確定其值並確保它不被更改,則編譯器也可以使用非常量值執行此操作。 例如:

$ cat test.c
inline int foo(int a, int b)
{
    return a * b;
}

int main() {
    int a;
    int b;
    a = 1;
    b = 2;
    return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70    movl    $0x00000002,%eax
0000000100000f75    ret

在上述片段的情況下,還可以進行其他優化。 以下是我想到的一些問題:

第一個最明顯的是循環展開。 由於迭代次數在運行時是已知的,因此編譯器可能決定展開循環 是否應用此優化取決於體系結構(即某些CPU可以“鎖定您的循環”並比其展開版本更快地執行代碼,這也通過使用更少的空間使代碼更加緩存友好,避免額外的μOP融合階段等)。

第二個優化可以加速50倍,使用SIMD指令(SSE,AVX等)。 例如,GCC非常擅長(英特爾必須也是如此,如果不是更好的話)。 我已經驗證了以下功能:

uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
    uint8_t s = 0;
    size_t i;
    for (i = 0; i < size; ++i)
        s = (uint8_t)(s + p[i]);
    return s;
}

...被轉換為一個循環,其中每個步驟一次求和16個值(即在_mm_add_epi8 ),附加的代碼處理對齊和奇數(<16)迭代計數。 然而,Clang在我最后一次檢查時完全失敗了。 因此,即使迭代次數未知,GCC也可能以這種方式減少循環。

如果我願意,我建議你不要優化你的代碼,除非你發現它是一個瓶頸。 否則你可能會浪費很多時間進行虛假和過早的優化。

我希望這回答了你的問題。 祝好運!

是的,您可以指望編譯器在執行子表達式消除方面做得很好,即使是通過循環也是如此。 這可能會導致內存使用量略有增加,但所有這些都會被任何體面的編譯器所考慮,並且幾乎總是這樣做是為了執行子表達式消除(因為我們所討論的內存是寄存器和L1緩存)。

這里有一些快速測試,以“證明”它自己。 結果表明你基本上不應該嘗試超越編譯器進行手動子表達式消除,只需自然地編寫代碼並讓編譯器做它擅長的事情(這就像弄清楚哪些表達式應該被真正消除而哪些不應該給出目標架構和周圍代碼。)

稍后,如果您對代碼的性能不滿意,您應該使用分析器來查看代碼並查看哪些語句和表達式占用最多的時間,然后嘗試確定是否可以重新組織代碼以幫助編譯出來,但我會說絕大多數時候它不會是這樣的簡單事情,它會做一些事情來減少緩存停頓(即更好地組織數據),消除冗余的程序間計算,以及像那。

(FTR在以下代碼中使用random只是確保編譯器不能過於熱衷於變量消除和循環展開)

PROG1:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    for (i = 0; i < loop_end; ++i) {
        ret += a * b * values[i % 10];
    }

    return ret;
}

PROG2:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    int c = a * b;
    for (i = 0; i < loop_end; ++i) {
        ret += c * values[i % 10];
    }

    return ret;
}

以下是結果:

> gcc -O2 prog1.c -o prog1; time ./prog1  
./prog1  1.62s user 0.00s system 99% cpu 1.630 total

> gcc -O2 prog2.c -o prog2; time ./prog2
./prog2  1.63s user 0.00s system 99% cpu 1.636 total

(這是測量牆壁時間,所以不要注意0.01秒的差異,運行幾次它們都在1.62-1.63秒的范圍內,所以它們的速度相同)

有趣的是,在沒有優化的情況下編譯時prog1更快:

> gcc -O0 prog1.c -o prog1; time ./prog1  
./prog1  2.83s user 0.00s system 99% cpu 2.846 total

> gcc -O0 prog2.c -o prog2; time ./prog2 
./prog2  2.93s user 0.00s system 99% cpu 2.946 total

同樣有趣的是,使用-O1編譯提供了最佳性能。

gcc -O1 prog1.c -o prog1; time ./prog1 
./prog1  1.57s user 0.00s system 99% cpu 1.579 total

gcc -O1 prog2.c -o prog2; time ./prog2
./prog2  1.56s user 0.00s system 99% cpu 1.563 total

GCC和英特爾是偉大的編譯器,並且非常聰明地處理這樣的事情。 我沒有任何使用Portland編譯器的經驗,但這些對於編譯器來說是非常基本的事情,所以如果它不能很好地處理這種情況,我會感到非常驚訝。

如果我是一個編譯器,我會認識到這兩個循環都沒有左手操作數, 根本沒有副作用(除了將i設置為10 ),所以我只是完全優化循環。

我並不是說這實際發生了; 看起來它可能會從您提供的代碼中發生。

暫無
暫無

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

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