如何以可移植的方式在 C 中执行算术右移?
Posted
技术标签:
【中文标题】如何以可移植的方式在 C 中执行算术右移?【英文标题】:How can I perform arithmetic right shift in C in a portable way? 【发布时间】:2015-08-07 14:14:49 【问题描述】:我们正在编写一个需要符号传播右移的模拟器。 仿真系统使用 2 的补码。
我读到 C 中带符号整数的 >>
运算符是实现定义的。所以我不能相信它会在所有平台上产生正确的位模式。
这意味着我需要使用位操作来重现算术右移,并且我希望尽可能避免不必要的分支。
编辑:
回应评论:
“缺少的一点是 OP 需要定义什么结果是“正确的” 当 x >> y"
在 x 中设置符号位时
我基本上想重现 SAR x86 指令的行为。 那里的负数用 2 的补码表示。对于负数,右移基本上也应该意味着除以 2。
这意味着从 1 开始的位模式。所以对于 1xxxxxxx,右移应该产生 11xxxxxx。对于以 0 开头的位模式,因此 0xxxxxxx 右移应导致 00xxxxxx。所以 MSB 是“粘性的”。未定义超过字长的移位。
【问题讨论】:
你期望它做什么? 如果您需要可移植性,您希望右移做什么?你在模仿什么样的架构? 不是说否定的表示也是实现定义的事实(好吧,只有3种可能性,但仍然......)。 更新后:检查MSB,进行无符号移位,将移入位设置为MSB值。问题出在哪里? 而且,据我了解,您正在使用特定的编译器实现某些东西,对吧?所以你确切地知道编译器的实现。所以相应地使用它。 【参考方案1】: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。
将整数与 +/-1 相乘可以用有条件的无分支否定 s^(s+x)
或 (x^s)-s
来模拟。当s
是0
时,什么都没有发生,x
保持不变,所以乘以 1。当s
是-1
时,我们得到-x
,所以乘以-1
。
sn-p 的第一行 -((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 只有逻辑移位。
【讨论】:
【参考方案2】:如果您可以拥有特定于平台的代码,则可以测试现有的>>
运算符(它可能会或可能不会对有符号整数执行您想要的操作,但很可能会扩展符号)。对于大多数平台来说,这是迄今为止最简单和最有效的解决方案,因此如果考虑可移植性,我将提供另一种解决方案作为后备方案。 (不过,我不完全确定是否有任何好的方法可以使用预处理器对此进行测试,因此需要在构建解决方案中进行测试。)
如果您想手动执行此操作,您可以通过有条件地对高位掩码进行按位或运算来执行此操作,或者在许多情况下:
#define asr(x, shift) ((x) / (1 << (shift)) // do not use as is, see below
除法解决方案的问题在于,所需的最大除数不能以与 x
相同的有符号类型表示,因此您需要为 x
的类型和必要的转换(例如, 首先是更大的类型,然后再返回,因为结果适合)。
这个解决方案源于这样一个事实,即移位二进制数(在算术意义上)等同于乘以和除以 2 的幂;这既适用于模拟算术右移的除法,也适用于 1 的左移以获得两个除数的幂。
但是,它并不完全等同于二进制补码机器上的符号扩展右移,特别是如果负数 x
的除法结果为零:真正的符号扩展移位应该给出 -1
(所有位 1) 在二进制补码机器上 - 这将是 -0
在一个补码上。类似地,否定结果可能与否定 x
相差一个,这也是由于二进制补码和一个补码之间的差异。我认为除法给出了正确的算术结果,但它与符号扩展结果不匹配,因此可能不适合模拟器。
【讨论】:
【参考方案3】:为了便于移植并避免实现定义的有符号整数右移行为,请使用unsigned
进行所有移位。
Follows 是 @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】:这是一个适用于所有有效班次值的简单技巧:
// 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;
您可能想要定义大于或等于字长的移位计数的行为:
// shift x right y bits with sign replication, intel behavior */
uint32_t sar32(uint32_t x, uint32_t y)
uint32_t bottom = x >> (y &= 31);
uint32_t top = -((x & (1u << 31)) >> y);
return top | bottom;
【讨论】:
【参考方案5】:一种可能的方法是首先执行无符号右移,然后根据最高有效位的值对移位后的值进行符号扩展。利用a
和b
两个位相加,和位为a ^ b
,进位位为a & b
,我们可以通过两种方式构造符号扩展。事实证明,使用基于求和位的方法效率更高。
下面的代码展示了将算术右移模拟为函数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;
【讨论】:
【参考方案6】:算术右移相当于除以 2 到你要移动的数字,如果除数为非负数,则向零舍入,否则向无穷大。
因此,GNU C 中的通用可移植算术右移宏可以是:
#if /*$auto**/ \
__GNUC__>=7
#define $let(L,R) __auto_type L = R
#else
#define $let(L,R) __typeof(R) L = R
#endif //
#define $sar(X,By) /**/ \
(__extension__( \
$let($sar_X,X); \
$let($sar_divisor, ($sar_X*0+1)<<(By) ); \
$sar__($sar_X, $sar_divisor); \
))
#define $sar__(X,Divisor) ((X)/(Divisor)+((X)%(Divisor)>=0?0:-1))
/**/
signedX >> shift
经常用更少的机器码做同样的事情(不管它是否是实现定义的),所以你可以:
#if (-2>>1==-1 && -2l>>1==-1 && -2ll>>1==-1)
//does signedX>>Shift do an arithmetic right shift?
#define $sar(X,By) ((X)>>(By))
#else
//...the previous snippet...
#endif
【讨论】:
【参考方案7】:转换为更大的整数类型将对位进行符号扩展。这样,我们可以强制新位为符号扩展位,而不是实现定义的位,方法是强制转换为更大的类型,执行移位,然后截断并转换回。
_Static_assert(sizeof(long)>sizeof(int), "sizeof(long) must be > sizeof(int)");
int SHR(int N, unsigned char I)
return (int)((long)N>>I);
这是一种相对简单直观的方式,不需要太多的按位运算,但只有在更大的整数类型可用时才可以选择。
【讨论】:
【参考方案8】:我看不出使用>>
有什么大问题,但是如果你想进行算术右移,那么你可以将这个数字除以2
的幂x
,其中x
是数量你想要做的右移,因为将一个数字除以 2 相当于一次右移。
假设你想做a >> x
。那么也可以通过a / (int)pow(2,x)
来实现。 pow(2,x)
是数学幂,或者您也可以将其视为 2
的幂 x
。
【讨论】:
pow
实际上是浮动的,对于这样一个简单的任务来说非常昂贵。它也可能不会像左移那样被编译器优化为常数 x。【参考方案9】:
无论 'int' 的机器定义如何,此函数都可以通过移动绝对值(即不带符号)然后添加符号来工作:
int shift(int value, int count)
return ((value > 0) - (value < 0)) * (abs(value) >> count);
【讨论】:
这不适用于负值。shift(-5,1)
产生 -2
,而算术移位应返回 -3
。见这里godbolt.org/z/u5jdTj。以上是关于如何以可移植的方式在 C 中执行算术右移?的主要内容,如果未能解决你的问题,请参考以下文章
是否有一种可移植的方式在 Makefile 中制作可移植的可选依赖项?
以可移植数据格式保存/加载 scipy sparse csr_matrix