我有点好奇创建一个宏来为设备寄存器生成位掩码,最高可达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_MASK
和BIT_FIELD_MASK
从0指数(0-31)开始运作。 BIT_FIELD_MASK
是从一个比特范围创建一个掩码,即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位会导致 实现定义 未定义的行为。以下是三种不同的转换实现:
x << 32 == x
。x << 32 == 0
但x << 64 == x
。x << y == 0
为所有y >= 32
。但是,如果将32位操作数移到32位或更多位置,编译器可以自由地做任何他们想做的事情,并且它们甚至可以自由地表现不一致(或者让恶魔飞出你的鼻子)。
实现BIT_FIELD_MASK:
只要a
和b
,这将通过位0 <= a <= 31
(包括)设置位0 <= b <= 31
。
#define BIT_MASK(a, b) (((unsigned) -1 >> (31 - (b))) & ~((1U << (a)) - 1))
移动大于或等于整数类型的大小是未定义的行为。 所以不,这不是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?
假设你有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)
关于什么:
#define BIT_MASK(n) (~(((~0ULL) >> (n)) << (n)))
这适用于所有endianess系统,执行-1反转所有位不适用于big-endian系统。
#define BIT_MASK(foo) ((~ 0ULL) >> (64-foo))
我对此有点偏执。我认为这假设unsigned long long
正好是64位。但这是一个开始,它可以达到64位。
也许这是正确的:
define BIT_MASK(foo) ((~ 0ULL) >> (sizeof(0ULL)*8-foo))
因为你需要避免移动与类型中的位数一样多的位(无论是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>
中定义。
对于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为零,则没有数据,因此代码无论如何都不应该执行任何操作。
@ iva2k的答案避免分支,并且当长度为64位时是正确的。在此基础上,你也可以这样做:
#define BIT_MASK(length) ~(((unsigned long long) -2) << length - 1);
不过,gcc无论如何都会产生完全相同的代码。