与更易读的方法相比,c ++中的Bitwise XOR的效率

时间:2010-02-22 01:29:43

标签: c++ performance bitwise-operators

我最近为我正在研究的一个研究项目编写了一些代码,效率非常重要。我一直在考虑使用一些常规方法来处理并使用按位XOR代替。我想知道的是,如果有差异(如果我执行此操作说数百万次),或者如果我使用03 in g ++后它是相同的话。

想到的两个例子:

我有一个实例(我正在处理纯粹的正整数)如果n是奇数,我需要将n改为n-1,如果n是偶数,则需要将n改为n(n + 1)。我想我有几个选择:

if(n%2) // or (n%2==0) and flip the order
    n=n-1
else
    n=n+1

n=n+2*n%2-1; //This of course was silly, but was the only non-bitwise 1 line I could come up with

最后:

n=n^1;

所有方法显然都做同样的事情,但我的感觉是第三种方法效率最高。

下一个例子是更一般的说明。假设我正在比较两个正整数,其中一个会比其他整数表现更好。或者差异真的不明显,即使我执行了几百万次这个操作:

if(n_1==n_2)
if(! (n_1 ^ n_2) )
if( n_1 ^ n_2) else \do work here

编译器是否会在所有这些实例中执行相同的操作?我只是好奇是否有一个实例我应该使用按位操作而不相信编译器为我做的工作。

修正:正确的问题陈述。

8 个答案:

答案 0 :(得分:9)

这很容易检查,只需启动你的反汇编程序。看看:

F.C:

unsigned int f1(unsigned int n)
{
  n ^= 1;  
  return n;
}

unsigned int f2(unsigned int n)
{
  if (n % 2)
    n=n-1;
  else
    n=n+1;

  return n;
}

构建和反汇编:

$ cc -O3 -c f.c 
$ otool -tV f.o 
f.o:
(__TEXT,__text) section
_f1:
00  pushq   %rbp
01  movq    %rsp,%rbp
04  xorl    $0x01,%edi
07  movl    %edi,%eax
09  leave
0a  ret
0b  nopl    _f1(%rax,%rax)
_f2:
10  pushq   %rbp
11  movq    %rsp,%rbp
14  leal    0xff(%rdi),%eax
17  leal    0x01(%rdi),%edx
1a  andl    $0x01,%edi
1d  cmovel  %edx,%eax
20  leave
21  ret

f1()看起来有点短,无论现实中的重要性是否达到某些基准。

答案 1 :(得分:4)

  

如果n为偶数,我需要将n更改为n-1,如果n为奇数,则需要将n更改为(n + 1)。

在这种情况下,无论效率如何,n = n ^ 1 错误

对于第二种情况,==与其他任何一种情况一样有效(如果不是更有效)。


通常,在优化方面,您应该自己进行基准测试。如果潜在的优化不值得基准测试,那就不值得做。

答案 2 :(得分:4)

我有点不同意这里的大多数答案,这就是为什么我仍然看到自己回答2010年的问题: - )

XOR实际上是CPU可以做的最快的操作,而好的部分是所有CPU都支持它。原因很简单:可以使用4个NAND门或5个NOR门创建XOR门 - 这意味着可以使用硅结构轻松创建。不出所料,我所知道的所有CPU都可以在1个时钟周期内执行您的XOR运算(甚至更少)。

如果你需要对一个数组中的多个项目进行XOR,现代的x64 CPU也可以像f.ex一样支持多个项目的XOR。英特尔的SIMD说明。

您选择使用if-then-else的替代解决方案。没错,大多数编译器能够解决这个简单的事情......但为什么要冒任何机会,结果又是什么呢?

编译器没有弄清楚它的结果是分支预测错误。单个分支预测失败将很容易占用17个时钟周期。如果你看一下处理器指令的执行速度,你会发现分支对你的性能非常不利,特别是在处理随机数据时。

请注意,这也意味着如果您错误地构建测试,数据将会影响您的性能测量。

总而言之:首先想想,然后是程序,然后是个人资料 - 而不是相反。并使用异或。

答案 3 :(得分:2)

关于确切知道的唯一方法是测试。我必须同意,需要一个相当聪明的编译器来生成以下的输出效率:

if(n%2) // or (n%2==0) and flip the order
    n=n-1
else
    n=n+1

尽可能n ^= 1;,但我最近没有检查过类似的内容,无论如何都不能确定。

关于你的第二个问题,我怀疑它是否有任何区别 - 对于任何这些方法,平等比较都会快速结束。如果你想要速度,最重要的是避免让一个分支参与其中 - 例如类似的东西:

if (a == b)
    c += d;

可以写成:c += d * (a==b);。看一下汇编语言,第二个看起来通常会有些混乱(从标志到正常寄存器的比较结果很丑陋)但通常避免任何分支都会表现得更好。

编辑:至少我方便的编译器(gcc和MSVC)不会为cmov生成if,但它们会为{{sete生成* (a==b) 1}}。我将代码扩展为可测试的东西。

Edit2:由于Potatoswatter提出了另一种使用逐位而不是乘法的可能性,我决定与其他人一起测试。这是添加的代码:

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

int addif1(int a, int b, int c, int d) { 
    if (a==b) 
        c+=d;
    return c;
}

int addif2(int a, int b, int c, int d) {
    return c += d * (a == b);
}

int addif3(int a, int b, int c, int d) {
    return c += d & -(a == b);
}

int main() {
    const int iterations = 50000;
    int x = rand();
    unsigned tot1 = 0;
    unsigned tot2 = 0;
    unsigned tot3 = 0;

    clock_t start1 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot1 +=addif1(i, j, i, x);
    }
    clock_t stop1 = clock();

    clock_t start2 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot2 +=addif2(i, j, i, x);
    }
    clock_t stop2 = clock();

    clock_t start3 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot3 +=addif3(i, j, i, x);
    }
    clock_t stop3 = clock();

    std::cout << "Ignore: " << tot1 << "\n";
    std::cout << "Ignore: " << tot2 << "\n";
    std::cout << "Ignore: " << tot3 << "\n";

    std::cout << "addif1: " << stop1-start1 << "\n";
    std::cout << "addif2: " << stop2-start2 << "\n";
    std::cout << "addif3: " << stop3-start3 << "\n";    
    return 0;
}

现在真正有趣的部分:第三个版本的结果相当有趣。对于MS VC ++,我们大致可以得到大多数人可能期望的内容:

Ignore: 2682925904
Ignore: 2682925904
Ignore: 2682925904
addif1: 4814
addif2: 3504
addif3: 3021

使用&代替*,可以提供明确的改进 - 几乎*相比if更多的改进}。使用gcc,结果有点不同:

Ignore: 2680875904
Ignore: 2680875904
Ignore: 2680875904
addif1: 2901
addif2: 2886
addif3: 7675

在这种情况下,使用if的代码更接近使用*的代码速度,但使用&的代码比任何一个代码都慢 - a 很多更慢!如果有人关心,我发现这令人惊讶,我用不同的标志重新编译了几次,每次重新运行几次,依此类推,结果完全一致 - 使用{{1}的代码一直相当慢。

使用gcc编译的代码的第三个版本的糟糕结果让我们回到我所说的开始[并结束此编辑]:

正如我所说的那样,“唯一可以确定的方法就是测试” - 但至少在这个有限的测试中,乘法始终优于&。可能有一些组合的编译器,编译器标志,CPU,数据模式,迭代计数等,有利于if乘法 - 毫无疑问差异很小足以使另一个方向的测试完全可信。尽管如此,我相信这是一项值得了解的技术;对于主流编译器和CPU来说,它看起来相当有效(尽管它对于MSVC肯定更多比使用gcc更有帮助)。

[恢复编辑2:] gcc使用if的结果证明了1)微优化可以是编译器特定的程度,以及2)与预期有多少不同的现实结果

答案 4 :(得分:1)

n^=1if ( n%2 ) --n; else ++n;快吗?是。我不希望编译器优化它。由于按位操作非常简洁,因此熟悉XOR可能是值得的,也许可以对该行代码添加注释。

如果它对程序的功能至关重要,它也可以被认为是一个可移植性问题:如果你在编译器上进行测试并且速度很快,那么在尝试使用另一个编译器时你可能会感到惊讶。通常这不是代数优化的问题。

x^yx==y快吗?不。以迂回方式做事通常都不好。

答案 5 :(得分:0)

一个好的编译器会优化n%2,但你总是可以检查生成的程序集。如果您看到分歧,请自行开始优化,因为鸿沟的速度和它一样慢。

答案 6 :(得分:0)

你应该相信你的编译器。 gcc / ++是多年开发的产物,它能够做任何你可能正在考虑做的优化。而且,如果你开始玩游戏,你可能会篡改它为优化你的代码所做的努力。

答案 7 :(得分:0)

n ^= 1n1==n2可能是你能做到的最好的,但实际上,如果你追求的是最高效率,那么就不要去寻找代码那样的小代码。

Here's an example of how to really tune for performance.

在采样证明它们应该集中在你应该关注的地方之前,不要指望低水平优化会有很大帮助。