用异或来交换变量是错误的
翻转一个字符串,例如把 “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;
}
}
受一些过时的教科书的误导,有人认为程序里少用一个变量,节省一个字节的空间,会让程序运行更快。这是不对的,至少在这里不成立:
这个技巧的意义完全在于应付变态的面试,所以知道就行,但绝对不能放在产品代码中。我也想不出问这样的面试题意义何在。
更有甚者,把其中三句:
*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,imullmovlsarl31, %eax
sarl 2,sublmovlleal(addlsublmovlmovlmovzbl(movbaddl1, %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
bool convert(char* buf, size_t bufsize, int value);
模板确实会使代码膨胀,但是这样的膨胀有时候是好事情,编译器能针对不同的常数生成快速算法。滥用 C++ 模板当然是错的,适当使用不会有问题。
转载于用异或来交换两个变量是错误的
用异或交换两个整数的陷阱
前面我们谈到了,可用通过异或运算交换两个数,而不需要任何的中间变量。 如下面:
void exchange(int &a, int &b)
{
a ^= b;
b ^= a;
a ^= b;
}
然而,这里面却存在着一个非常隐蔽的陷阱。
通常我们在对数组进行操作的时候,会交换数组中的两个元素,如exchang(&a[i], &b[j]), 这儿如果i==j了(这种情况是很可能发生的),得到的结果就并非我们所期望的。
void main()
{
int a[2] = {1, 2};
exchange(a[0], a[1]); //交换a[0]和a[1]的值
printf("1---a[0]=%d a[1]=%d\n", a[0], a[1]);
exchange(a[0], a[0]); //将a[0]与自己进行交换
printf("2---a[0]=%d a[1]=%d\n", a[0], a[1]);
}
上面那段测试代码的输出是:
1—a[0]=2 a[1]=1
2—a[0]=0 a[1]=1
很意外吧,第一次的交换正确的执行了,但是第二次调用exchange的时候却将a[0]置为了0. 仔细分析,不难发现,这正是我们在exchange里面用异或实现交换所造成的。如果输入a和b是同一个数,exchange里面代码相当于:
a ^= a;
a ^= a;
a ^= a;
成了a做了3次于自己的异或,其结果当然是0了。
既然这样,我们就不能够在任何使用交换的地方采用异或了,即使要用,也一定要在交换之前判断两个数是否已经相等了,如下:
void exchange(int &a, int &b)
{
if(a == b) return; //防止&a,&b指向同一个地址;那样结果会错误。
a ^= b;
b ^= a;
a ^= b;
}
转载于用异或交换两个整数的陷阱
今天做数组操作时,做一些交换,调用的swap()函数,用的异或来些写的,结果排查了很长时间才发现错误。。。