为什么(int)((unsigned int)((int)v)?

nal*_*zok 26 c casting bitwise-operators

我在其中找到此代码的网站

int v, sign;
// or, to avoid branching on CPUs with flag registers (IA32):
sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));  // if v < 0 then -1, else 0. 
Run Code Online (Sandbox Code Playgroud)

此语句为变量符号指定变量v的符号(-1或0).我想知道为什么(int)((unsigned int)((int)v)用而不是普通的v?

Jon*_*ler 32

请注意,您已经在问题中提取了表达式的片段(您引用的(int)((unsigned int)((int)v)左括号(比右括号多一个)).赋值语句的RHS表达式是完整的:

-(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
Run Code Online (Sandbox Code Playgroud)

如果添加一些空格,您会发现:

-(int) (  (unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)  );
       ^  ^            ^^      ^    ^                          ^  ^
       |  +------------++------+    +--------------------------+  |
       +----------------------------------------------------------+
Run Code Online (Sandbox Code Playgroud)

也就是说,外部(int)演员适用于所有:

((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
Run Code Online (Sandbox Code Playgroud)

铸造的内部铸件(int)是空的; 它的结果立即转为unsigned int.的(unsigned int)铸造确保了右移被很好地定义.表达式作为一个整体确定最高有效位是0还是1.外部int将结果转换回a int,然后将-其否定,因此表达式为-1if v为负且0if v为零或正 - 这是什么评论说.

  • 使用`double v = -2.0;`,然后`(unsigned int)v`导致未定义的行为,但`(unsigned int)(int)v`是明确定义的.此外,使用`long long v = LLONG_MAX`,则`(unsigned int)v`是明确定义的,但`(unsigned int)(int)v`会导致实现定义的行为,这可能会引发信号.另一个不同的情况是16位int系统上的`unsigned short v = USHRT_MAX`.这可能都是没有意义的,因为chux已经指出代码只能用于`int v;`,在这种情况下,将`v`转换为int当然是多余的. (5认同)
  • @MM但这里的v是一个`int` (4认同)

chq*_*lie 20

引用C标准6.5.7p5:

E1 >> E2的结果是E1右移E2位位置.如果E1具有无符号类型或者E1具有有符号类型和非负值,则结果的值是E1/2E2的商的整数部分.如果E1具有带符号类型和负值,则结果值是实现定义的.

作者正在撰写如何实现一个函数sign(int v),该函数返回-1负数,有效返回00和正数.一个天真的方法是:

int sign(int v) {
    if (v < 0)
        return -1;
    else
        return 0;
}
Run Code Online (Sandbox Code Playgroud)

但是,此解决方案可以编译为执行比较的代码,并对由比较设置的CPU标志进行分支.这是低效的.他提出了一个更简单,更直接的解决方案:

sign = -(v > 0);
Run Code Online (Sandbox Code Playgroud)

但是这个计算仍然需要对不直接作为布尔值产生比较结果的CPU进行比较和分支.具有标志寄存器的CPU通常在比较指令或甚至大多数算术指令时设置各种标志.所以他提出了另一种基于移位符号位的解决方案,但正如上面标准所规定的那样,他不能依赖于右移右值的结果.

转换vunsigned删除此问题,因为正确指定了右移无符号值.假设符号位处于最高位置,对于所有现代处理器都是如此,但不是由C标准强制要求,右移(unsigned)v一个小于其类型中的位数的值会产生1负值的值,0否则.否定结果应产生-1负值v0正值和零的预期值v.但是表达式是无符号的,因此明确的否定将产生UINT_MAX或者0,当存储到一个int或者甚至只是作为一个时,它会导致算术溢出(int).将此结果转换回int正确否定之前计算所需的结果,-1对于负数v0正数或零v.

算术溢出通常是良性的,并且被大多数程序员广泛忽略,但是现代编译器倾向于利用其未定义来执行积极的优化,因此依赖于预期但无根据的行为是不明智的,并且最好在所有情况下避免算术溢出.

表达式可以简化为:

sign = -(int)((unsigned)v >> (sizeof(int) * CHAR_BIT - 1));
Run Code Online (Sandbox Code Playgroud)

请注意,如果将右移定义为复制平台的位(当前CPU几乎是通用的行为),则表达式将更加简单(假设int v):

sign = v >> (sizeof(v) * CHAR_BIT - 1));   // works on x86 CPUs
Run Code Online (Sandbox Code Playgroud)

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); 
Run Code Online (Sandbox Code Playgroud)

对于32位整数,上面的最后一个表达式求值为sign = v >> 31.这是一种比显而易见的方式更快的操作,sign = - (v <0).这个技巧有效,因为当有符号整数向右移位时,最左边的位的值被复制到其他位.当值为负时,最左边的位为1,否则为0; 所有1位给出-1.不幸的是,这种行为是特定于架构的.

作为结语,我建议使用最易读的版本并依赖编译器来生成最有效的代码:

sign = -(v < 0);
Run Code Online (Sandbox Code Playgroud)

正如在这个有启发性的页面上可以验证的那样:http://gcc.godbolt.org/# 编译上面的代码gcc -O3 -std=c99 -m64确实为上面的所有解决方案生成了下面的代码,甚至是最天真的if/ else声明:

sign(int):
    movl    %edi, %eax
    sarl    $31, %eax
    ret
Run Code Online (Sandbox Code Playgroud)


Tom*_*zes 10

它首先是铸造int,然后是unsigned int,然后它正在执行转移,然后它会重新投入int,最后它会否定结果并存储它sign.无符号转换是可能影响结果的转换,因为它将强制逻辑移位(将填零),而不是算术移位(将签名扩展).

请注意,他们实际上想要算术移位,但我不相信C保证其可用性,这可能是为什么他们手动执行逻辑移位符号位的否定.