C语言 为什么这种计算整数符号的方法避免在具有标志寄存器 (IA32) 的 CPU 上分支



在下面的示例中(来源(:

int v;      // we want to find the sign of v
int sign;   // the result goes here 
// CHAR_BIT is the number of bits per byte (normally 8).
sign = -(v < 0);  // if v < 0 then -1, else 0. 
// or, to avoid branching on CPUs with flag registers (IA32):
sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
// or, for one less instruction (but not portable):
sign = v >> (sizeof(int) * CHAR_BIT - 1); 

您能否解释一下后两种方法,尤其是第二种方法?为什么它避免在带有标志寄存器的 CPU 上分支?

最后一种方法假设对于 -ve 有符号整数,>>执行算术移位。 这可能是也可能不是这样 - 标准说它是"实现定义的"。

第二种方法是假设符号位的位置(特别是"填充"确实会干扰(......从技术上讲,这是一个错误,标准方面。 然而,更糟糕的是班次的成本 - 除非机器在没有jmp的情况下无法完成第一种方法,在这种情况下,移位可能会更快。

所以问题是,你的编译器用-(v < 0)做什么? 在大多数处理器上,有几种方法可以避免流水线崩溃的jmp,并且希望您的编译器能够选择最佳方法...我会以显而易见的方式写这个:(v < 0) ? -1 : 0,让编译器完成它的工作。

当我查看 gcc (4.8( 对 -(v < 0)(v < 0) ? -1 : 0 的作用 (-O2( 时,两者都编译为sar $01f, %eax x86_64。 这让我感到惊讶(见下文(...但是您列表中的最后一种方法。

但是,我认为真正的信息是停止尝试对编译器进行二次猜测,并让它选择最佳方法。


我曾经以编写汇编程序为生,(对于x86(我会做的是:

  add   %rax, %rax
  sbb   %rax, %rax

。但是,GCC (4.8( -O0 给了我:

  shr     $01f,%eax
  movzbl  %al, %eax
  neg     %eax

。和 -O2 给了我:

  sar     $01f, %eax

。很明显,GCC 比我更不害怕轮班(在x86_64上(! 使用班次可避免设置任何标志依赖项,这可能会有所帮助。 但是我能找到的关于教学时间的少量信息表明,班次已经变得快得令人眼花缭乱! 当我尝试将sar版本与add/sbb进行计时时,sar的速度大约是原来的两倍......所以我可以不用担心轮班需要多长时间!

在 x86(自 386 以来(上,编译器可以:

  test    %eax, %eax
  setl    %al
  movzbl  %al, %eax
  neg     %eax

或(自奔腾 Pro 以来(:

  test    %eax, %eax
  mov     $0, %eax
  mov     $-1, %edx
  cmovl   %edx, %eax

。因此,有很多方法可以避免有条件的JMP。 但随着班次的速度...这一切都是学术性的!

您的 ARM 几乎可以有条件地执行任何操作,因此无需分支即可完成(x < 0) ? -1 : 0。 我不是ARM专家,但我认为SBFX可以在一个指令中完成这项工作 - 但我不知道这有多理想。 显然,算术右移ASRS也可以完成这项工作。

您的PowerPC具有加载具有当前标志的通用寄存器的说明...这也将避免跳跃。 但它也有算术右移。

无论如何,这并没有改变我的主要结论,即不值得对编译器进行二次猜测——除非编译器是垃圾,在这种情况下,最佳解决方案可能是进行显式转换......但这很可能取决于处理器!

数第二个方法只是将最高有效位 (MSB( 下移到位置 0,然后取反。惯例是 MSB 包含数字符号(1 -> 负数,0 -> 正数(。因此,对于 32 位数字,这相当于 -(v >> 31)

不过,我不明白这如何取决于"带有标志寄存器的 CPU"。

最后一个只是移动数字。由于原始号码已签名,因此移位操作可能会(或可能不会(保留该符号。实际行为取决于 C 编译器的实现细节(它是随符号扩展移动还是只是移动位(。让我们看一下位模式(为了更好的可读性,我使用 8 位数字(:

1000 0001

可以读作 129(无符号(或 -127 符号。将其向右移动一次可能会导致:

1100 0000 (sign extending shift)
0100 0000 (logical shift)

这将分别代表数字 -64 或 64。如果按 7 进行移位(对于 32 位数字为 31(,则会得到 1111 1111 或 -1。

C 标准没有明确说明编译器必须支持哪种类型的移位。在Java中,有两个移位运算符(>>表示符号扩展移位,>>>表示逻辑移位(。

最新更新