【问题标题】:while (n > 1) is 25% faster than while (n)?while (n > 1) 比 while (n) 快 25%?
【发布时间】:2011-09-17 02:20:57
【问题描述】:

我有两个逻辑上等价的函数:

long ipow1(int base, int exp) {
    // HISTORICAL NOTE:
    // This wasn't here in the original question, I edited it in,
    if (exp == 0) return 1;

    long result = 1;

    while (exp > 1) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result * base;
}

long ipow2(int base, int exp) { 
    long result = 1;

    while (exp) {
        if (exp & 1) result *= base;
        exp >>= 1;
        base *= base;
    }

    return result;
}

注意:

这些循环是等价的,因为在前一种情况下,我们返回result * base(处理exp 被缩减为1 的情况),但在第二种情况下,我们是返回result


奇怪的是,-O3-O0 ipow1 的表现都比 ipow2 高出约 25%。这怎么可能?

我使用的是 Windows 7、x64、gcc 4.5.2 并使用 gcc ipow.c -O0 -std=c99 进行编译。

这是我的分析代码:

int main(int argc, char *argv[]) {
    LARGE_INTEGER ticksPerSecond;
    LARGE_INTEGER tick;
    LARGE_INTEGER start_ticks, end_ticks, cputime;

    double totaltime = 0;
    int repetitions = 10000;
    int rep = 0;
    int nopti = 0;

    for (rep = 0; rep < repetitions; rep++) {
        if (!QueryPerformanceFrequency(&ticksPerSecond)) printf("\tno go QueryPerformance not present");
        if (!QueryPerformanceCounter(&tick)) printf("no go counter not installed");  
        QueryPerformanceCounter(&start_ticks); 

        /* start real code */

        for (int i = 0; i < 55; i++) {
            for (int j = 0; j < 11; j++) {
                nopti = ipow1(i, j); // or ipow2
            }
        }

        /* end code */

        QueryPerformanceCounter(&end_ticks); 
        cputime.QuadPart = end_ticks.QuadPart - start_ticks.QuadPart;
        totaltime += (double)cputime.QuadPart / (double)ticksPerSecond.QuadPart;
    }   

    printf("\tTotal elapsed CPU time:   %.9f  sec  with %d repetitions - %ld:\n", totaltime, repetitions, nopti);

    return 0;
}

【问题讨论】:

  • @quasiverse:看代码,两个函数在逻辑上是相等的。
  • ipow1 执行比ipow2 少两个“步骤”(exp &gt; 1exp != 0 相对),但返回result * base 而不是result,使它们实际上相等。在你的脑海中尝试一下。
  • @nightcracker:伙计,这是一个很多人都会犯的简单错误,因为它的呈现方式。三个人已经犯了同样的错误,如果不是这里的 cmets,还有更多人会犯同样的错误。你不需要沾沾自喜。
  • @Jason:我有点过于激进了,对不起。
  • 虽然这两个计算给出了相同的结果,但它们并不相同——ipow2 最终会执行两个额外的分支,编译器不够聪明,无法删除。

标签: c performance gcc micro-optimization


【解决方案1】:

不,真的,两者不等价。 ipow2ipow1 不返回时返回正确的结果。

http://ideone.com/MqyqU

附:我不在乎你留下多少“解释”为什么它们是一样的,只需要一个反例就可以反驳你的说法。

附言-1 关于你对已经试图向你指出这一点的每个人的令人难以忍受的傲慢的问题。

【讨论】:

  • 啊,你是对的,你一定知道我在以前的版本中有if (exp == 0) return 1,但我删除了它,因为我认为它已经过时了,但事实并非如此。问题仍然存在。
  • ...也许不是,我必须测试一下,一个分支可能会补偿其余代码的差异。另外,请您解释一下我的“无法忍受的傲慢”,因为我还没有发现任何评论指出了我的这种差异。
  • @nightcracker,我有足够的代表来查看已删除的答案以及您留下的 cmets。 Jason 的回答告诉你,exp = 0 是不同的。我引用:“这些在逻辑上是不等价的。你需要exp &gt; 0exp &gt;=1 才能让它们等价。”他是完全正确的,当且仅当exp &gt; 0 然后ipow1(base, exp) == ipow2(base, exp)。但你回答“想打赌?阅读回报声明并做一些思考。-1”。
  • @Ben Voigt:哦该死的你是对的,我误解了他的回答,我以为他在谈论while 声明。现在我重新读了它,我明白了。不过,我已经向他道歉,因为他如此咄咄逼人。
【解决方案2】:

这是因为使用 while (exp > 1) for 将从 exp 运行到 2(它将在 exp = 2 时执行,将其递减到 1 然后结束循环)。 使用 while (exp),for 将从 exp 运行到 1(它将在 exp = 1 时执行,将其减为 0,然后结束循环)。

因此,使用 while (exp) 您有一个额外的迭代,这需要额外的时间来运行。

编辑:即使在循环之后使用 exp>1 while 进行乘法,请记住,乘法并不是循环中唯一的内容。

【讨论】:

    【解决方案3】:

    如果您不想阅读所有这些直接跳到底部,我仅通过分析代码就得出了 21% 的差异。

    不同的系统、编译器版本、由不同人/发行版构建的相同编译器版本会给出不同的指令组合,这只是您可能得到的一个示例。

    long ipow1(int base, int exp) {
        long result = 1;
    
        while (exp > 1) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
        }
    
        return result * base;
    }
    
    long ipow2(int base, int exp) {
        long result = 1;
    
        while (exp) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
        }
    
        return result;
    }
    
    0000000000000000 <ipow1>:
       0:   83 fe 01                cmp    $0x1,%esi
       3:   ba 01 00 00 00          mov    $0x1,%edx
       8:   7e 1d                   jle    27 <ipow1+0x27>
       a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
      10:   40 f6 c6 01             test   $0x1,%sil
      14:   74 07                   je     1d <ipow1+0x1d>
      16:   48 63 c7                movslq %edi,%rax
      19:   48 0f af d0             imul   %rax,%rdx
      1d:   d1 fe                   sar    %esi
      1f:   0f af ff                imul   %edi,%edi
      22:   83 fe 01                cmp    $0x1,%esi
      25:   7f e9                   jg     10 <ipow1+0x10>
      27:   48 63 c7                movslq %edi,%rax
      2a:   48 0f af c2             imul   %rdx,%rax
      2e:   c3                      retq   
      2f:   90                      nop
    
    0000000000000030 <ipow2>:
      30:   85 f6                   test   %esi,%esi
      32:   b8 01 00 00 00          mov    $0x1,%eax
      37:   75 0a                   jne    43 <ipow2+0x13>
      39:   eb 19                   jmp    54 <ipow2+0x24>
      3b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
      40:   0f af ff                imul   %edi,%edi
      43:   40 f6 c6 01             test   $0x1,%sil
      47:   74 07                   je     50 <ipow2+0x20>
      49:   48 63 d7                movslq %edi,%rdx
      4c:   48 0f af c2             imul   %rdx,%rax
      50:   d1 fe                   sar    %esi
      52:   75 ec                   jne    40 <ipow2+0x10>
      54:   f3 c3                   repz retq 
    

    隔离循环:

        while (exp > 1) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
        }
    
    
    //if exp & 1 not true jump to 1d to skip   
      10:   40 f6 c6 01             test   $0x1,%sil
      14:   74 07                   je     1d <ipow1+0x1d>
    //result *= base  
      16:   48 63 c7                movslq %edi,%rax
      19:   48 0f af d0             imul   %rax,%rdx
    //exp>>=1  
      1d:   d1 fe                   sar    %esi
    //base *= base  
      1f:   0f af ff                imul   %edi,%edi
    //while(exp>1) stayin the loop  
      22:   83 fe 01                cmp    $0x1,%esi
      25:   7f e9                   jg     10 <ipow1+0x10>
    

    将某个值与零进行比较通常会为您节省一条指令,您可以在此处查看

        while (exp) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
        }
    
    
    //base *= base  
      40:   0f af ff                imul   %edi,%edi
    //if exp & 1 not true jump to skip  
      43:   40 f6 c6 01             test   $0x1,%sil
      47:   74 07                   je     50 <ipow2+0x20>
    //result *= base  
      49:   48 63 d7                movslq %edi,%rdx
      4c:   48 0f af c2             imul   %rdx,%rax
    //exp>>=1  
      50:   d1 fe                   sar    %esi
    //no need for a compare  
      52:   75 ec                   jne    40 <ipow2+0x10>
    

    你的计时方法会产生很多错误/混乱。根据循环的节拍频率和计时器的准确性,您可以在一个中创建很多增益,在另一个中创建很多损失。这种方法通常会提供更好的准确性:

    开始时间 = ... for(rep=bignumber;rep;rep--) { //待测代码 ... } 结束时间 = ... 总计 = 结束时间 - 开始时间;

    当然,如果您在操作系统时序上运行它,无论如何都会有相当多的错误。

    您还想为计时器变量使用 volatile 变量,这有助于编译器不重新安排执行顺序。 (去过那里)。

    如果我们从基乘的角度来看:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    unsigned int mults;
    
    long ipow1(int base, int exp) {
        long result = 1;
    
        while (exp > 1) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
            mults++;
        }
    
        result *= base;
    
        return result;
    }
    
    long ipow2(int base, int exp) {
        long result = 1;
    
        while (exp) {
            if (exp & 1) result *= base;
            exp >>= 1;
            base *= base;
            mults++;
        }
    
        return result;
    }
    
    
    int main ( void )
    {
        int i;
        int j;
    
        mults = 0;
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow1(i, j); // or ipow2
                }
            }
        printf("mults %u\n",mults);
    
        mults=0;
    
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow2(i, j); // or ipow2
                }
            }
        printf("mults %u\n",mults);
    
    }
    

    mults 1045
    mults 1595
    

    ipow2() 增加 50%。实际上,不仅仅是乘数,您还要多执行 50% 的循环。

    ipow1() 在其他乘法上得到一点回报:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    unsigned int mults;
    
    long ipow1(int base, int exp) {
        long result = 1;
    
        while (exp > 1) {
            if (exp & 1) mults++;
            exp >>= 1;
            base *= base;
        }
        mults++;
    
        return result;
    }
    
    long ipow2(int base, int exp) {
        long result = 1;
    
        while (exp) {
            if (exp & 1) mults++;
            exp >>= 1;
            base *= base;
        }
    
        return result;
    }
    
    
    int main ( void )
    {
        int i;
        int j;
    
        mults = 0;
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow1(i, j); // or ipow2
                }
            }
        printf("mults %u\n",mults);
    
        mults=0;
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow2(i, j); // or ipow2
                }
            }
        printf("mults %u\n",mults);
    
    }
    

    ipow1() 执行结果*=base 与 ipow2() 不同的次数(更多)

    mults 990
    mults 935
    

    作为一个 long * int 可以使这些更昂贵。不足以弥补 ipow2() 中循环的损失。

    即使不反汇编,粗略猜测您希望编译器使用的操作/指令。考虑到处理器通常不一定是 x86,一些处理器会比其他处理器更好地运行此代码(从执行的指令数量的角度来看,不包括所有其他因素)。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    unsigned int ops;
    
    long ipow1(int base, int exp) {
        long result = 1;
        ops++; //result = immediate
        while (exp > 1) {
            ops++; // compare exp - 1
            ops++; // conditional jump
                //if (exp & 1)
            ops++; //exp&1
            ops++; //conditional jump
            if (exp & 1)
            {
                result *= base;
                ops++;
            }
            exp >>= 1;
            ops++;
            //ops+=?; //using a signed number can cost you this on some systems
            //always use unsigned unless you have a specific reason to use signed.
            //if this had been a short or char variable it might cost you even more
            //operations
            //if this needs to be signed it is what it is, just be aware of
            //the cost
            base *= base;
            ops++;
        }
        result *= base;
        ops++;
        return result;
    }
    
    long ipow2(int base, int exp) {
        long result = 1;
        ops++;
        while (exp) {
            //ops++; //cmp exp-0, often optimizes out;
            ops++; //conditional jump
            //if (exp & 1)
            ops++;
            ops++;
            if (exp & 1)
            {
                result *= base;
                ops++;
            }
            exp >>= 1;
            ops++;
            //ops+=?; //right shifting a signed number
            base *= base;
            ops++;
        }
        return result;
    }
    
    
    
    int main ( void )
    {
        int i;
        int j;
    
        ops = 0;
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow1(i, j); // or ipow2
                }
            }
        printf("ops %u\n",ops);
    
        ops=0;
            for (i = 0; i < 55; i++) {
                for (j = 0; j < 11; j++) {
                    ipow2(i, j); // or ipow2
                }
            }
        printf("ops %u\n",ops);
    
    }
    

    假设我计算了所有主要操作并且没有不公平地给出一个功能而不是另一个功能:

    ops 7865
    ops 9515
    

    使用此分析,ipow2 慢了 21%。

    我认为最大的杀手是循环次数增加了 50%。假设它依赖于数据,您可能会在基准测试中发现输入,这些输入使函数之间的差异大于或小于您所看到的 25%。

    【讨论】:

      【解决方案4】:

      你的函数不是“逻辑上相等的”。

      while (exp > 1){...}
      

      逻辑上等于

      while (exp){...}
      

      为什么这么说?

      【讨论】:

      • 您是否阅读了我的问题中的 cmets 和 bold 部分?
      • facepalm ...您阅读评论了吗!?
      【解决方案5】:

      这真的会生成相同的汇编代码吗?当我尝试时(我承认在 OpenSuse 11.4 上使用 gcc 4.5.1)我发现了细微的差别。

      ipow1.s:

      cmpl    $1, -24(%rbp)
      jg  .L4
      movl    -20(%rbp), %eax
      cltq
      imulq   -8(%rbp), %rax
      leave
      

      ipow2.s:

      cmpl    $0, -24(%rbp)
      jne .L4
      movq    -8(%rbp), %rax
      leave
      

      也许处理器的分支预测使用jg 比使用jne 更有效?一条分支指令的运行速度似乎不太可能比另一条快 25%(尤其是当cmpl 完成了大部分繁重的工作时)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-07-27
        • 1970-01-01
        • 1970-01-01
        • 2014-01-20
        • 1970-01-01
        • 2013-07-12
        相关资源
        最近更新 更多