C宏创建一个掩码 - 可能吗?我找到了一个GCC错误吗?

问题描述 投票:14回答:8

我有点好奇创建一个宏来为设备寄存器生成位掩码,最高可达64位。这样BIT_MASK(31)生产0xffffffff

然而,几个C的例子并没有像想象的那样工作,因为我得到了0x7fffffff。这是因为编译器假设我想要签名输出,而不是无符号输出。所以我尝试了32,并注意到该值回绕到0.这是因为C标准声明如果移位值大于或等于要移位的操作数中的位数,则结果是未定义的。那讲得通。

但是,鉴于以下计划,bits2.c

#include <stdio.h>

#define BIT_MASK(foo) ((unsigned int)(1 << foo) - 1)

int main()
{
    unsigned int foo;
    char *s = "32";

    foo = atoi(s);
    printf("%d %.8x\n", foo, BIT_MASK(foo));

    foo = 32;
    printf("%d %.8x\n", foo, BIT_MASK(foo));

    return (0);
}

如果我用gcc -O2 bits2.c -o bits2编译,并在Linux / x86_64机器上运行它,我会得到以下结果:

32 00000000
32 ffffffff

如果我使用相同的代码并在Linux / MIPS(big-endian)机器上编译它,我得到这个:

32 00000000
32 00000000

在x86_64机器上,如果我使用gcc -O0 bits2.c -o bits2,那么我得到:

32 00000000
32 00000000

如果我将BIT_MASK调整为((unsigned int)(1UL << foo) - 1),那么无论gcc的优化级别如何,两种形式的输出都是32 00000000

因此,似乎在x86_64上,gcc正在优化某些内容,或者32位数字上左移32位的未定义特性由每个平台的硬件决定。

考虑到上述所有情况,是否可以以编程方式创建一个C宏,从单个位或位范围创建位掩码?

即:

BIT_MASK(6) = 0x40
BIT_FIELD_MASK(8, 12) = 0x1f00

假设BIT_MASKBIT_FIELD_MASK从0指数(0-31)开始运作。 BIT_FIELD_MASK是从一个比特范围创建一个掩码,即8:12

c bit-manipulation
8个回答
12
投票

这是宏的一个版本,可用于任意正输入。 (负输入仍然会调用未定义的行为......)

#include <limits.h>
/* A mask with x least-significant bits set, possibly 0 or >=32 */
#define BIT_MASK(x) \
    (((x) >= sizeof(unsigned) * CHAR_BIT) ?
        (unsigned) -1 : (1U << (x)) - 1)

当然,这是一个有点危险的宏,因为它两次评估它的论点。如果您使用GCC或目标C99,这是使用static inline的好机会。

static inline unsigned bit_mask(int x)
{
    return (x >= sizeof(unsigned) * CHAR_BIT) ?
        (unsigned) -1 : (1U << x) - 1;
}

正如Mysticial所指出的那样,使用32位整数移位超过32位会导致 实现定义 未定义的行为。以下是三种不同的转换实现:

  • 在x86上,只检查移位量的低5位,所以x << 32 == x
  • 在PowerPC上,只检查移位量的低6位,所以x << 32 == 0x << 64 == x
  • 在Cell SPU上,检查所有位,所以x << y == 0为所有y >= 32

但是,如果将32位操作数移到32位或更多位置,编译器可以自由地做任何他们想做的事情,并且它们甚至可以自由地表现不一致(或者让恶魔飞出你的鼻子)。

实现BIT_FIELD_MASK:

只要ab,这将通过位0 <= a <= 31(包括)设置位0 <= b <= 31

#define BIT_MASK(a, b) (((unsigned) -1 >> (31 - (b))) & ~((1U << (a)) - 1))

8
投票

移动大于或等于整数类型的大小是未定义的行为。 所以不,这不是GCC的错误。

在这种情况下,文字1的类型为int,在您使用的两个系统中都是32位。因此,移动32将调用此未定义的行为。


在第一种情况下,编译器无法将移位量解析为32.因此它可能只发出正常的移位指令。 (在x86中只使用底部的5位)所以你得到:

(unsigned int)(1 << 0) - 1

这是零。

在第二种情况下,GCC能够将移位量解析为32.由于它是未定义的行为,它(显然)只是将整个结果替换为0:

(unsigned int)(0) - 1

所以你得到ffffffff


因此,这是GCC使用未定义行为作为优化机会的情况。 (虽然我个人而言,我更愿意发出警告。)

相关:Why does integer overflow on x86 with GCC cause an infinite loop?


2
投票

假设你有n位的工作掩码,例如

// set the first n bits to 1, rest to 0
#define BITMASK1(n) ((1ULL << (n)) - 1ULL)

您可以通过再次移动来制作范围位掩码:

// set bits [k+1, n] to 1, rest to 0
#define BITNASK(n, k) ((BITMASK(n) >> k) << k)

在任何情况下,结果的类型都是unsigned long long int

如上所述,BITMASK1是UB,除非n很小。通用版本需要条件并且两次评估参数:

#define BITMASK1(n) (((n) < sizeof(1ULL) * CHAR_BIT ? (1ULL << (n)) : 0) - 1ULL)

1
投票

关于什么:

#define BIT_MASK(n) (~(((~0ULL) >> (n)) << (n)))

这适用于所有endianess系统,执行-1反转所有位不适用于big-endian系统。


0
投票
#define BIT_MASK(foo) ((~ 0ULL) >> (64-foo))

我对此有点偏执。我认为这假设unsigned long long正好是64位。但这是一个开始,它可以达到64位。

也许这是正确的:

define BIT_MASK(foo) ((~ 0ULL) >> (sizeof(0ULL)*8-foo))

0
投票

因为你需要避免移动与类型中的位数一样多的位(无论是unsigned long还是unsigned long long),在处理类型的全宽时,你必须更加狡猾。一种方法是偷偷摸摸:

#define BIT_MASK(n) (((n) == CHAR_BIT * sizeof(unsigned long long)) ? \
                         ((((1ULL << (n-1)) - 1) << 1) | 1) : \
                           ((1ULL << (n  )) - 1))

对于n这样的常量64,编译器会计算表达式并仅生成所使用的大小写。对于运行时变量n,如果n大于unsigned long long中的位数(或者是负数),则会像以前一样严重失败,但是对于n范围内的0..(CHAR_BIT * sizeof(unsigned long long))值,可以正常工作而不会溢出。

请注意,CHAR_BIT<limits.h>中定义。


0
投票

对于n = 8 * sizeof(1ul),“传统”公式(1ul<<n)-1在不同的编译器/处理器上具有不同的行为。最常见的是溢出n = 32。任何添加的条件将多次评估n。去64位(1ull<<n)-1是一个选项,但问题迁移到n = 64。

我的首选方案是:

#define BIT_MASK(n) (~( ((~0ull) << ((n)-1)) << 1 ))

n = 64时不会溢出,只评估n一次。

如果n是变量,它将编译为2个LSH指令。 n也不能为0(结果将是编译器/处理器特定的),但对于我所拥有的所有用途(*)来说,它是一种罕见的可能性,并且可以通过仅在必要时添加防护“if”语句来处理(甚至更好地“断言”以检查上边界和下边界。


(*) - 通常数据来自文件或管道,大小以字节为单位。如果size为零,则没有数据,因此代码无论如何都不应该执行任何操作。


-2
投票

@ iva2k的答案避免分支,并且当长度为64位时是正确的。在此基础上,你也可以这样做:

#define BIT_MASK(length) ~(((unsigned long long) -2) << length - 1);

不过,gcc无论如何都会产生完全相同的代码。

© www.soinside.com 2019 - 2024. All rights reserved.