如何以可移植的方式在 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 来模拟。当s0 时,什么都没有发生,x 保持不变,所以乘以 1。当s-1 时,我们得到-x,所以乘以-1

sn-p 的第一行 -((unsigned) x >> 31) 提取符号位。 在这里,unsigned 转换确保了compilation into a logical right shift(汇编中的 SHR)。因此,立即结果为0或1,取反后s0-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】:

一种可能的方法是首先执行无符号右移,然后根据最高有效位的值对移位后的值进行符号扩展。利用ab两个位相加,和位为a ^ b,进位位为a &amp; 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 &gt;&gt; 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】:

我看不出使用&gt;&gt; 有什么大问题,但是如果你想进行算术右移,那么你可以将这个数字除以2 的幂x,其中x 是数量你想要做的右移,因为将一个数字除以 2 相当于一次右移。

假设你想做a &gt;&gt; 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 中执行算术右移?的主要内容,如果未能解决你的问题,请参考以下文章

如何以可移植的方式创建流程?

以可移植的方式使用 DB Api

是否有一种可移植的方式在 Makefile 中制作可移植的可选依赖项?

以可移植数据格式保存/加载 scipy sparse csr_matrix

如果我以可移植性为目标,我应该对具有负值的字符使用带符号的字符吗?

是否使用 BYTE 可移植类型的变量?