我们正在编写一个模拟器,我们需要传播右移的符号。 仿真系统使用2的补码。
我读到C中有符号整数的>>
运算符是实现定义的。所以我不能依赖它会在所有平台上产生正确的位模式。
这意味着我需要使用位操作来重现算术右移,如果可能的话,我希望避免不必要的分支。
编辑:
回应评论:
“缺少的是OP需要定义什么结果是”正确的“ 当符号位在x中设置时,x>> Y“
我基本上想要重现SAR x86指令的行为。 在那里,负数用2的补码表示。对于负数,右移基本上应该除以2。
这意味着从1开始的位模式。因此对于1xxxxxxx,右移应该得到11xxxxxx。对于以0开头的位模式,所以0xxxxxxx右移应该导致00xxxxxx。所以MSB是“粘性的”。没有定义超过字长的移位。
答案 0 :(得分:3)
如果您可以拥有特定于平台的代码,则可以测试现有的>>
运算符(可能会或可能不会执行您想要的有符号整数,但很可能会扩展该符号)。对于大多数平台而言,这是迄今为止最简单,最有效的解决方案,因此如果可移植性是一个问题,我会提供另一种解决方案作为后备。 (我并不完全确定有任何好方法可以使用预处理器进行测试,因此测试需要进入构建解决方案。)
如果您想手动执行此操作,可以通过有条件地按位对高位掩码执行此操作,或者在许多情况下执行此操作:
#define asr(x, shift) ((x) / (1 << (shift)) // do not use as is, see below
除法解决方案的问题在于,所需的最大除数在与x
相同的有符号类型中无法表示,因此您需要针对x
的类型适当地转换类型必要的转变(例如,首先是更大的类型,然后返回,因为结果适合)。
这个解决方案是由于移位二进制数相等(在算术意义上)乘以除以2的幂;这既适用于模拟算术右移的除法,也适用于左移1以获得两个除数的幂。
然而,它并不完全等同于两个补码机上的符号扩展右移,特别是如果负x
的除法导致零:真正的符号扩展移位应该在双补码机器上给出-1
(所有位1) - 这将是-0
的补码。类似地,负面结果可能会被一个负x
的结果所取消,这也是由于两个补码和一个补码之间的差异。我认为除法给出了正确的算术结果,但它与符号扩展结果不匹配,因此可能不适合模拟器。
答案 1 :(得分:3)
您可以采用不同的方式,例如分别生成顶部和底部,然后组合:(未测试)
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
在设置y
的情况下y
等于0 .. 31
为0,那么怪物(严重地只使用分支)的工作原理是将掩码设置为全零。在y
中(您可以通过屏蔽if (x & (1u << 31))
res = ~(~x >> y);
else
res = x >> y;
轻松实现这一点。)
你仍然需要小心不正确的班次金额,但是处理这取决于你的模仿。
另外,如果你不介意分支,我会建议:(未经测试)
{{1}}
答案 2 :(得分:3)
int s = -((unsigned) x >> 31);
int sar = (s^x) >> n ^ s;
这需要5个按位运算。
如上所述,算术右移x >> n
对应于除法x / 2**n
。如果系统仅支持逻辑右移,则可以先将负数转换为正数,然后再将其符号复制回sgn(x) * (abs(x)/2**n)
。这等效于右移sgn(x) * ((sgn(x)*x)/2**n)
前后乘以+/- 1。
可以使用条件无分支否定s^(s+x)
或(x^s)-s
来模拟将+/- 1整数相乘。当s
为0
时,什么也没有发生,并且x
保持不变,因此与1相乘。当s
为-1
时,我们得到-x
,因此与-1
相乘。
摘要的第一行-((unsigned) x >> 31)
提取符号位。
在这里,unsigned
转换可确保compilation into a logical right shift(汇编中的SHR)。因此,直接结果为0或1,并且在取反s
之后是所需的0
或-1
。
在移位前后有两个无分支的求反,我们得出((s^s+x) >> n) + s ^ s
。这会执行除法,将结果舍入为零(例如-5>>1 = -2
)。但是,算术右移(汇编中的SAR)限制了结果(即-5>>1 = -3
)。要实现此行为,必须放弃+s
操作。
演示在这里:https://godbolt.org/和https://onlinegdb.com/Hymres0y8。
PS:我到达这里是因为gnuplot只有逻辑上的偏移。
答案 3 :(得分:1)
为了便于移植并避免实现已定义的有符号整数右移的行为,请使用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;
}
或作为单行
y = (((unsigned) x) >> shift) | (((0-(((unsigned) x) >> (W-1))) << 1) << (W-1-shift));
答案 4 :(得分:1)
这是一个简单的技巧,适用于所有有效的班次值:
a, b = np.mgrid[...].reshape(2, -1)
您可能要定义大于或等于单词大小的移位计数的行为:
// shift x right y bits (0..31) with sign replication */
uint32_t sar32(uint32_t x, uint32_t y) {
uint32_t bottom = x >> y;
uint32_t top = -((x & (1u << 31)) >> y);
return top | bottom;
}
答案 5 :(得分:0)
我在使用>>
时没有遇到任何重大问题,但如果您想进行算术右移,则可以将数字除2
到幂x
,其中x
是您想要做的右移的数量,因为将数字除以2相当于单个右移。
假设你想做a >> x
。然后也可以通过a / (int)pow(2,x)
来实现。 pow(2,x)
是数学上的力量,或者您也可以将其2
视为权力x
。
答案 6 :(得分:0)
一种可能的方法是首先执行无符号右移,然后根据最高有效位的值对移位值进行符号扩展。使用以下事实:当添加两个位a
和b
时,总和位为a ^ b
且进位位为a & b
,我们可以通过两种方式构造符号扩展。事实证明,使用基于sum位的方法更有效。
下面的代码显示了算术右移作为函数arithmetic_right_shift()
和测试框架的模拟; T
是您希望操作的整数类型。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#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;
}
答案 7 :(得分:0)
我想我会这样写:
-2>>1==-1 /*statically known; does this platform shift arithmetically?*/
? X>>By /*do the implementation-defined thing*/
: X/((X-X+1)<<By)
/*otherwise (unlikely) do division (X-X+1 == appropriately promoted 1) */
;
答案 8 :(得分:-2)
此功能将在计算机中定义为'int'时起作用,方法是移动绝对值(即不带符号),然后添加符号:
int shift(int value, int count)
{
return ((value > 0) - (value < 0)) * (abs(value) >> count);
}