用异或运算交换连个变量

在论坛的帖子里看到一个面试题,交换两个整数。以往知道的方法有两种,一是使用临时变量temp,二是两个整数相加减的算法。在帖子里发现了第三种算法,异或运算:

a=a^b;

b=b^a;

a=b^a;

一时好奇,研究了一下,得出本文。

异位运算交换两个整数的算法原理。

交换两个整数常规的实现就是使用临时变量,异位运算交换两个整数不需要临时变量,其实是把临时变量与其中的一个整数结合起来了,也就是说把其中的一个整数当做临时变量来用,这一点与两数相加减的算法是一到致的。下面讲讲原理。

异或运算有两个特性:

1、一个数异或本身恒等于0,如5^5恒等于0;

2、一个数异或0恒等于本身,如5^0恒等于5。

交换两个整数a和b,无非是a=b和b=a这两个操作,当然,你不能直接这么做。该怎么变呢?

算式一:a=b^(a^a)=a^(a^b);

算式二:b=a^(b^b)^(a^a)=a^(a^b)^(a^b);

注意上面算式二中的a还是原来的a,不要认为是改变后的a。

为什么右边的式子都留个a,没为什么,我就是想把b做为临时变量来用,此处要注意,既然做为临时变量用那么b就是最后才计算出来的数。接下来认真的分析下上面的两个算式。得出以下java语句:

把a^b做为临时变量值赋给b(临时变量),得

b=a^b;

计算出a:

a=a^b;注意这时的b可就是上面的式子已改变过的b了。

计算出b:

b=a^b;注意仔细观察上面的式二。

至此完成了两个整数的交换。

 

用异或来交换变量是错误的

 

陈硕 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

 

翻转一个字符串,例如把 "12345" 变成 "54321",这是一个最简单的不过的编码任务,即便是 C 语言初学者的也能毫不费力地写出类似如下的代码:

 

// 版本一,用中间变量交换两个数,好代码

void reverse_by_swap(char* str, int n)

{

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    char tmp = *begin;

    *begin = *end;

    *end = tmp;

    ++begin;

    --end;

  }

}

 

这个代码清晰,直白,没有任何高深的技巧。

 

不知从什么时候开始,有人发明了不使用临时变量交换两个数的办法,用“不用临时变量交换两个数”在 google 上能搜到很多文章。下面是一个典型的实现:

 

// 版本二,用异或运算交换两个数,烂代码

void reverse_by_xor(char* str, int n)

{

  // WARNING: BAD code

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

    ++begin;

    --end;

  }

}

 

受一些过时的教科书的误导,有人认为程序里少用一个变量,节省一个字节的空间,会让程序运行更快。这是不对的,至少在这里不成立:

 

1.       这个所谓的“技巧”在现代的机器上只会更慢(我甚至怀疑它从来就不可能比原始办法快)。原始办法是两次内存读和写,这个"技巧"是六读三写加三次异或(或许编译器可以优化成两读三写加三次异或)。

2.       同样也不能节省内存,因为中间变量 tmp 通常会是寄存器(稍后有汇编代码供分析)。就算它在函数的局部堆栈(stack)上,反正栈已经开在那儿了,也没有进一步的函数调用,根本节约不了一丁点内存。

3.       相反,由于计算步骤较多,会使用更多的指令,编译后的机器码长度会增加。(这不是什么大问题,短的代码不一定快,后面有另外一个例子。)

 

这个技巧的意义完全在于应付变态的面试,所以知道就行,但绝对不能放在产品代码中。我也想不出问这样的面试题意义何在。

 

更有甚者,把其中三句:

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

写成一句:

    *begin ^= *end ^= *begin ^= *end; // WRONG

这更是大有问题,会导致未定义的行为(undefined behavior)。C 语言的一条语句中,一个变量的值只允许改变一次,像 x = x++ 这种代码都是未定义行为。在C语言里没有哪条规则保证这两种写法是等价的。
(致语言律师:我知道,黑话叫序列点,一个语句可能不止一个序列点,请允许我在这里使用不精确的表述。)

 

这不是一个值得炫耀的技巧,只会丑化劣化代码。

 

翻转字符串这个问题在 C++ 有更简单的解法——调用标准库里的 std::reverse。有人担心调用函数会有开销,这种担心是多余的,现在的编译器会把std::reverse() 这种简单函数自动内联展开,生成出来的优化汇编代码和“版本一”一样快。

 

// 版本三,用 std::reverse 颠倒一个区间,优质代码

void reverse_by_std(char* str, int n)

{

  std::reverse(str, str + n);

}

 

 

======== 第二部分,编译器会分别生成什么代码 ========

 

注意:查看编译器生成的汇编代码固然是了解程序行为的一个重要手段,但是千万不要认为看到的东西是永恒真理,它只是一时一地的真相。将来换了硬件平台或编译器,情况可能会变化。重要的不是为什么版本一比版本二快,而是如何发现这个事实。不要“猜 guess”,要“测 benchmark”。

 

g++ 版本 4.4.1,编译参数-O2 -march=core2,x86 Linux 系统。

版本一编译的汇编代码是:

 

.L3:

        movzbl  (%edx), %ecx

        movzbl  (%eax), %ebx

        movb    %bl, (%edx)

        movb    %cl, (%eax)

        incl    %edx

        decl    %eax

        cmpl    %eax, %edx

        jb      .L3

 

我用 C 语言翻译一下:

register char bl, cl;

register char* eax;

register char* edx;

 

L3:

cl = *edx; // 读

bl = *eax; // 读

*edx = bl; // 写

*eax = cl; // 写

++edx;

--eax;

if (edx < eax) goto L3;

 

一共两读两写,临时变量没有使用内存,都在寄存器里完成。考虑指令级并行和cache的话,中间六条语句估计能在3、4个周期执行完。

 

版本二

 

.L9:

        movzbl  (%edx), %ecx

        xorb    (%eax), %cl

        movb    %cl, (%eax)

        xorb    (%edx), %cl

        movb    %cl, (%edx)

        decl    %edx

        xorb    %cl, (%eax)

        incl    %eax

        cmpl    %edx, %eax

        jb      .L9

 

C 语言翻译:

// 声明与前面一样

cl = *edx;   // 读

cl ^= *eax;  // 读,异或

*eax = cl;   // 写

cl ^= *edx;  // 读,异或

*edx = cl;   // 写

--edx;

*eax ^= cl;  // 读、写,异或

++eax;

if (eax < edx) goto L9;

 

一共六读三写三次异或,多了两条指令。指令多不一定就慢,但是这里异或版实测比临时变量版要慢许多,因为它每条指令都用到了前面一条指令的计算结果,没法并行执行。

 

版本三,生成的代码与版本一一样快。

 

.L21:

        movzbl  (%eax), %ecx

        movzbl  (%edx), %ebx

        movb    %bl, (%eax)

        movb    %cl, (%edx)

        incl    %eax

.L23:

        decl    %edx

        cmpl    %edx, %eax

        jb      .L21

 

 

这告诉我们,不要想当然地优化,也不要低估编译器的能力。关于现在的编译器有多聪明,这里有一个不错的介绍 http://www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf

 

 

Bjarne Stroustrup 说过, I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well. 中文据韩磊的翻译《代码整洁之道》 http://www.china-pub.com/196266 (陈硕对文字有修改,出错责任在我):我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;以某种全局策略一以贯之地处理全部出错情况;性能调校至接近最优,省得引诱别人实施无原则的优化(unprincipled optimizations),搞出一团乱麻。整洁的代码只做好一件事。

 

这恐怕就是Bjarne提及的没有原则的优化,甚至根本连优化都不是。代码的清晰性是首要的。

 

 

======== 第三部分,为什么短的代码不一定快 ========

 

我前两天的一篇博客谈到负整数的除法运算 http://blog.csdn.net/Solstice/archive/2010/01/06/5139302.aspx ,其中引用了一段把整数转为字符串的代码。函数反复计算一个整数除以10的商和余数。我原以为编译器会用一条DIV除法指令来算,实际生成的代码让我大吃一惊:

 

.L2:

        movl    $1717986919, %eax

        imull   %ebx

        movl    %ebx, %eax

        sarl    $31, %eax

        sarl    $2, %edx

        subl    %eax, %edx

        movl    %edx, %eax

        leal    (%edx,%edx,4), %edx

        addl    %edx, %edx

        subl    %edx, %ebx

        movl    %ebx, %edx

        movl    %eax, %ebx

        movzbl  (%edi,%edx), %eax

        movb    %al, (%esi)

        addl    $1, %esi

        testl   %ebx, %ebx

        jne     .L2

 

一条 DIV 指令被替换成了十来条指令,编译器不是傻子,必然有原因。这里我不详细解释到底是怎么算的,基本思路是把除法转换为乘法,用倒数来算。其中出现了一个魔数 1717986919,转换成16进制是 0x66666667,等于 (2**33+3)/5。

 

现代处理器上乘法运算和加减法一样快,比除法快一个数量级左右,编译器生成这样的代码是有理由的。10多年前出版的神作《程序设计实践》上介绍过如何做 micro benchmarking,方法和结果都值得一读,当然里边的数据恐怕有点过时了。

 

有本奇书《Hacker's Delight》,国内译作《高效程序的奥秘》 http://www.china-pub.com/18801 ,展示了大量这种速算技巧,第10章专门讲整数常量的除法。我不会把书中如天书般的技巧应用到产品代码中,但是我相信现代编译器的作者是知道这些技巧的,他们会合理地使用这些技巧来提高生成代码的质量。现在已经不是那个懂点汇编就能打败编译器的时代了。有一篇文章《The “C is Efficient” Language Fallacy》http://scienceblogs.com/goodmath/2006/11/the_c_is_efficient_language_fa.php 的观点我非常赞同:

Making real applications run really fast is something that's done with the help of a compiler. Modern architectures have reached the point where people can't code effectively in assembler anymore - switching the order of two independent instructions can have a dramatic impact on performance in a modern machine, and the constraints that you need to optimize for are just more complicated than people can generally deal with.

So for modern systems, writing an efficient program is sort of a partnership. The human needs to careful choose algorithms - the machine can't possibly do that. And the machine needs to carefully compute instruction ordering, pipeline constraints, memory fetch delays, etc. The two together can build really fast systems. But the two parts aren't independent: the human needs to express the algorithm in a way that allows the compiler to understand it well enough to be able to really optimize it.

 

最后,说几句C++模板。假如要编写一个任意进制的转换程序。C 语言的函数声明是:

bool convert(char* buf, size_t bufsize, int value, int radix);

 

既然进制是编译期常量,C++ 可以用带非类型模板参数的函数模板来实现,函数里边的代码与 C 相同。

template<int radix>

bool convert(char* buf, size_t bufsize, int value);

 

模板确实会使代码膨胀,但是这样的膨胀有时候是好事情,编译器能针对不同的常数生成快速算法。滥用 C++ 模板当然是错的,适当使用不会有问题。

 

用异或来交换变量是错误的

 

陈硕 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

 

翻转一个字符串,例如把 "12345" 变成 "54321",这是一个最简单的不过的编码任务,即便是 C 语言初学者的也能毫不费力地写出类似如下的代码:

 

// 版本一,用中间变量交换两个数,好代码

void reverse_by_swap(char* str, int n)

{

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    char tmp = *begin;

    *begin = *end;

    *end = tmp;

    ++begin;

    --end;

  }

}

 

这个代码清晰,直白,没有任何高深的技巧。

 

不知从什么时候开始,有人发明了不使用临时变量交换两个数的办法,用“不用临时变量交换两个数”在 google 上能搜到很多文章。下面是一个典型的实现:

 

// 版本二,用异或运算交换两个数,烂代码

void reverse_by_xor(char* str, int n)

{

  // WARNING: BAD code

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

    ++begin;

    --end;

  }

}

 

受一些过时的教科书的误导,有人认为程序里少用一个变量,节省一个字节的空间,会让程序运行更快。这是不对的,至少在这里不成立:

 

1.       这个所谓的“技巧”在现代的机器上只会更慢(我甚至怀疑它从来就不可能比原始办法快)。原始办法是两次内存读和写,这个"技巧"是六读三写加三次异或(或许编译器可以优化成两读三写加三次异或)。

2.       同样也不能节省内存,因为中间变量 tmp 通常会是寄存器(稍后有汇编代码供分析)。就算它在函数的局部堆栈(stack)上,反正栈已经开在那儿了,也没有进一步的函数调用,根本节约不了一丁点内存。

3.       相反,由于计算步骤较多,会使用更多的指令,编译后的机器码长度会增加。(这不是什么大问题,短的代码不一定快,后面有另外一个例子。)

 

这个技巧的意义完全在于应付变态的面试,所以知道就行,但绝对不能放在产品代码中。我也想不出问这样的面试题意义何在。

 

更有甚者,把其中三句:

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

写成一句:

    *begin ^= *end ^= *begin ^= *end; // WRONG

这更是大有问题,会导致未定义的行为(undefined behavior)。C 语言的一条语句中,一个变量的值只允许改变一次,像 x = x++ 这种代码都是未定义行为。在C语言里没有哪条规则保证这两种写法是等价的。
(致语言律师:我知道,黑话叫序列点,一个语句可能不止一个序列点,请允许我在这里使用不精确的表述。)

 

这不是一个值得炫耀的技巧,只会丑化劣化代码。

 

翻转字符串这个问题在 C++ 有更简单的解法——调用标准库里的 std::reverse。有人担心调用函数会有开销,这种担心是多余的,现在的编译器会把std::reverse() 这种简单函数自动内联展开,生成出来的优化汇编代码和“版本一”一样快。

 

// 版本三,用 std::reverse 颠倒一个区间,优质代码

void reverse_by_std(char* str, int n)

{

  std::reverse(str, str + n);

}

 

 

======== 第二部分,编译器会分别生成什么代码 ========

 

注意:查看编译器生成的汇编代码固然是了解程序行为的一个重要手段,但是千万不要认为看到的东西是永恒真理,它只是一时一地的真相。将来换了硬件平台或编译器,情况可能会变化。重要的不是为什么版本一比版本二快,而是如何发现这个事实。不要“猜 guess”,要“测 benchmark”。

 

g++ 版本 4.4.1,编译参数-O2 -march=core2,x86 Linux 系统。

版本一编译的汇编代码是:

 

.L3:

        movzbl  (%edx), %ecx

        movzbl  (%eax), %ebx

        movb    %bl, (%edx)

        movb    %cl, (%eax)

        incl    %edx

        decl    %eax

        cmpl    %eax, %edx

        jb      .L3

 

我用 C 语言翻译一下:

register char bl, cl;

register char* eax;

register char* edx;

 

L3:

cl = *edx; // 读

bl = *eax; // 读

*edx = bl; // 写

*eax = cl; // 写

++edx;

--eax;

if (edx < eax) goto L3;

 

一共两读两写,临时变量没有使用内存,都在寄存器里完成。考虑指令级并行和cache的话,中间六条语句估计能在3、4个周期执行完。

 

版本二

 

.L9:

        movzbl  (%edx), %ecx

        xorb    (%eax), %cl

        movb    %cl, (%eax)

        xorb    (%edx), %cl

        movb    %cl, (%edx)

        decl    %edx

        xorb    %cl, (%eax)

        incl    %eax

        cmpl    %edx, %eax

        jb      .L9

 

C 语言翻译:

// 声明与前面一样

cl = *edx;   // 读

cl ^= *eax;  // 读,异或

*eax = cl;   // 写

cl ^= *edx;  // 读,异或

*edx = cl;   // 写

--edx;

*eax ^= cl;  // 读、写,异或

++eax;

if (eax < edx) goto L9;

 

一共六读三写三次异或,多了两条指令。指令多不一定就慢,但是这里异或版实测比临时变量版要慢许多,因为它每条指令都用到了前面一条指令的计算结果,没法并行执行。

 

版本三,生成的代码与版本一一样快。

 

.L21:

        movzbl  (%eax), %ecx

        movzbl  (%edx), %ebx

        movb    %bl, (%eax)

        movb    %cl, (%edx)

        incl    %eax

.L23:

        decl    %edx

        cmpl    %edx, %eax

        jb      .L21

 

 

这告诉我们,不要想当然地优化,也不要低估编译器的能力。关于现在的编译器有多聪明,这里有一个不错的介绍 http://www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf

 

 

Bjarne Stroustrup 说过, I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well. 中文据韩磊的翻译《代码整洁之道》 http://www.china-pub.com/196266 (陈硕对文字有修改,出错责任在我):我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;以某种全局策略一以贯之地处理全部出错情况;性能调校至接近最优,省得引诱别人实施无原则的优化(unprincipled optimizations),搞出一团乱麻。整洁的代码只做好一件事。

 

这恐怕就是Bjarne提及的没有原则的优化,甚至根本连优化都不是。代码的清晰性是首要的。

 

 

======== 第三部分,为什么短的代码不一定快 ========

 

我前两天的一篇博客谈到负整数的除法运算 http://blog.csdn.net/Solstice/archive/2010/01/06/5139302.aspx ,其中引用了一段把整数转为字符串的代码。函数反复计算一个整数除以10的商和余数。我原以为编译器会用一条DIV除法指令来算,实际生成的代码让我大吃一惊:

 

.L2:

        movl    $1717986919, %eax

        imull   %ebx

        movl    %ebx, %eax

        sarl    $31, %eax

        sarl    $2, %edx

        subl    %eax, %edx

        movl    %edx, %eax

        leal    (%edx,%edx,4), %edx

        addl    %edx, %edx

        subl    %edx, %ebx

        movl    %ebx, %edx

        movl    %eax, %ebx

        movzbl  (%edi,%edx), %eax

        movb    %al, (%esi)

        addl    $1, %esi

        testl   %ebx, %ebx

        jne     .L2

 

一条 DIV 指令被替换成了十来条指令,编译器不是傻子,必然有原因。这里我不详细解释到底是怎么算的,基本思路是把除法转换为乘法,用倒数来算。其中出现了一个魔数 1717986919,转换成16进制是 0x66666667,等于 (2**33+3)/5。

 

现代处理器上乘法运算和加减法一样快,比除法快一个数量级左右,编译器生成这样的代码是有理由的。10多年前出版的神作《程序设计实践》上介绍过如何做 micro benchmarking,方法和结果都值得一读,当然里边的数据恐怕有点过时了。

 

有本奇书《Hacker's Delight》,国内译作《高效程序的奥秘》 http://www.china-pub.com/18801 ,展示了大量这种速算技巧,第10章专门讲整数常量的除法。我不会把书中如天书般的技巧应用到产品代码中,但是我相信现代编译器的作者是知道这些技巧的,他们会合理地使用这些技巧来提高生成代码的质量。现在已经不是那个懂点汇编就能打败编译器的时代了。有一篇文章《The “C is Efficient” Language Fallacy》http://scienceblogs.com/goodmath/2006/11/the_c_is_efficient_language_fa.php 的观点我非常赞同:

Making real applications run really fast is something that's done with the help of a compiler. Modern architectures have reached the point where people can't code effectively in assembler anymore - switching the order of two independent instructions can have a dramatic impact on performance in a modern machine, and the constraints that you need to optimize for are just more complicated than people can generally deal with.

So for modern systems, writing an efficient program is sort of a partnership. The human needs to careful choose algorithms - the machine can't possibly do that. And the machine needs to carefully compute instruction ordering, pipeline constraints, memory fetch delays, etc. The two together can build really fast systems. But the two parts aren't independent: the human needs to express the algorithm in a way that allows the compiler to understand it well enough to be able to really optimize it.

 

最后,说几句C++模板。假如要编写一个任意进制的转换程序。C 语言的函数声明是:

bool convert(char* buf, size_t bufsize, int value, int radix);

 

既然进制是编译期常量,C++ 可以用带非类型模板参数的函数模板来实现,函数里边的代码与 C 相同。

template<int radix>

bool convert(char* buf, size_t bufsize, int value);

 

模板确实会使代码膨胀,但是这样的膨胀有时候是好事情,编译器能针对不同的常数生成快速算法。滥用 C++ 模板当然是错的,适当使用不会有问题。

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值