簡體   English   中英

優化C代碼

[英]Optimization of C code

對於高性能計算課程的分配,我需要優化以下代碼片段:

int foobar(int a, int b, int N)
{
    int i, j, k, x, y;
    x = 0;
    y = 0;
    k = 256;
    for (i = 0; i <= N; i++) {
        for (j = i + 1; j <= N; j++) {
            x = x + 4*(2*i+j)*(i+2*k);
            if (i > j){
               y = y + 8*(i-j);
            }else{
               y = y + 8*(j-i);
            }
        }
    }
    return x;
}

使用一些建議,我設法優化代碼(或至少我認為如此),例如:

  1. 不斷傳播
  2. 代數簡化
  3. 復制傳播
  4. 常見的Subexpression消除
  5. 死代碼消除
  6. 循環不變量刪除
  7. 按位移位而不是乘法,因為它們更便宜。

這是我的代碼:

int foobar(int a, int b, int N) {

    int i, j, x, y, t;
    x = 0;
    y = 0;
    for (i = 0; i <= N; i++) {
        t = i + 512;
        for (j = i + 1; j <= N; j++) {
            x = x + ((i<<3) + (j<<2))*t;
        }
    }
    return x;
}

根據我的導師的說法,優化良好的代碼指令應該在匯編語言級別中具有更少或更少成本的指令。因此必須運行,指令在比原始代碼更短的時間內,即使用::

執行時間=指令計數*每條指令的周期

當我使用以下命令生成匯編代碼時: gcc -o code_opt.s -S foobar.c

盡管已經進行了一些優化,但生成的代碼擁有比原始代碼多得多的行,並且運行時間較低,但沒有原始代碼那么多。 我究竟做錯了什么?

不要粘貼匯編代碼,因為兩者都非常廣泛。 所以我在main中調用函數“foobar”,我在linux中使用time命令測量執行時間

int main () {
    int a,b,N;

    scanf ("%d %d %d",&a,&b,&N);
    printf ("%d\n",foobar (a,b,N));
    return 0;
}

y不影響代碼的最終結果 - 刪除:

int foobar(int a, int b, int N)
{
    int i, j, k, x, y;
    x = 0;
    //y = 0;
    k = 256;
    for (i = 0; i <= N; i++) {
        for (j = i + 1; j <= N; j++) {
            x = x + 4*(2*i+j)*(i+2*k);
            //if (i > j){
            //   y = y + 8*(i-j);
            //}else{
            //   y = y + 8*(j-i);
            //}
        }
    }
    return x;
}

k只是一個常數:

int foobar(int a, int b, int N)
{
    int i, j, x;
    x = 0;
    for (i = 0; i <= N; i++) {
        for (j = i + 1; j <= N; j++) {
            x = x + 4*(2*i+j)*(i+2*256);
        }
    }
    return x;
}

內部表達式可以轉換為: x += 8*i*i + 4096*i + 4*i*j + 2048*j 使用math將它們全部推到外循環: x += 8*i*i*(Ni) + 4096*i*(Ni) + 2*i*(Ni)*(N+i+1) + 1024*(Ni)*(N+i+1)

您可以展開上面的表達式,並應用平方和和多維數據集公式的總和來獲得一個緊密的表單表達式,它應該比雙嵌套循環運行得更快。 我把它作為鍛煉留給你。 結果, ij也將被刪除。

ab也應去除如果可能的話-因為ab作為參數提供,但在你的代碼從來沒有使用過。

平方和和方塊總和公式:

  • Sum(x 2 ,x = 1..n)= n(n + 1)(2n + 1)/ 6
  • 總和(X 3,X = 1..N)= N 2(N + 1)2/4

原來:

for (i = 0; i <= N; i++) {
    for (j = i + 1; j <= N; j++) {
        x = x + 4*(2*i+j)*(i+2*k);
        if (i > j){
           y = y + 8*(i-j);
        }else{
           y = y + 8*(j-i);
        }
    }
}

刪除y計算:

for (i = 0; i <= N; i++) {
    for (j = i + 1; j <= N; j++) {
        x = x + 4*(2*i+j)*(i+2*k);
    }
}

分裂ijk

for (i = 0; i <= N; i++) {
    for (j = i + 1; j <= N; j++) {
        x = x + 8*i*i + 16*i*k ;                // multiple of  1  (no j)
        x = x + (4*i + 8*k)*j ;                 // multiple of  j
    }
}

從外部移動它們(並移除運行Ni次數的循環):

for (i = 0; i <= N; i++) {
    x = x + (8*i*i + 16*i*k) * (N-i) ;
    x = x + (4*i + 8*k) * ((N*N+N)/2 - (i*i+i)/2) ;
}

Rewritting:

for (i = 0; i <= N; i++) {
    x = x +         ( 8*k*(N*N+N)/2 ) ;
    x = x +   i   * ( 16*k*N + 4*(N*N+N)/2 + 8*k*(-1/2) ) ;
    x = x +  i*i  * ( 8*N + 16*k*(-1) + 4*(-1/2) + 8*k*(-1/2) );
    x = x + i*i*i * ( 8*(-1) + 4*(-1/2) ) ;
}

重寫 - 重新計算:

for (i = 0; i <= N; i++) {
    x = x + 4*k*(N*N+N) ;                            // multiple of 1
    x = x +   i   * ( 16*k*N + 2*(N*N+N) - 4*k ) ;   // multiple of i
    x = x +  i*i  * ( 8*N - 20*k - 2 ) ;             // multiple of i^2
    x = x + i*i*i * ( -10 ) ;                        // multiple of i^3
}

外部的另一個移動(並刪除i循環):

x = x + ( 4*k*(N*N+N) )              * (N+1) ;
x = x + ( 16*k*N + 2*(N*N+N) - 4*k ) * ((N*(N+1))/2) ;
x = x + ( 8*N - 20*k - 2 )           * ((N*(N+1)*(2*N+1))/6);
x = x + (-10)                        * ((N*N*(N+1)*(N+1))/4) ;

上述循環刪除都使用求和公式:

Sum(1,i = 0..n)= n + 1
Sum(i 1 ,i = 0..n)= n(n + 1)/ 2
Sum(i 2 ,i = 0..n)= n(n + 1)(2n + 1)/ 6
總和(I 3,I = 0..N)= N 2(N + 1)2/4

此函數與以下公式等效,該公式僅包含4個整數乘法1個整數除法

x = N * (N + 1) * (N * (7 * N + 8187) - 2050) / 6;

為此,我只需將嵌套循環計算的總和輸入Wolfram Alpha

sum (sum (8*i*i+4096*i+4*i*j+2048*j), j=i+1..N), i=0..N

是解決方案的直接鏈接。 在編碼前思考。 有時你的大腦可以比任何編譯器更好地優化代碼。

簡單地掃描第一個例程,你注意到的第一件事是涉及“y”的表達式是完全未使用的並且可以被刪除(正如你所做的那樣)。 這進一步允許消除if / else(就像你一樣)。

剩下的是兩個for循環和凌亂的表達。 下一步就是將那些不依賴於j表達式分解出來。 你刪除了一個這樣的表達式,但是(i<<3) (即i * 8)保留在內循環中,可以刪除。

Pascal的回答提醒我,你可以使用循環步幅優化。 首先移動(i<<3) * t離開內循環(稱之為i1 ),然后在初始化循環時計算等於(i<<2) * t的值j1 在每次迭代時,將j1增加4 * t (這是預先計算的常數)。 x = x + i1 + j1;替換內部表達式x = x + i1 + j1;

有人懷疑可能有某種方法可以將兩個循環合二為一,但是我沒有看到它。

我能看到的其他一些事情。 您不需要y ,因此您可以刪除其聲明和初始化。

此外,實際上並未使用傳入ab的值,因此您可以將它們用作局部變量而不是xt

此外,不是每次通過你添加i到512,你可以注意到t從512開始並且每次迭代增加1。

int foobar(int a, int b, int N) {
    int i, j;
    a = 0;
    b = 512;
    for (i = 0; i <= N; i++, b++) {
        for (j = i + 1; j <= N; j++) {
            a = a + ((i<<3) + (j<<2))*b;
        }
    }
    return a;
}

一旦你到達這一點,你還可以觀察到,除了初始化jij僅用於單個多個 - i<<3j<<2 我們可以直接在循環邏輯中編碼,因此:

int foobar(int a, int b, int N) {
    int i, j, iLimit, jLimit;
    a = 0;
    b = 512;
    iLimit = N << 3;
    jLimit = N << 2;
    for (i = 0; i <= iLimit; i+=8) {
        for (j = i >> 1 + 4; j <= jLimit; j+=4) {
            a = a + (i + j)*b;
        }
        b++;
    }
    return a;
}

好的......所以這是我的解決方案,以及內聯評論來解釋我做了什么以及如何做。

int foobar(int N)
{ // We eliminate unused arguments 
    int x = 0, i = 0, i2 = 0, j, k, z;

    // We only iterate up to N on the outer loop, since the
    // last iteration doesn't do anything useful. Also we keep
    // track of '2*i' (which is used throughout the code) by a 
    // second variable 'i2' which we increment by two in every
    // iteration, essentially converting multiplication into addition.
    while(i < N) 
    {           
        // We hoist the calculation '4 * (i+2*k)' out of the loop
        // since k is a literal constant and 'i' is a constant during
        // the inner loop. We could convert the multiplication by 2
        // into a left shift, but hey, let's not go *crazy*! 
        //
        //  (4 * (i+2*k))         <=>
        //  (4 * i) + (4 * 2 * k) <=>
        //  (2 * i2) + (8 * k)    <=>
        //  (2 * i2) + (8 * 512)  <=>
        //  (2 * i2) + 2048

        k = (2 * i2) + 2048;

        // We have now converted the expression:
        //      x = x + 4*(2*i+j)*(i+2*k);
        //
        // into the expression:
        //      x = x + (i2 + j) * k;
        //
        // Counterintuively we now *expand* the formula into:
        //      x = x + (i2 * k) + (j * k);
        //
        // Now observe that (i2 * k) is a constant inside the inner
        // loop which we can calculate only once here. Also observe
        // that is simply added into x a total (N - i) times, so 
        // we take advantange of the abelian nature of addition
        // to hoist it completely out of the loop

        x = x + (i2 * k) * (N - i);

        // Observe that inside this loop we calculate (j * k) repeatedly, 
        // and that j is just an increasing counter. So now instead of
        // doing numerous multiplications, let's break the operation into
        // two parts: a multiplication, which we hoist out of the inner 
        // loop and additions which we continue performing in the inner 
        // loop.

        z = i * k;

        for (j = i + 1; j <= N; j++) 
        {
            z = z + k;          
            x = x + z;      
        }

        i++;
        i2 += 2;
    }   

    return x;
}

代碼,沒有任何解釋歸結為:

int foobar(int N)
{
    int x = 0, i = 0, i2 = 0, j, k, z;

    while(i < N) 
    {                   
        k = (2 * i2) + 2048;

        x = x + (i2 * k) * (N - i);

        z = i * k;

        for (j = i + 1; j <= N; j++) 
        {
            z = z + k;          
            x = x + z;      
        }

        i++;
        i2 += 2;
    }   

    return x;
}

我希望這有幫助。

int foobar(int N)//避免不使用傳遞參數

{

int i, j, x=0;   //Remove unuseful variable, operation so save stack and Machine cycle

for (i = N; i--; )               //Don't check unnecessary comparison condition 

   for (j = N+1; --j>i; )

     x += (((i<<1)+j)*(i+512)<<2);  //Save Machine cycle ,Use shift instead of Multiply

return x;

}

暫無
暫無

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

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