一个数据交换函数引发的思考
2016-08-06 20:07
197 查看
近日,在书中看到一个关于数据交换函数的源代码,发现挺有意思,具体代码如下:
根据 C 语言异或赋值操作符(^=)的计算规则和异或运算符(^)的运算法则,应按照从右到左的顺序进行计算,具体计算过程演示如下:
从计算过程可以看出,a 和 b 的值的确进行了交换,那我们通过具体程序来进行验算一下:
笔者的计算环境是:Linuxmint 17.3 + gcc 4.8.4/clang 3.5.0。先使用 gcc 进行编译,看看结果如何:
结果非常令人诧异,只有 b 的值进行了交换,而 a 的值却是 0,为什么会是 0 ?我们待会再来分析,现在我们再用 clang 进行编译,看看结果又是如何:
结果还是令人欢欣鼓舞的,那为什么会出现两种不同的结果呢?直观的感觉肯定是与编译器有关的,为了证实这一想法,我们通过反汇编之后来看看其中的差异之处:
截取其中与 swap 函数有关的段落如下:
通过以上汇编代码和简要的分析,大家发现问题了吗?很显然,第24行的计算过程出现了问题,因为此时寄存器 edx 中存放的是最初始的 a 的值,而寄存器 eax 中存放的是 b 的新值(也就是 a 的初始值),因此,计算后寄存器 edx 的值就是 0 了(即*a ^ *a)。知道了原因,那如何修正这一问题呢?显然,只需要在第 23 行和第 24 行之间插入如下代码即可:
即通过以上代码重新取得 a 的新值(即 *a ^ *b)即可。接下来,我们再看看另外一种反汇编的情况:
同样,截取其中与 swap 函数有关的段落如下:
对以上代码的分析过程可以参考前一部分,也是比较清晰易懂的。从运算过程可以看出,在进行异或运算之前,都是取出 a 或 b 的最新的值,即保证了计算过程严格按照文章开始时演算的步骤进行,也就能够得出正确的值。
由此可见,我们的猜测是正确的,原因的确与编译器有关。如果希望 swap 函数能够在不同的编译环境下正常工作,我们可以将原异或赋值表达式拆分成以下 3 个异或赋值表达式即可。
另外,在使用 gcc 编译时,如果加上优化选项 -O1/-O2/-O3/-Os(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。
void swap(int* a, int* b) { *a ^= *b ^= *a ^= *b; }
根据 C 语言异或赋值操作符(^=)的计算规则和异或运算符(^)的运算法则,应按照从右到左的顺序进行计算,具体计算过程演示如下:
*a = *a ^ *b; *b = *b ^ *a = *b ^ ( *a ^ *b ) = *a; //将式1代入 *a = *a ^ *b = ( *a ^ *b ) ^ *a = *b; //将式1和式2代入
从计算过程可以看出,a 和 b 的值的确进行了交换,那我们通过具体程序来进行验算一下:
#include <stdio.h> void swap(int* a, int* b) { *a ^= *b ^= *a ^= *b; } int main(int argc, char** argv) { int a = 13, b = 68; printf("Before exchange: a = %d, b = %d\n", a, b); swap(&a, &b); printf("After exchange: a = %d, b = %d\n", a, b); return 0; }
笔者的计算环境是:Linuxmint 17.3 + gcc 4.8.4/clang 3.5.0。先使用 gcc 进行编译,看看结果如何:
$ gcc -o swap_gcc swap.c $ ./swap_gcc Before exchange: a = 13, b = 68 After exchange: a = 0, b = 13
结果非常令人诧异,只有 b 的值进行了交换,而 a 的值却是 0,为什么会是 0 ?我们待会再来分析,现在我们再用 clang 进行编译,看看结果又是如何:
$ clang -o swap_clang swap.c $ ./swap_clang Before exchange: a = 13, b = 68 After exchange: a = 68, b = 13
结果还是令人欢欣鼓舞的,那为什么会出现两种不同的结果呢?直观的感觉肯定是与编译器有关的,为了证实这一想法,我们通过反汇编之后来看看其中的差异之处:
$ objdump -d swap_gcc
截取其中与 swap 函数有关的段落如下:
000000000040052d <swap>: 40052d: 55 push %rbp 40052e: 48 89 e5 mov %rsp,%rbp 400531: 48 89 7d f8 mov %rdi,-0x8(%rbp) //保存 a 的值 400535: 48 89 75 f0 mov %rsi,-0x10(%rbp) //保存 b 的值 400539: 48 8b 45 f8 mov -0x8(%rbp),%rax 40053d: 8b 10 mov (%rax),%edx //取 a 的值并存入寄存器 edx 40053f: 48 8b 45 f0 mov -0x10(%rbp),%rax 400543: 8b 08 mov (%rax),%ecx //取 b 的值并存入寄存器 ecx 400545: 48 8b 45 f8 mov -0x8(%rbp),%rax 400549: 8b 30 mov (%rax),%esi //取 a 的值并存入寄存器 esi 40054b: 48 8b 45 f0 mov -0x10(%rbp),%rax 40054f: 8b 00 mov (%rax),%eax //取 b 的值并存入寄存器 eax 400551: 31 c6 xor %eax,%esi //将寄存器 eax 与 esi 中的值进行异或运算后存入寄存器 esi 中,即 *a = *a ^ *b 400553: 48 8b 45 f8 mov -0x8(%rbp),%rax 400557: 89 30 mov %esi,(%rax) //将寄存器 esi 中的值写入原先存放 a 的值的地址处,至此,完成了最后一个异或赋值表达式 *a ^= *b 的计算 400559: 48 8b 45 f8 mov -0x8(%rbp),%rax 40055d: 8b 00 mov (%rax),%eax //取 a 的新值(即 *a ^ *b)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值被覆盖了 40055f: 31 c1 xor %eax,%ecx //将寄存器 eax 与 ecx 中的值进行异或运算后存入寄存器 ecx 中,即 *b = *b ^ (*a ^ *b) = *a 400561: 48 8b 45 f0 mov -0x10(%rbp),%rax 400565: 89 08 mov %ecx,(%rax) //将寄存器 ecx 中的值写入原先存放 b 的值的地址处,至此,完成了中间那个异或赋值表达式 *b ^= *a 的计算 400567: 48 8b 45 f0 mov -0x10(%rbp),%rax 40056b: 8b 00 mov (%rax),%eax //取 b 的新值(即 *a)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值再次被覆盖了 40056d: 31 c2 xor %eax,%edx //将寄存器 eax 与 edx 中的值进行异或运算后存入寄存器 edx 中,发现问题了吗??? 40056f: 48 8b 45 f8 mov -0x8(%rbp),%rax 400573: 89 10 mov %edx,(%rax) //将寄存器 edx 中的值写入原先存放 a 的值的地址处 400575: 5d pop %rbp 400576: c3 retq
通过以上汇编代码和简要的分析,大家发现问题了吗?很显然,第24行的计算过程出现了问题,因为此时寄存器 edx 中存放的是最初始的 a 的值,而寄存器 eax 中存放的是 b 的新值(也就是 a 的初始值),因此,计算后寄存器 edx 的值就是 0 了(即*a ^ *a)。知道了原因,那如何修正这一问题呢?显然,只需要在第 23 行和第 24 行之间插入如下代码即可:
mov -0x8(%rbp),%rax mov (%rax),%edx
即通过以上代码重新取得 a 的新值(即 *a ^ *b)即可。接下来,我们再看看另外一种反汇编的情况:
$ objdmup -d swap_clang
同样,截取其中与 swap 函数有关的段落如下:
00000000004004e0 <swap>: 4004e0: 55 push %rbp 4004e1: 48 89 e5 mov %rsp,%rbp 4004e4: 48 89 7d f8 mov %rdi,-0x8(%rbp) 4004e8: 48 89 75 f0 mov %rsi,-0x10(%rbp) 4004ec: 48 8b 75 f0 mov -0x10(%rbp),%rsi 4004f0: 8b 06 mov (%rsi),%eax 4004f2: 48 8b 75 f8 mov -0x8(%rbp),%rsi 4004f6: 8b 0e mov (%rsi),%ecx 4004f8: 31 c1 xor %eax,%ecx 4004fa: 89 0e mov %ecx,(%rsi) 4004fc: 48 8b 75 f0 mov -0x10(%rbp),%rsi 400500: 8b 06 mov (%rsi),%eax 400502: 31 c8 xor %ecx,%eax 400504: 89 06 mov %eax,(%rsi) 400506: 48 8b 75 f8 mov -0x8(%rbp),%rsi 40050a: 8b 0e mov (%rsi),%ecx 40050c: 31 c1 xor %eax,%ecx 40050e: 89 0e mov %ecx,(%rsi) 400510: 5d pop %rbp 400511: c3 retq
对以上代码的分析过程可以参考前一部分,也是比较清晰易懂的。从运算过程可以看出,在进行异或运算之前,都是取出 a 或 b 的最新的值,即保证了计算过程严格按照文章开始时演算的步骤进行,也就能够得出正确的值。
由此可见,我们的猜测是正确的,原因的确与编译器有关。如果希望 swap 函数能够在不同的编译环境下正常工作,我们可以将原异或赋值表达式拆分成以下 3 个异或赋值表达式即可。
*a ^= *b; *b ^= *a; *a ^= *b;
另外,在使用 gcc 编译时,如果加上优化选项 -O1/-O2/-O3/-Os(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。
相关文章推荐
- 【数据库】_由2000W多条开房数据引发的思考、实践----给在校生的一个真实【练耙场】,同学们,来开始一次伟大的尝试吧。
- 由一个函数引发的改进思考
- 一个函数命名所引发的思考
- 一个题目引发的闭包、函数声明以及作用域的简单思考
- 一个“强制转换”的例子引发的思考
- 对话框的数据交换,对里面的函数实现比较详细
- 数据表行列互换的一个思考
- 数据表行列互换的一个思考
- 关于栈的弹出函数设计是否应该有两个返回值,一个表示数据,一个表示是否栈为空
- Linux那些事儿之我是UHCI(14)一个函数引发的故事(五)
- VC++一个函数引起的思考
- 一个“笑话”引发的思考
- Linux那些事儿之我是UHCI(12)一个函数引发的故事(三)
- Linux那些事儿之我是UHCI(13)一个函数引发的故事(四)
- 用函数获得一个类型下所有的数据,并用逗号隔开,显示出来
- BGP的一个经典试验引发的思考
- 一个游戏引发的思考(概率问题)
- [BizTalk][Adapter][部署]BTS学习笔记1:建立一个简单的Biztalk数据交换项目(一)
- 由对BLOB数据的操作所引发的一系列思考
- Linux那些事儿之我是UHCI(11)一个函数引发的故事(二)