【问题标题】:Why (int)((unsigned int)((int)v)?为什么是 (int)((unsigned int)((int)v)?
【发布时间】:2016-02-26 05:54:15
【问题描述】:

The website in which I found this code

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. 

该语句用变量 v 的符号(-1 或 0)分配变量 sign。我想知道为什么使用(int)((unsigned int)((int)v) 而不是普通的 v?

【问题讨论】:

  • @chux:它不是 UB,只是实现定义。 C11 6.5.7:...如果 E1 具有带符号类型和负值,则结果值是实现定义的。该评论似乎没有意义。
  • 建议从网站添加int v; int sign;。
  • @chux:评论涉及另一个问题,即在没有测试和分支的情况下提取符号位。 CPU是否有标志寄存器无关紧要。关于负值有符号类型右移的问题没有评论,但应该评论。
  • 为什么比sign = -(v &lt; 0)快?

标签: c casting bitwise-operators


【解决方案1】:

请注意,您已在问题中提取了表达式的片段(您引用 (int)((unsigned int)((int)v),它的左括号 ( 比右括号 ) 多一个)。赋值语句的 RHS 表达式全文如下:

-(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));

如果你添加几个空格,你会发现:

-(int) (  (unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)  );
       ^  ^            ^^      ^    ^                          ^  ^
       |  +------------++------+    +--------------------------+  |
       +----------------------------------------------------------+

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

((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));

(int) 的内部转换是空的;其结果立即转换为unsigned int。 (unsigned int) 演员确保正确定义正确的移位。整个表达式确定最高有效位是 0 还是 1。外部int 将结果转换回int,然后- 将其取反,因此表达式为-1 if v 是负数,0 如果 v 是零或正数 - 这就是评论所说的。

【讨论】:

  • 评论只是说您可以将函数实现为算术表达式而无需测试和分支。没有记录对演员表的微妙需求。
  • “空洞”是什么意思?这当然不是多余的(对于某些v,(unsigned int)(int)v 与(unsigned int)v 不同)
  • @M.M:你愿意给个插图吗?
  • 使用 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 当然是多余的。
  • @M.M 但这里的 v 是 int
【解决方案2】:

引用 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

【讨论】:

  • 我还是不明白为什么 v >> (sizeof(int) * CHAR_BIT - 1) 应该被转换回 int。为什么这会导致整数溢出?
  • 让我们分解这些步骤并为简单起见假设为 32 位。如果v 是负数,v &gt;&gt; 31 是实现定义的,所以我们不应该使用它。 (unsigned)v &gt;&gt; 31 的值为 1 为负值 v 和 0 为正值。但它是一个无符号表达式,因此-((unsigned)v &gt;&gt; 31) 的值是 0xFFFFFFFF,它不适合 int:将其转换为 int 或简单地将其存储到 int 变量会调用未定义的行为。在当前的实现中,这通常不是问题,但为了可移植性,需要在否定之前强制转换为(int)。
  • @chqrlie:将 UINT_MAX 转换为“int”不允许调用未定义行为。一个实现可以指定它调用一个实现定义的信号,或者可以指定它产生-1,或者如果需要可以指定它产生一些其他特定值(例如8675309),但是实现必须指定一个它将返回的值或指定它将发出信号。两种情况都不是 UB。
  • @supercat:你是对的,undefined behavior 不是正确的术语,但解释仍然成立,需要在否定之前强制转换为 (int) 以避免定义的实现行为。恕我直言,指定 2s 补码并摆脱所有这些违反直觉的过时废话将大大简化语言并防止高级编译器过度优化导致的模糊错误,这些错误从看似无辜的程序中未定义行为的微妙案例中获得不公平的优势。跨度>
  • @chqrlie:更一般地说,考虑到编译器很少由提供代码运行平台的相同实体提供,并且在许多情况下,使用编译器的程序员会了解有关平台的信息编译器编写者没有,标准应该认识到绝大多数 C 实现将有两个部分,并区分“编译器 UB”和“平台 UB”,以便知道平台支持行为的程序员标准未规定的情况将有权利用它。
【解决方案3】:

它首先转换为int,然后转换为unsigned int,然后执行转换,然后转换回int,最后它否定结果并将其存储在sign。无符号转换是可能影响结果的转换,因为它将强制进行逻辑移位(将填零),而不是算术移位(将符号扩展)。

请注意,他们实际上想要算术移位,但我不相信 C 保证它的可用性,这大概是他们手动执行逻辑移位符号位的否定的原因。

【讨论】:

  • 转移负数是实现定义的。据我所知,C 不需要二进制补码,因此它无法保证算术移位的可用性。相关:stackoverflow.com/questions/12276957/…
  • 二进制补码并不意味着算术移位,负值右移是实现定义的周期。
猜你喜欢
  • 1970-01-01
  • 2021-08-07
  • 2010-12-24
  • 2018-08-25
  • 2019-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多