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

时间:2015-08-07 14:14:50

标签: c bit-manipulation

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

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

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

编辑:

回应评论:

  

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

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

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

9 个答案:

答案 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整数相乘。当s0时,什么也没有发生,并且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)

一种可能的方法是首先执行无符号右移,然后根据最高有效位的值对移位值进行符号扩展。使用以下事实:当添加两个位ab时,总和位为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);
}