在下面的示例中(来源(:
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中,有两个移位运算符(>>
表示符号扩展移位,>>>
表示逻辑移位(。