【问题标题】:Optimization of C codeC代码优化
【发布时间】:2012-11-13 09:34:33
【问题描述】:

对于一门名为“高性能计算”的课程,我需要优化以下代码片段:

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. 通用子表达式消除
  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;
}

【问题讨论】:

  • 常数传播、代数简化、复制传播、公共子表达式消除、死代码消除、循环不变删除,以及使用按位移位而不是乘法,因为它们更便宜 - 有趣事实:这些正是现代优化编译器可以自己完成的“简单”优化,而且通常比程序员更好;出于这个原因,通常程序员关心更“高级”(例如算法)优化(或更微妙的东西 - 例如与缓存相关 - 通常需要分析)。
  • 告诉编译器你想要所有优化的花里胡哨,因为it is smarter than you about this stuff?
  • 运行gcc 和-O3 应该有一些效果。虽然我不确定优化编译器在多大程度上违背了本练习的目的。
  • 不幸的是,许多“HPC 编程”课程看起来像是直接从 1970 年代传来的。去过也做过。不幸的是,对于 OP,基于这个任务,包括这个任务。 :(
  • 另外:只有我一个人,还是整个函数看起来可以被扁平化为一个单一的、无循环的表达式?

标签: c performance optimization compiler-optimization bit-shift


【解决方案1】:

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;

}

【讨论】:

    【解决方案2】:

    最初:

    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);
        }
    }
    

    拆分i、j、k:

    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
        }
    }
    

    将它们移到外部(并删除运行N-i 次的循环):

    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) ;
    }
    

    重写:

    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) ;
    

    上述两个循环删除都使用 summation 公式:

    总和(1, i = 0..n) = n+1
    Sum(i1, i = 0..n) = n(n + 1)/2
    Sum(i2, i = 0..n) = n(n + 1)(2n + 1)/6
    Sum(i3, i = 0..n) = n2(n + 1)2/4

    【讨论】:

    • @Mysticial:可能有点像“看起来太复杂了”。干得好,超级立方体!
    • @Mysticial:大概是“不要替人家做作业”,我承认这是我非常同情的观点。
    • 我喜欢这种方法,但这根本不会返回正确的结果。小例子:用 N = 1 运行它,它返回 2049 而不是正确的 2048。
    • @NikB.:你确定吗? codepad test 显示 2048。
    • 它对我有用,在 VS2010 64 位上。稍后我将使用不同的编译器仔细检查。那是函数的整个主体(加上int x=0, k=256;),然后是return x?
    【解决方案3】:

    好的...这是我的解决方案,以及用于解释我做了什么以及如何做的内联 cmets。

    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;
    }
    

    我希望这会有所帮助。

    【讨论】:

      【解决方案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
      

      Here 是解决方案的直接链接。编码前三思。有时你的大脑可以比任何编译器更好地优化代码。

      【讨论】:

      • 这就是为什么许多开发人员被数学家取代的原因。 ;)
      • 这包括最重要的一课。在代码中移动小位将使您的速度提高 10%-1000%。一个更好的算法,比如这个,可能会导致一百倍的加速,这是任何其他“优化”都不可能的。
      • @Alexander 另一个教训是,不要手动进行形式推导,如果可能,请使用符号数学软件。其他人也尝试推导出公式,但没有人找到最简单的公式,即 Wolfram Alpha 推导出的公式。
      • 我很确定很多人可以手动找到公式。不比 Wolfram Alpha 或任何其他符号计算器快,但同样好。
      • @ypercube 你是绝对正确的。当我写那条评论时,我正在倾听其他答案的诞生,我很惊讶地看到人们努力推导出正确的公式。我不想伤害任何人,我只是想强调符号计算器的用途。
      【解决方案5】:

      我可以看到其他一些东西。你不需要y,所以你可以删除它的声明和初始化。

      此外,为a 和b 传入的值实际上并未使用,因此您可以将它们用作局部变量,而不是x 和t。

      此外,您可以注意到t 从 512 开始,每次迭代递增 1,而不是每次都将 i 添加到 512。

      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;
      }
      

      一旦你到达这一点,你还可以观察到,除了初始化 j 之外,i 和 j 仅在单个多个中使用 - i&lt;&lt;3 和 j&lt;&lt;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;
      }
      

      【讨论】:

        【解决方案6】:

        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。使用数学将它们全部推到外循环:x += 8*i*i*(N-i) + 4096*i*(N-i) + 2*i*(N-i)*(N+i+1) + 1024*(N-i)*(N+i+1)。

        您可以扩展上面的表达式,并应用sum of squares and sum of cubes formula 来获得一个封闭形式的表达式,它应该比双重嵌套循环运行得更快。我把它作为练习留给你。因此,i 和 j 也将被删除。

        a 和 b 也应尽可能删除 - 因为 a 和 b 作为参数提供,但从未在您的代码中使用。

        平方和和立方和公式:

        • 总和(x2, x = 1..n) = n(n + 1)(2n + 1)/6
        • Sum(x3, x = 1..n) = n2(n + 1)2/4

        【讨论】:

        • 如果我没有错字:x = (7 * N^4 + 8194 * N^3 + 6137 * N^2 - 2050 * N) / 6
        • 谢谢@nhahtdh!附带说明:对这个术语的评估可能会导致溢出。一些重新排列可能有助于解决这个问题(例如拉出 N,对大部分进行排序以使大部分可被 6 整除)
        • @stefan:我觉得拉出可整除的部分,计算余数除以6,然后做减法,再做加法应该是一个好顺序。 (N^4 + 4*N^3 + 5*N^2 - 4*N)/6 - 341*N + ...
        • 是的,我会按照N * ((N + 1)(N(1365 + N) - 342) + (N-2)(N-1)(N+1)/6) 的方式做一些事情:D
        • 不,仅当您处理整数时;-) 要允许这样做,只需像这样拆分它:N ((N + 1)(N(1365 + N) - 342)) + N(N-2)(N-1)(N+1)/6
        【解决方案7】:

        简要浏览第一个例程,您首先注意到的是涉及“y”的表达式完全未使用并且可以删除(就像您所做的那样)。这进一步允许消除 if/else(就像您所做的那样)。

        剩下的是两个for 循环和凌乱的表达式。下一步是分解该表达式中不依赖于j 的部分。您删除了一个这样的表达式,但 (i&lt;&lt;3)(即 i * 8)仍保留在内循环中,并且可以删除。

        Pascal 的回答提醒我,您可以使用循环步幅优化。首先将(i&lt;&lt;3) * t 移出内部循环(称为i1),然后在初始化循环时计算等于(i&lt;&lt;2) * t 的值j1。在每次迭代中将j1 增加4 * t(这是一个预先计算的常数)。用x = x + i1 + j1;替换你的内心表达。

        有人怀疑可能有某种方法可以将两个循环合并为一个循环,但我没有立即看到它。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-01-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-01
          相关资源
          最近更新 更多