一个数据交换函数引发的思考

近日,在书中看到一个关于数据交换函数的源代码,发现挺有意思,具体代码如下:

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(默认情况下是不进行优化的),我们也能够得到正确的答案。有兴趣的读者可以自己进行验证,笔者就不再赘述了。

posted @ 2016-08-06 20:07  茫茫人海1314  阅读(878)  评论(1编辑  收藏  举报