引用 C 标准 6.5.7p5:
E1 >> E2 的结果是 E1 右移 E2 位位置。如果 E1 具有无符号类型或 E1 具有带符号类型和非负值,则结果的值是 E1 / 2E2 的商的整数部分。如果 E1 具有带符号类型和负值,则结果值是实现定义的。
作者正在写如何实现一个函数sign(int v),它有效地返回-1 用于负数和0 用于0 和正数。一种天真的方法是这样的:
int sign(int v) {
if (v < 0)
return -1;
else
return 0;
}
但此解决方案可能会编译为执行比较并在比较设置的 CPU 标志上分支的代码。这是低效的。他提出了一个更简单、更直接的解决方案:
sign = -(v > 0);
但是这种计算仍然需要在不直接将比较结果作为布尔值生成的 CPU 上进行比较和分支。带有标志寄存器的 CPU 通常在比较指令甚至大多数算术指令时设置各种标志。所以他提出了另一种基于移位符号位的解决方案,但正如标准上面规定的那样,他不能依赖右移一个负值的结果。
将v 转换为unsigned 消除了这个问题,因为正确指定了右移无符号值。假设符号位在最高位置,这对于所有现代处理器都是正确的,但 C 标准没有强制要求,将 (unsigned)v 右移比其类型中的位数少 1 会产生 1 的值负值和0 否则。否定结果应该产生预期值-1 为负v 和0 为正和零v。但是表达式是无符号的,所以普通的否定会产生UINT_MAX 或0,当存储到int 或什至只是转换为(int) 时会导致算术溢出。将此结果转换回int,然后再正确计算所需的结果,-1 表示负数v,0 表示正数或零v。
算术溢出通常是良性的,并且被大多数程序员广泛忽略,但现代编译器倾向于利用其未定义性来执行激进的优化,因此依赖预期的行为和最佳行为是不明智的在所有情况下避免算术溢出。
表达式可以简化为:
sign = -(int)((unsigned)v >> (sizeof(int) * CHAR_BIT - 1));
请注意,如果将右移定义为为您的平台复制位(当前 CPU 的几乎通用行为),则表达式会简单得多(假设 int v):
sign = v >> (sizeof(v) * CHAR_BIT - 1)); // works on x86 CPUs
bithacks 页面https://graphics.stanford.edu/~seander/bithacks.html 确实很有启发性,包含详细解释:
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);
对于 32 位整数,上述最后一个表达式的计算结果为 sign = v >> 31。这是一种比明显方式更快的操作,sign = -(v
作为结语,我建议使用可读性最高的版本,并依靠编译器来生成最高效的代码:
sign = -(v < 0);
正如可以在这个启发性的页面上验证的那样:http://gcc.godbolt.org/# 用gcc -O3 -std=c99 -m64 编译上面的代码确实会为上面的所有解决方案生成下面的代码,即使是最天真的if/else 声明:
sign(int):
movl %edi, %eax
sarl $31, %eax
ret