设为首页 收藏本站
查看: 942|回复: 0

[经验分享] 一个数据交换函数引发的思考

[复制链接]

尚未签到

发表于 2017-7-5 09:07:05 | 显示全部楼层 |阅读模式
  近日,在书中看到一个关于数据交换函数的源代码,发现挺有意思,具体代码如下:



1 void swap(int* a, int* b)
2 {
3     *a ^= *b ^= *a ^= *b;
4 }
  根据 C 语言异或赋值操作符(^=)的计算规则和异或运算符(^)的运算法则,应按照从右到左的顺序进行计算,具体计算过程演示如下:



1 *a = *a ^ *b;
2 *b = *b  ^ *a = *b ^ ( *a ^ *b ) = *a;        //将式1代入
3 *a = *a ^ *b = ( *a ^ *b ) ^ *a = *b;         //将式1和式2代入
  从计算过程可以看出,a 和 b 的值的确进行了交换,那我们通过具体程序来进行验算一下:



1 #include <stdio.h>
2
3 void swap(int* a, int* b)
4 {
5     *a ^= *b ^= *a ^= *b;
6 }
7
8 int main(int argc, char** argv)
9 {
10     int a = 13, b = 68;
11
12     printf("Before exchange: a = %d, b = %d\n", a, b);
13     swap(&a, &b);
14     printf("After exchange: a = %d, b = %d\n", a, b);
15
16     return 0;
17 }
  笔者的计算环境是: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 函数有关的段落如下:



1 000000000040052d <swap>:
2   40052d:    55                          push   %rbp
3   40052e:    48 89 e5              mov    %rsp,%rbp
4   400531:    48 89 7d f8         mov    %rdi,-0x8(%rbp)                            //保存 a 的值
5   400535:    48 89 75 f0         mov    %rsi,-0x10(%rbp)                          //保存 b 的值
6   400539:    48 8b 45 f8         mov    -0x8(%rbp),%rax
7   40053d:    8b 10                    mov    (%rax),%edx                                   //取 a 的值并存入寄存器 edx
8   40053f:    48 8b 45 f0          mov    -0x10(%rbp),%rax
9   400543:    8b 08                    mov    (%rax),%ecx                                    //取 b 的值并存入寄存器 ecx
10   400545:    48 8b 45 f8        mov    -0x8(%rbp),%rax
11   400549:    8b 30                   mov    (%rax),%esi                                     //取 a 的值并存入寄存器 esi
12   40054b:    48 8b 45 f0        mov    -0x10(%rbp),%rax
13   40054f:    8b 00                    mov    (%rax),%eax                                    //取 b 的值并存入寄存器 eax
14   400551:    31 c6                    xor       %eax,%esi                                      //将寄存器 eax 与 esi 中的值进行异或运算后存入寄存器 esi 中,即 *a = *a ^ *b
15   400553:    48 8b 45 f8        mov    -0x8(%rbp),%rax
16   400557:    89 30                    mov    %esi,(%rax)                                    //将寄存器 esi 中的值写入原先存放 a 的值的地址处,至此,完成了最后一个异或赋值表达式 *a ^= *b 的计算
17   400559:    48 8b 45 f8         mov    -0x8(%rbp),%rax
18   40055d:    8b 00                    mov    (%rax),%eax                                  //取 a 的新值(即 *a ^ *b)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值被覆盖了
19   40055f:    31 c1                      xor       %eax,%ecx                                    //将寄存器 eax 与 ecx 中的值进行异或运算后存入寄存器 ecx 中,即 *b = *b ^ (*a ^ *b) = *a
20   400561:    48 8b 45 f0         mov    -0x10(%rbp),%rax
21   400565:    89 08                    mov    %ecx,(%rax)                                   //将寄存器 ecx 中的值写入原先存放 b 的值的地址处,至此,完成了中间那个异或赋值表达式 *b ^= *a 的计算
22   400567:    48 8b 45 f0         mov    -0x10(%rbp),%rax
23   40056b:    8b 00                    mov    (%rax),%eax                                  //取 b 的新值(即 *a)并存入寄存器 eax,注意此时寄存器 eax 中原先保存的值再次被覆盖了
24   40056d:    31 c2                    xor       %eax,%edx                                    //将寄存器 eax 与 edx 中的值进行异或运算后存入寄存器 edx 中,发现问题了吗???
25   40056f:    48 8b 45 f8          mov    -0x8(%rbp),%rax
26   400573:    89 10                    mov    %edx,(%rax)                                   //将寄存器 edx 中的值写入原先存放 a 的值的地址处
27   400575:    5d                         pop     %rbp
28   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 函数有关的段落如下:



1 00000000004004e0 <swap>:
2   4004e0:    55                              push   %rbp
3   4004e1:    48 89 e5                   mov    %rsp,%rbp
4   4004e4:    48 89 7d f8              mov    %rdi,-0x8(%rbp)
5   4004e8:    48 89 75 f0              mov    %rsi,-0x10(%rbp)
6   4004ec:    48 8b 75 f0              mov    -0x10(%rbp),%rsi
7   4004f0:    8b 06                          mov    (%rsi),%eax
8   4004f2:    48 8b 75 f8               mov    -0x8(%rbp),%rsi
9   4004f6:    8b 0e                          mov    (%rsi),%ecx
10   4004f8:    31 c1                         xor    %eax,%ecx
11   4004fa:    89 0e                         mov    %ecx,(%rsi)
12   4004fc:    48 8b 75 f0              mov    -0x10(%rbp),%rsi
13   400500:    8b 06                        mov    (%rsi),%eax
14   400502:    31 c8                        xor    %ecx,%eax
15   400504:    89 06                        mov    %eax,(%rsi)
16   400506:    48 8b 75 f8             mov    -0x8(%rbp),%rsi
17   40050a:    8b 0e                       mov    (%rsi),%ecx
18   40050c:    31 c1                        xor    %eax,%ecx
19   40050e:    89 0e                       mov    %ecx,(%rsi)
20   400510:    5d                             pop    %rbp
21   400511:    c3                             retq   
  对以上代码的分析过程可以参考前一部分,也是比较清晰易懂的。从运算过程可以看出,在进行异或运算之前,都是取出 a 或 b 的最新的值,即保证了计算过程严格按照文章开始时演算的步骤进行,也就能够得出正确的值。
  由此可见,我们的猜测是正确的,原因的确与编译器有关。如果希望 swap 函数能够在不同的编译环境下正常工作,我们可以将原异或赋值表达式拆分成以下 3 个异或赋值表达式即可。



1 *a ^= *b;
2 *b ^= *a;
3 *a ^= *b;
  另外,在使用 gcc 编译时,如果加上优化选项 -O1/-O2/-O3/-Os(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-390811-1-1.html 上篇帖子: RestTemplate请求 下篇帖子: 消息中间件
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表