如何以便携方式在C中执行算术右移?

我们正在编写一个模拟器,我们需要传播右移的符号。 仿真系统使用2的补码。

我读到C中有符号整数的>>运算符是实现定义的。 所以我不能依赖它将在所有平台中产生正确的位模式的事实。

这意味着我需要使用位操作来重现算术右移,如果可能的话我想避免不必要的分支。

编辑:

回应评论:

“缺少的一点是,当符号位在x中设置为x >> y时,OP需要定义什么结果是”正确的“

我基本上想要重现SAR x86指令的行为。 在那里,负数用2的补码表示。 对于负数,右移基本上应该除以2。

这意味着从1开始的位模式。因此对于1xxxxxxx,右移应该得到11xxxxxx。 对于以0开头的位模式,所以0xxxxxxx右移应该导致00xxxxxx。 所以MSB是“粘性的”。 没有定义超过字长的移位。

您可以采用不同的方式,例如分别生成顶部和底部,然后组合:(未测试)

 uint32_t bottom = x >> y; uint32_t top = -(x >> 31) << (32 - y); return top | bottom; 

那里的否定将1变成了一个完整的掩码,向左移动所以它们只是它们应该存在的位置。 我假设这里的所有东西都是uint32_t ,所以依靠二的补语否定是好的。

不能为零。 这是一个廉价的分支,但很好地预测。 零移位应该比其他任何移动动态明显更为罕见。

如果你必须避免它,即使是那个分支也可以避免。 例如,(未测试)

 uint32_t bottom = x >> y; uint32_t mask = -((x >> 31) & ~((32 - y) >> 5)); uint32_t top = mask << ((32 - y) & 31); return top | bottom; 

那个怪物(严重地只是使用一个分支)通过将掩码设置为全零来工作,如果32 - y有第5位设置,相当于y为零,假设y0 .. 31 (你可以很容易地做到这一点)掩盖y )。

你仍然需要小心不正确的转移金额,但这取决于你正在模仿的东西。


此外,如果你不介意分支,我会建议:(未测试)

 if (x & (1u << 31)) res = ~(~x >> y); else res = x >> y; 

如果你可以拥有特定于平台的代码,你可以测试现有的>>运算符(对于有符号整数,可能会或可能不会执行所需的操作,但很可能会扩展符号)。 对于大多数平台而言,这是迄今为止最简单,最有效的解决方案,因此如果可移植性是一个问题,我会提供另一种解决方案作为后备。 (但我并不完全确定有任何好的方法可以使用预处理器对此进行测试,因此测试需要进入构建解决方案。)

如果您想手动执行此操作,可以通过有条件地按位或运行高位掩码来执行此操作,或者在许多情况下:

 #define asr(x, shift) ((x) / (1 << (shift)) // do not use as is, see below 

除法解决方案的问题在于所需的最大除数在与x相同的有符号类型中是不可表示的,因此您需要针对x的类型和必要的移位(例如,首先更大类型和类型)适当地转换类型。然后回来,因为结果将适合)。

该解决方案遵循以下事实:移位二进制数相等(在算术意义上)乘以除以2的幂; 这既适用于模拟算术右移的除法,也适用于左移1以获得两个除数的幂。

然而,它并不完全等同于两个补码机上的符号扩展右移,特别是如果负x的除法导致零:真正的符号扩展移位应该给出一个二(1)所有位1补充机器 - 这将是一个补码上的-0 。 类似地,负面结果可能会被一个负x结果所取消,这也是由于两个和一个补码之间的差异。 我认为除法给出了正确的算术结果,但它与符号扩展结果不匹配,因此可能不适合仿真器。

为了便于移植并避免实现已定义的有符号整数右移的行为,请使用unsigned整数进行移位。

以下是@harold答案的变体。 它不会移位位宽(也就是UB),也不会取决于2的补码。 没有分支。 如果在不使用2的补码的稀有机器上,可以创建陷阱值。

 #if INT_MAX == 0x7FFF && UINT_MAX == 0xFFFF #define W 16 #elif INT_MAX == 0x7FFFFFFF && UINT_MAX == 0xFFFFFFFF #define W 32 #else // Following often works #define W (sizeof (unsigned)*CHAR_BIT) #endif int TwosComplementArithmeticRightShift(int x, int shift) { unsigned ux = (unsigned) x; unsigned sign_bit = ux >> (W-1); y = (ux >> shift) | (((0-sign_bit) << 1) << (W-1-shift)); return y; } 

或作为一个class轮

  y = (((unsigned) x) >> shift) | (((0-(((unsigned) x) >> (W-1))) << 1) << (W-1-shift)); 

我没有看到使用>>任何重大问题,但是如果你想进行算术右移,那么你可以将数字除以2得到幂x ,其中x是你想要做的右移的数量,因为划分了数字2相当于单个右移。

假设你想做a >> x 。 然后它也可以通过做a / (int)pow(2,x)pow(2,x)是数学上的幂,或者你也可以把它作为2的幂x

一种可能的方法是首先执行无符号右移,然后根据最高有效位的值对移位值进行符号扩展。 使用以下事实:当添加两个比特ab ,和比特是a ^ b并且进位比特是a & b ,我们可以用两种方式构造符号扩展。 事实certificate,使用基于和位的方法更有效。

下面的代码显示了算术右移仿真函数arithmetic_right_shift()以及测试框架; T是您希望操作的整数类型。

 #include  #include  #include  #define T int #define EXTEND_USING_CARRY_BIT (1) #define EXTEND_USING_SUM_BIT (2) #define SIGN_EXTEND_METHOD EXTEND_USING_SUM_BIT T arithmetic_right_shift (T a, int s) { unsigned T mask_msb = (unsigned T)1 << (sizeof(T) * CHAR_BIT - 1); unsigned T ua = a; ua = ua >> s; mask_msb = mask_msb >> s; #if (SIGN_EXTEND_METHOD == EXTEND_USING_SUM_BIT) return (T)((ua ^ mask_msb) - mask_msb); #else // SIGN_EXTEND_METHOD return (T)(ua - 2 * (ua & mask_msb)); #endif // SIGN_EXTEND_METHOD } int sar_ref (int a, int s) { int res; __asm mov eax, dword ptr [a]; __asm mov ecx, s; __asm sar eax, cl; __asm mov dword ptr [res], eax; return res; } int main (void) { unsigned int x; int a, s, res, ref; s = 0; do { x = 0; do { a = (int)x; res = arithmetic_right_shift (a, s); ref = sar_ref (a, s); if (ref != res) { printf ("!!!! a=%08x s=%d res=%08x ref=%08x\n", a, s, res, ref); return EXIT_FAILURE; } x++; } while (x); s++; } while (s < 32); return EXIT_SUCCESS; }