【问题标题】:Sign extension with bitwise shift operation带位移运算的符号扩展
【发布时间】:2015-01-21 17:37:26
【问题描述】:

this Q&A 之后,我试图检查答案,所以我写道:

#include <stdio.h>

int main ()
{

        int t;int i;
        for (i=120;i<140;i++){
                t = (i - 128) >> 31;
                printf ("t = %X , i-128 = %X ,  ~t & i = %X , ~t = %X \n", t, i-128 , (~t &i), ~t);
        }

        return 0;
}

输出是:

t = FFFFFFFF , i-128 = FFFFFFF8 ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFF9 ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFA ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFB ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFC ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFD ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFE ,  ~t & i = 0 , ~t = 0 
t = FFFFFFFF , i-128 = FFFFFFFF ,  ~t & i = 0 , ~t = 0 
t = 0 , i-128 = 0 ,  ~t & i = 80 , ~t = FFFFFFFF 
t = 0 , i-128 = 1 ,  ~t & i = 81 , ~t = FFFFFFFF 
t = 0 , i-128 = 2 ,  ~t & i = 82 , ~t = FFFFFFFF 
t = 0 , i-128 = 3 ,  ~t & i = 83 , ~t = FFFFFFFF 
t = 0 , i-128 = 4 ,  ~t & i = 84 , ~t = FFFFFFFF 
t = 0 , i-128 = 5 ,  ~t & i = 85 , ~t = FFFFFFFF 
t = 0 , i-128 = 6 ,  ~t & i = 86 , ~t = FFFFFFFF 
t = 0 , i-128 = 7 ,  ~t & i = 87 , ~t = FFFFFFFF 
t = 0 , i-128 = 8 ,  ~t & i = 88 , ~t = FFFFFFFF 
t = 0 , i-128 = 9 ,  ~t & i = 89 , ~t = FFFFFFFF 
t = 0 , i-128 = A ,  ~t & i = 8A , ~t = FFFFFFFF 
t = 0 , i-128 = B ,  ~t & i = 8B , ~t = FFFFFFFF 

如果t声明为整数,为什么任何负数的~t都是-1 == 0xFFFFFFFF

【问题讨论】:

  • ~0 == 0xFFFFFFFF, ~~x==x。你只反转零和-1。 (假设 32 位整数)
  • 我不确定问题是否明确。你能改进它吗?
  • 标准说右移一个负值是实现定义的。阅读您的编译器文档以了解它在这种情况下的作用...
  • @0x90 好的,请阅读我的答案,如果有帮助,请告诉我。
  • @0x90 你是否为 c++ 找到相同的答案,以及哪些编译器?

标签: c++ c bit-manipulation


【解决方案1】:

发件人:Right shifting negative numbers in C

编辑:根据最新draft standard 的第 6.5.7 节,负数的这种行为取决于实现:

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

而且,您的实现可能正在使用二进制补码进行算术移位


运算符&gt;&gt; 作为有符号右移算术右移,将所有位右移指定次数。重要的是&gt;&gt; 在移位后将最左边的符号位(Most Significant Bit MSB)填充到最左边的位。这称为符号扩展,用于在您右移负数时保留负数的符号

下面是我的图解表示,并带有一个示例来说明它是如何工作的(一个字节):
示例:

i = -5 >> 3;  shift bits right three time 

五的补码形式是1111 1011内存表示:

 MSB
+----+----+----+---+---+---+---+---+
|  1 |  1 | 1  | 1 | 1 | 0 | 1 | 1 |   
+----+----+----+---+---+---+---+---+
   7    6   5    4   3   2   1   0  
  ^  This seventh, the left most bit is SIGN bit  

下面是&gt;&gt; 的工作原理?当你做-5 &gt;&gt; 3

                        this 3 bits are shifted 
                         out and loss
 MSB                   (___________)      
+----+----+----+---+---+---+---+---+
|  1 |  1 | 1  | 1 | 1 | 0 | 1 | 1 |   
+----+----+----+---+---+---+---+---+
  | \                 \  
  |  ------------|     ----------|
  |              |               |
  ▼              ▼               ▼
+----+----+----+---+---+---+---+---+
|  1 |  1 | 1  | 1 | 1 | 1 | 1 | 1 |
+----+----+----+---+---+---+---+---+
(______________)
 The sign is        
 propagated

注意:最左边的三位是 1,因为在每个移位符号位都被保留并且每个位也是正确的。我写了符号被传播,因为这三位都是因为符号(而不是数据)。

[回答]
在您的输出中

前八行

      ~t is 0
==>    t is FFFFFFFF 
==>    t is -1

(注意:-1的2补码是FFFFFFFF,因为1 = 00000001,1的补码是FFFFFFFE,2的补码=1的补码+1即:FFFFFFFE+@987654336 @ = FFFFFFFF)

所以t 在循环中的前八次总是被评估-1是的,如何?

在for循环中

for (i=120;i<140;i++){
     t = (i - 128) >> 31;

i 前八次的值是i = 120, 121, 122, 123, 124, 125, 126 ,127,所有八个值小于 128。所以返回(i - 128) = -8, -7, -6, -5, -4, -3, -2, -1。因此在前八次表达式t = (i - 128) &gt;&gt; 31 右移一个负数。

t =   (i - 128)  >> 31
t =  -ve number  >> 31

因为在您的系统中 int 是 4 字节 = 32 位,所以大多数 31 位是移出和丢失的,并且 由于符号位的传播,即 1 对于负数所有位值变为1。 (正如我在上图中显示的一个字节)

所以拳头八次:

    t =  -ve number  >> 31 ==  -1 
    t = -1
  and this gives 
    ~t = 0

因此~t的拳头八次输出为0。

剩余的最后几行

      ~t is FFFFFFFF
==>   ~t is -1   
==>    t is 0 

对于剩余的最后一行,在for循环中

for (i=120;i<140;i++){
     t = (i - 128) >> 31;

i 值是128, 129, 130, 132, 133, 134, 135, 136, 137, 138, 139, 都大于或等于 128。符号位是0

所以 (i - 128) 对于剩余的最后几行是&gt;=0 并且对于所有这些 MSB 符号位 = 0。并且因为您再次将所有位右移 31 次,然后叹息位移出和符号位0 传播并用0 填充所有位,幅度变为0

我认为如果我也写一个正数的例子会很好。所以我们以5 &gt;&gt; 3为例,五是一字节是0000 0101

                        this 3 bits are shifted 
                         out and loss
 MSB                   (___________)      
+----+----+----+---+---+---+---+---+
|  0 |  0 | 0  | 0 | 0 | 1 | 0 | 1 |   
+----+----+----+---+---+---+---+---+
  | \                 \  
  |  ------------|     ----------|
  |              |               |
  ▼              ▼               ▼
+----+----+----+---+---+---+---+---+
|  0 |  0 | 0  | 0 | 0 | 0 | 0 | 0 |
+----+----+----+---+---+---+---+---+
(______________)
 The sign is        
 propagated

再看我写符号被传播,所以最左边的三个零是由于符号位。

这就是运算符&gt;&gt; 有符号右移所做的,并且保留左操作数的符号

【讨论】:

  • 其实这个答案对于 C++ 来说是错误的。 &gt;&gt; 符号填充还是零填充取决于实现。
  • @JamesKanze 只适用于 c++,OP 使用 ptintf 头文件也是 .h 并且标记为 C 所以我认为这里的答案是正确的。是的,但链接的问题来自 c++ 和 Java。我的回答对于 Java 也是正确的。所以我认为我不应该删除我的答案。
  • @JamesKanze 如果运算符超载,您是对的,但在关于分支预测的原始问题中,&gt;&gt; 是一个幼稚的问题。
  • 没有。 C++在这里只是复制了C,C标准很清楚。如果左侧操作数具有带符号类型并且为负数,则结果由实现定义。 (我相信,原因是并非所有处理器都有算术右移指令。)
  • @JamesKanze 是的,你是对的,我不知道这一点。我刚找到一个链接。谢谢:)
【解决方案2】:

为什么 t = (i - 128) >> 31 对每个数字都给出零或-1?

当一个非负的 32 位整数右移 31 个位置时,所有非零位都被移出,最高有效位被 0 填充,所以最终得到 0。

通常,当一个负的 32 位整数右移 31 个位置时,最高有效位不会被 0 填充,而是被设置为数字的符号,因此符号会传播到所有位和2 的补码表示所有设置为 1 的位等于 -1。最终效果就像您反复将数字除以 2,但略有扭曲......结果向 -infinity 而不是向 0 四舍五入。例如 -2&gt;&gt;1==-1-3&gt;&gt;1==-2-5&gt;&gt;1==-3。这称为算术右移

当我说“通常”时,我的意思是 C 标准允许负值右移的几种不同行为。最重要的是,它允许有符号整数的非 2 补码表示。但是,通常你有 2 的补码表示和我在上面展示/解释的行为。

【讨论】:

    【解决方案3】:

    因为t 要么是0 要么是-1,所以~t 也总是-1 或0。

    这是由于(实现定义的)行为或(i - 128) &gt;&gt; 31,它本质上复制了 (i-128) 的最高位 [假设 32 位整数]。如果i > 128,它将导致最高位为零。如果i 小于 128,则结果为负,因此设置了最高位。

    由于~t 是“与t 相对的所有位”,因此如果t 为零,您可以预期t 始终为0xffffffff。

    【讨论】:

    • 这里的重点是移位是算术移位而不是逻辑移位,因为 U 声明它 int 而不是 u32unsigned int
    【解决方案4】:

    &gt;&gt; 运算符右移在大多数编译器中是算术右移,意思是除以 2。

    所以,如果,例如int i ==-4 (0xfffffffc),然后是 i&gt;&gt;1 == -2 (0xffffffffe)。

    话虽如此,我建议您检查代码的汇编。
    例如x86 有 2 条单独的指令 - shrsar,分别表示逻辑移位和算术移位。
    通常,编译器对无符号变量使用shr(逻辑移位),对有符号变量使用sar(算术移位)。


    下面是用gcc -S生成的C代码和对应的程序集:

    交流:

    int x=10;
    unsigned int y=10;
    
    int main(){
        unsigned int z=(x>>1)+(y>>1);
        return 0;
    }
    

    作为:

        .file   "a.c"
    .globl x
        .data
        .align 4
        .type   x, @object
        .size   x, 4
    x:
        .long   10
    .globl y
        .align 4
        .type   y, @object
        .size   y, 4
    y:
        .long   10
        .text
    .globl main
        .type   main, @function
    main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp
        movl    x, %eax
        sarl    %eax ; <~~~~~~~~~~~~~~~~ Arithmetic shift, for signed int
        movl    y, %edx
        shrl    %edx ; <~~~~~~~~~~~~~~~~ Logical shift, for unsigned int
        addl    %edx, %eax
        movl    %eax, -4(%ebp)
        movl    $0, %eax
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
        .section    .note.GNU-stack,"",@progbits
    

    【讨论】:

      【解决方案5】:

      C 和 C++ 中的规则是负值右移的结果是实现定义的。因此,请阅读编译器的文档。您得到的各种解释都是有效的方法,但这些方法都不是语言定义强制要求的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-09
        • 2020-06-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多