第45章 打造无分支的abs()函数

请回顾前文第12章第2节的那个函数。请设想一下,能否用x86的汇编指令打造一个无分支的版本?

int my_abs (int i)
{
          if (i<0)
                   return -i;
          else
                   return i;
};

答案当然是肯定的。

45.1 x64下的GCC 4.9.1优化

首先我们来看看,如果我们采用GCC 4.9来编译的话,会出现什么情况。

指令清单45.1 x64下的GCC 4.9优化

my_abs:
         mov    edx, edi
         mov    eax, edi
         sar    edx, 31
; EDX is 0xFFFFFFFF here if sign of input value is minus
; EDX is 0 if sign of input value is plus (including 0)
; the following two instructions have effect only if EDX is 0xFFFFFFFF
; or idle if EDX is 0
         xor    eax, edx
         sub    eax, edx
         ret

以下是其实现过程的描述:

SAR算术右[1]移:算术右移31位,我们知道算术右移是带符号的操作。有符号数的最高有效位MSB就是其符号位。也就是说,如果MSB的值为1(原操作数是负数),那么右移31位后,不管原来的数是什么,得到的结果都会是0xffffffff。当然如果MSB的值为0(原操作数是正数或零),那么最后的结果就会是零。执行完SAR后,返回值被存放在EDX寄存器中。后面就是XOR指令。当原操作数是负数时,这条指令就变成了“XOR{寄存器},0xffffffff”,相当于逐位求非的逻辑非运算;其后的SUB指令会完成补码运算的“减去负一(加一)”运算、求得绝对值,因为此时EDX寄存器的值是0xffffffff——十进制的“-1”。有关两个数的逻辑运算和递增运算详解,请参阅本书第30章。

因此,当源操作数是负数的时候,最后两条运算指令会对源操作数进行处理。在符号位的值为零,即零和正数的情况下,最后两条运算指令不会调整源操作数的值。

有关本例的详细算法,请查阅参考书目[War02,pp.2-4]的相关介绍。GCC或许借鉴了现有的成熟算法,或许这正是它自己推演的计算结果。

45.2 ARM64下的GCC 4.9优化

GCC 4.9 for ARM64的编译方式和GCC for x64的编译方式几乎相同,只是它使用的是自己CPU平台的64位寄存器而已。由于ARM64可以通过参数调节符ASR在别的指令里完成位移运算,因此GCC 4.9 for ARM64比GCC for x64分配的指令少。

指令清单45.2 ARM64下的GCC 4.9优化

my_abs:
; sign-extend input 32-bit value to X0 64-bit register:
          sxtw   x0, w0
          eor    x1, x0, x0, asr 63
; X1=X0^(X0>>63) (shift is arithmetical)
          sub    x0, x1, x0, asr 63
; X0=X1-(X0>>63)=X0^(X0>>63)-(X0>>63) (all shifts are arithmetical)
          ret

[1] 原文为左移。