对签名数据进行逻辑右移

时间:2012-11-04 18:08:19

标签: c++ c x86 bit-manipulation bit-shift

在此之前,这不是我的作业,这是一本名为“计算机系统程序员的视角”的书(实用书btw)

我需要在有符号整数上执行逻辑移位,而不使用以下任何内容:

  • 铸造
  • if,while,select,for,do-while,?:
  • 任何类型的指针

允许的运营商是: ! +〜| >> << ^

到目前为止我尝试了什么?

/* 
 * logicalShift - shift x to the right by n, using a logical shift
 *   Can assume that 0 <= n <= 31
 *   Examples: logicalShift(0x87654321,4) = 0x08765432
 *   Legal ops: ~ & ^ | + << >>
 *   Max ops: 20
 *   Rating: 3 
 */
int logicalShift(int x, int n) {
    int mask = ~0;
    int shiftAmount = 31 + ((~n)+1);//this evaluates to 31 - n on two's complement machines
    mask = mask << shiftAmount;
    mask = ~mask;//If n equals 0, it means we have negated all bits and hence have mask = 0
    x = x >> n;
    return x & mask; 
}

只要n不等于0就可以正常工作,当它执行此代码时,因为掩码将转为全0并且函数将返回0。

我很欣赏任何正确方向的暗示,而不是完整的代码
同样,这不是一个功课;实验室作业在此公开http://csapp.cs.cmu.edu/public/labs.html

P.S不重复,不要发布涉及转换为无符号然后转移的解决方案。

4 个答案:

答案 0 :(得分:5)

你可以这样制作面具:

int mask = 1 << shiftAmount;
mask |= mask - 1;

与其他方法相比

                    this approach | other approach
can have 0 bits set :     no      |      yes
can have 32 bits set:    yes      |       no

答案 1 :(得分:3)

该问题是搜索C ++中逻辑偏移的第一个结果。

因此,回答允许 cast 的一般情况也很有意义-因为此处显示的代码均未编译(GCC 9.2,-O3)带有正确(且快速)的操作码(只需一条shr而不是sar的指令)。

最新版本

此版本还适用于即将推出的int128_t(目前在GCC,Clang和ICC中为__int128)和其他将来的类型。如果允许您使用type_traits,并且您的代码将来可以使用,而无需考虑什么是正确的无符号类型,则应使用它进行正确的转换。

代码

#include <type_traits>

template<class T>
inline T logicalShift(T t1, T t2) {
  return 
    static_cast<
      typename std::make_unsigned<T>::type
    >(t1) >> t2;
}

汇编代码分析

将其打包为f(T x, T y) { return logicalShift(x, y); }会导致以下汇编程序指令(GCC9.2,-O3):

f(int, int):
        mov     eax, edi
        mov     ecx, esi
        shr     eax, cl
        ret
f(unsigned int, unsigned int):
        mov     eax, edi
        mov     ecx, esi
        shr     eax, cl
        ret
f(__int128, __int128):
        mov     rcx, rdx
        mov     rax, rdi
        mov     rdx, rsi
        shrd    rax, rsi, cl
        shr     rdx, cl
        xor     esi, esi
        and     ecx, 64
        cmovne  rax, rdx
        cmovne  rdx, rsi
        ret
f(unsigned __int128, unsigned __int128):
        mov     rcx, rdx
        mov     rax, rdi
        mov     rdx, rsi
        shrd    rax, rsi, cl
        shr     rdx, cl
        xor     esi, esi
        and     ecx, 64
        cmovne  rax, rdx
        cmovne  rdx, rsi
        ret

T a, b; T c = a >> b;的结果是:

f(int, int):
        mov     eax, edi      # 32-bit registers
        mov     ecx, esi
        sar     eax, cl       # lower 8-bit of cx register
        ret
f(long, long):
        mov     rax, rdi      # 64-bit registers
        mov     ecx, esi
        sar     rax, cl
        ret
f(unsigned int, unsigned int):
        mov     eax, edi
        mov     ecx, esi
        shr     eax, cl
        ret
f(__int128, __int128):
        mov     rcx, rdx
        mov     rdx, rsi
        mov     rax, rdi
        sar     rdx, cl
        shrd    rax, rsi, cl
        mov     rsi, rdx
        sar     rsi, 63
        and     ecx, 64
        cmovne  rax, rdx
        cmovne  rdx, rsi
        ret

我们看到,区别主要只是shr,而不是sar(__ int128还有更多)。什么代码可能更快?


不发布版本

(将精简指令设置为〜&^ | + << >>)

问题-向左移动(SALSHL

@Fingolfin的原始想法很好。但是我们的处理器不会为int mask = ~0 << nn >= 32;的初衷,但是为什么呢?

C ++标准(草稿N4713,8.5.7,第2个)表示<<:

  

E1 << E2的值是E1个左移E2位的位置; 空位填充为零。如果E1具有无符号类型,则结果的值为E1 × 2^E2,与结果类型中可表示的最大值相比,模减少了1。否则,如果E1具有带符号的类型且非负值,并且E1 × 2^E2在结果类型的相应无符号类型中可表示,则将该值转换为结果类型,是结果值; 否则,行为是不确定的

听起来像(E1 << E2) % (UINTxx_MAX + 1),我们只从右开始用0填充,然后用模运算切掉前导位。简单明了。

分析左移

16位短,32位int和64位长的汇编代码(GCC 9.2,-O3)是:

g(short, short):
        movsx   eax, di    # 16-bit to 32-bit register
        mov     ecx, esi
        sal     eax, cl    # 1st is 32-bit, 2nd is 8-bit register
        ret
g(int, int):
        mov     eax, edi   # 32-bit registers
        mov     ecx, esi
        sal     eax, cl    # 1st is 32-bit, 2nd is 8-bit register
        ret
g(long, long):
        mov     rax, rdi   # 64-bit registers
        mov     ecx, esi
        sal     rax, cl    # 1st is 64-bit, 2nd is 8-bit register
        ret

因此,我们讨论了~0 << iint i = 0; i <= 33; i++的主张,但是我们真正得到了什么?

 0: 11111111111111111111111111111111
 1: 11111111111111111111111111111110
 2: 11111111111111111111111111111100
 3: 11111111111111111111111111111000
 4: 11111111111111111111111111110000
 5: 11111111111111111111111111100000
 6: 11111111111111111111111111000000
 7: 11111111111111111111111110000000
 8: 11111111111111111111111100000000
 9: 11111111111111111111111000000000
10: 11111111111111111111110000000000
11: 11111111111111111111100000000000
12: 11111111111111111111000000000000
13: 11111111111111111110000000000000
14: 11111111111111111100000000000000
15: 11111111111111111000000000000000
16: 11111111111111110000000000000000
17: 11111111111111100000000000000000
18: 11111111111111000000000000000000
19: 11111111111110000000000000000000
20: 11111111111100000000000000000000
21: 11111111111000000000000000000000
22: 11111111110000000000000000000000
23: 11111111100000000000000000000000
24: 11111111000000000000000000000000
25: 11111110000000000000000000000000
26: 11111100000000000000000000000000
27: 11111000000000000000000000000000
28: 11110000000000000000000000000000
29: 11100000000000000000000000000000
30: 11000000000000000000000000000000
31: 10000000000000000000000000000000
32: 11111111111111111111111111111111
33: 11111111111111111111111111111110

我们看到的结果更像是~0 << (i%2^5)

因此,请看一下长(又名int64_t)案例:(与x86的MSVC一起编译)

 0: 1111111111111111111111111111111111111111111111111111111111111111
 1: 1111111111111111111111111111111111111111111111111111111111111110
 2: 1111111111111111111111111111111111111111111111111111111111111100
 3: 1111111111111111111111111111111111111111111111111111111111111000
 4: 1111111111111111111111111111111111111111111111111111111111110000
 5: 1111111111111111111111111111111111111111111111111111111111100000
 6: 1111111111111111111111111111111111111111111111111111111111000000
 7: 1111111111111111111111111111111111111111111111111111111110000000
 8: 1111111111111111111111111111111111111111111111111111111100000000
 9: 1111111111111111111111111111111111111111111111111111111000000000
10: 1111111111111111111111111111111111111111111111111111110000000000
11: 1111111111111111111111111111111111111111111111111111100000000000
12: 1111111111111111111111111111111111111111111111111111000000000000
13: 1111111111111111111111111111111111111111111111111110000000000000
14: 1111111111111111111111111111111111111111111111111100000000000000
15: 1111111111111111111111111111111111111111111111111000000000000000
16: 1111111111111111111111111111111111111111111111110000000000000000
17: 1111111111111111111111111111111111111111111111100000000000000000
18: 1111111111111111111111111111111111111111111111000000000000000000
19: 1111111111111111111111111111111111111111111110000000000000000000
20: 1111111111111111111111111111111111111111111100000000000000000000
21: 1111111111111111111111111111111111111111111000000000000000000000
22: 1111111111111111111111111111111111111111110000000000000000000000
23: 1111111111111111111111111111111111111111100000000000000000000000
24: 1111111111111111111111111111111111111111000000000000000000000000
25: 1111111111111111111111111111111111111110000000000000000000000000
26: 1111111111111111111111111111111111111100000000000000000000000000
27: 1111111111111111111111111111111111111000000000000000000000000000
28: 1111111111111111111111111111111111110000000000000000000000000000
29: 1111111111111111111111111111111111100000000000000000000000000000
30: 1111111111111111111111111111111111000000000000000000000000000000
31: 1111111111111111111111111111111110000000000000000000000000000000
32: 1111111111111111111111111111111100000000000000000000000000000000
33: 1111111111111111111111111111111000000000000000000000000000000000
34: 1111111111111111111111111111110000000000000000000000000000000000
35: 1111111111111111111111111111100000000000000000000000000000000000
36: 1111111111111111111111111111000000000000000000000000000000000000
37: 1111111111111111111111111110000000000000000000000000000000000000
38: 1111111111111111111111111100000000000000000000000000000000000000
39: 1111111111111111111111111000000000000000000000000000000000000000
40: 1111111111111111111111110000000000000000000000000000000000000000
41: 1111111111111111111111100000000000000000000000000000000000000000
42: 1111111111111111111111000000000000000000000000000000000000000000
43: 1111111111111111111110000000000000000000000000000000000000000000
44: 1111111111111111111100000000000000000000000000000000000000000000
45: 1111111111111111111000000000000000000000000000000000000000000000
46: 1111111111111111110000000000000000000000000000000000000000000000
47: 1111111111111111100000000000000000000000000000000000000000000000
48: 1111111111111111000000000000000000000000000000000000000000000000
49: 1111111111111110000000000000000000000000000000000000000000000000
50: 1111111111111100000000000000000000000000000000000000000000000000
51: 1111111111111000000000000000000000000000000000000000000000000000
52: 1111111111110000000000000000000000000000000000000000000000000000
53: 1111111111100000000000000000000000000000000000000000000000000000
54: 1111111111000000000000000000000000000000000000000000000000000000
55: 1111111110000000000000000000000000000000000000000000000000000000
56: 1111111100000000000000000000000000000000000000000000000000000000
57: 1111111000000000000000000000000000000000000000000000000000000000
58: 1111110000000000000000000000000000000000000000000000000000000000
59: 1111100000000000000000000000000000000000000000000000000000000000
60: 1111000000000000000000000000000000000000000000000000000000000000
61: 1110000000000000000000000000000000000000000000000000000000000000
62: 1100000000000000000000000000000000000000000000000000000000000000
63: 1000000000000000000000000000000000000000000000000000000000000000
64: 0000000000000000000000000000000000000000000000000000000000000000
65: 0000000000000000000000000000000000000000000000000000000000000000

轰!

(同样,直到31在GCC中也很短,因为它使用EAX的32位sal寄存器)

但是,此结果仅由编译器创建:

x86 msvc v19.22 ,/ O2:
_x$ = 8                                       ; size = 8
_y$ = 16                                                ; size = 8
__int64 g(__int64,__int64) PROC                                  ; g, COMDAT
        mov     eax, DWORD PTR _x$[esp-4]
        mov     edx, DWORD PTR _x$[esp]
        mov     ecx, DWORD PTR _y$[esp-4]
        jmp     __allshl
__int64 g(__int64,__int64) ENDP  
x64 msvc v19.22 ,/ O2:
x$ = 8
y$ = 16
__int64 g(__int64,__int64) PROC                                  ; g, COMDAT
        mov     rax, rcx
        mov     rcx, rdx
        shl     rax, cl
        ret     0
__int64 g(__int64,__int64) ENDP      

x64 MSVC代码显示出与GCC 9.2代码相同的行为-使用shl而不是sal

从那时起,我们现在知道处理器本身(第六代Intel Core)仅使用cl寄存器的最后一位,这取决于移位操作的第一个寄存器的长度,即使C ++标准还有其他规定。

一个小修正

因此,这就是代码中断的地方。本能地,我将使用32 - n的shiftAmount来解决上面的问题,您已经通过使用shiftAmount的{​​{1}}避免了这一问题。知道n为0..31时,没有shiftAmount为32。很好。

但是,一方面减少意味着另一方面。 31 - n现在需要从mask开始(我们不移-2,我们移0b1111):

0b1110

它有效!

备用代码和分析

Fingolfins代码(固定)

上面的代码,如Asambler(GCC 9.2,-O3):

int logSh3(int x, int n) {
    int mask = ~2 + 1;
    int shiftAmount = 31 + ((~n) + 1);//this evaluates to 31 - n on two's complement machines
    mask = mask << shiftAmount;
    mask = ~mask;//If n equals 0, it means we have negated all bits and hence have mask = 0
    x = x >> n;
    return x & mask;
}

9条指令

哈罗兹代码

logSh3(int, int):
        mov     ecx, 31
        mov     edx, -2
        mov     eax, edi
        sub     ecx, esi
        sal     edx, cl
        mov     ecx, esi
        not     edx
        sar     eax, cl
        and     eax, edx
        ret
int logSh2(int x, int n) {
    int shiftAmount = 31 + ((~n) + 1);//this evaluates to 31 - n on two's complement machines
    int mask = 1 << shiftAmount;
    mask |= mask + ((~1) + 1);
    x = x >> n;
    return x & mask;
}

8条指令

我们可以做得更好吗?

另一种解决方案

我们可以logSh2(int, int): mov ecx, esi mov r8d, edi mov edi, -2147483648 shr edi, cl sar r8d, cl lea eax, [rdi-1] or eax, edi and eax, r8d ret 进行右移,而不是向左移,将其向后移一并反转。

0b1000
int logSh4(int x, int n) {
    int mask = 0x80000000;
    mask = mask >> n;
    mask = mask << 1;
    mask = ~mask;//If n equals 0, it means we have negated all bits and hence have mask = 0
    x = x >> n;
    return x & mask;
}

7条指令

更好的方式?

logSh4(int, int): mov ecx, esi mov edx, -2147483648 sar edx, cl sar edi, cl lea eax, [rdx+rdx] not eax and eax, edi ret 向右移动,再将其向左移动一次并加1。因此,我们得到了相反的结果:

0b0111
int logSh5(int x, int n) {
    int mask = 0x7fffffff;
    mask = mask >> n;
    mask = (mask << 1) | 1;
    x = x >> n;
    return x & mask;
}
剩余

6条指令。精细。 (但是简单的转换仍然是实践中最好的解决方案)

答案 2 :(得分:2)

俗气的答案显然违反了问题的实质:casting 1 不是用C或C ++进行类型转换的唯一方法

return (x | 0U) >> n;    // assumes int and unsigned are the same width

Godbolt for x86-64 GCC)。请注意,它也依赖于n==0为负的x的实现定义的行为,因为这会使符号位置1。但是在普通2的补码实现中,无符号和整数之间的转换仅保留位模式以及正整数的值。使用memcpy而不是使用隐式类型转换来使类型双关语显式可以使其更健壮。

|运算符使用usual arithmetic conversions的规则来使两个操作数具有相同的类型。我们可以使用它来隐式转换为无符号的规则转换,以围绕铸造的人为限制。

0U数字文字后缀不是强制转换,只是无符号常量的纯语言语法。另一种选择是x & UINT_MAXx | (~UINT_MAX)。甚至更有趣,如果您知道int的宽度并且小于或等于unsigned,则可以编写一个像0xffffffff这样的常量,对于正号{{1 }},如果适合的话,它的类型将为int,如果适合的话,它将具有unsignedlong的类型,甚至更大的类型。

当然,long long也不是演员,所以实际上就是这么做!但这更显然只是一个愚蠢的规则,旨在寻找有问题的漏洞,而不是故意的漏洞。


假设:

  • 这可能仅适用于2的补码,其中从有符号到无符号的转换不会修改对象表示的位模式。 (相关:How can an (int) be converted to (unsigned int) while preserving the original bit pattern?-在对无符号进行模减少之前,转换将保留值,而不是位模式。)

  • unsigned tmp = x;unsigned int的宽度相同,因此我们不会丢失任何位,也不会在逻辑运算符之前进行符号扩展以引入高int位右移。**

例如1不起作用;会在逻辑右移之前将(x | 0ULL) >> n扩展为x到64位(或其他值),因此结果的int小宽度将有效地包含符号位移入。(假设为2的补码。)

解决方法:使用无符号位字段,其非填充位的确切数量为int。有符号整数类型具有numeric_limits<T>::digits个非符号位和1个符号位。 (以及未知数量的填充)。这将需要分配,而不仅仅是与|一起使用。

struct uint { 
   unsigned long u : (1 + std::numeric_limits<int>::digits);
}

如果您想memcpy进入此结构而不是赋值(例如,确保在非2补码系统上右移之前位模式不变),则可以使用类似char pad[sizeof(int)];。这绝对是多余的,要使填充大小至少等于intmemcpy(&struct_tmp, &x, sizeof(x))或其他方向是必需的),则需要更多填充。我不确定结构是否保证至少与unsigned long一样宽,因为我们将其用作位域的基本类型,所以我也必须仔细检查标准的那部分。不过,GCC和MSVC就是这种情况。 unsigned long long的大小最大为8。IIRC,保证unsigned long的宽度至少与int一样。

如果我们想计算char[]数组所需的确切填充量,则必须处理sizeof(int) - sizeof(unsigned)为负数的可能性。但是可以计算另一个填充 bitfield 成员的额外位数,将填充计算为CHAR_BIT * sizeof(int) - (1 + digits)

在C99中,我们可以使用联合类型校正来替换memcpy,但是我们仍然可能需要一个位域来确保将无符号值截断为正确的宽度。


脚注1 :ISO C ++ 7.6.3 Explicit type conversion (cast notation)[expr.cast]说:

2:显式类型转换可以使用功能符号,类型转换运算符(dynamic_cast,static_cast,reinterpret_cast,const_cast)或强制转换符号表示。

    cast-expression:
        unary-expression
        ( type-id ) cast-expression

整个部分的名称为[expr.cast],因此将类型转换的这些语法中的任何一种都视为类型转换是合理的,而不仅仅是“ cast表达式”。 幸运的是,无需尝试证明unsigned(x) >> n不包含强制转换。

答案 3 :(得分:0)

我知道我有点晚了,但我认为最好的方法是做这样的事情:

int logicalShift(int x, int n) {
   int mask = ( 1 << sizeof(int) - n ) - 1;
   return x >> n & mask;
}

掩码将最高n位设置为0。

当然,这可以写成:

int logicalShift(int x, int n) {
   return x >> n & ( 1 << sizeof(int) - n ) - 1;
}

但是编译器可能会理解如何优化第一个。