C中的位掩码
Posted
技术标签:
【中文标题】C中的位掩码【英文标题】:Bit mask in C 【发布时间】:2010-09-23 21:48:17 【问题描述】:用m
设置位前面是k
未设置位,然后是n
未设置位,在C中构造位掩码的最佳方法是什么:
00..0 11..1 00..0
k m n
例如,k=1、m=4、n=3 会产生位掩码:
01111000
【问题讨论】:
对于许多诸如此类的小技巧的答案,一个非常好的在线资源是Bit Twiddling Hacks。 通常,位掩码宏定义在 inclusive 位索引上,例如#define BITS(p,q) ...
其中 p = m + n - 1 和 q = n, p >= q
Hacker's Delight 更全面(1.8 千页)而且很棒。
@grigy 我真的不明白为什么你需要在这里有k
。仅使用 m
和 n
指定要设置的位范围更容易。
【参考方案1】:
虽然最佳答案简单有效,但它们没有为n=0
和m=31
的情况设置 MSB:
~(~0 << 31) << 0
= 0111 1111 1111 1111 1111 1111 1111 1111
((1 << 31)-1) << 0
= 0111 1111 1111 1111 1111 1111 1111 1111
我对 32 位无符号字的建议如下所示:
unsigned int create_mask(unsigned int n,unsigned int m)
// 0 <= start_bit, end_bit <= 31
assert(n >=0 && m<=31);
return (m - n == 31 ? ~0: ((1 << (m-n)+1)-1) << n);
这实际上得到了[m,n]
(闭合区间)范围内的位,因此create_mask(0,0)
将返回第一个位(位0)的掩码,create_mask(4,6)
返回位4 到6 的掩码,即... 00111 0000
.
【讨论】:
【参考方案2】:(仅限)对于在支持 BMI2 的 x86 系统(Intel Haswell 或更新版本、AMD Excavator 或更新版本)上更高效的解决方案感兴趣的人:
mask = _bzhi_u32(-1,m)<<n;
bzhi
指令将从指定位位置开始的高位归零。
_bzhi_u32
内部函数编译为此指令。测试代码:
#include <stdio.h>
#include <x86intrin.h>
/* gcc -O3 -Wall -m64 -march=haswell bitmsk_mn.c */
unsigned int bitmsk(unsigned int m, unsigned int n)
return _bzhi_u32(-1,m)<<n;
int main()
int k = bitmsk(7,13);
printf("k= %08X\n",k);
return 0;
输出:
$./a.out
k= 000FE000
代码片段_bzhi_u32(-1,m)<<n
编译成三条指令
movl $-1, %edx
bzhi %edi, %edx, %edi
shlx %esi, %edi, %eax
比@Jonathan Leffler 的代码少一条指令
和@Darius Bacon。
在 Intel Haswell 处理器或更新的处理器上,bzhi
和 shlx
都有 1 个周期的延迟和
每个周期的吞吐量为 2。在 AMD Ryzen 上,这两条指令的吞吐量甚至达到了每周期 4 条。
【讨论】:
【参考方案3】:我喜欢这两种解决方案。这是我想到的另一种方式(可能不是更好)。
((~((unsigned int)0) << k) >> (k + n)) << n
编辑:
我以前的版本中有一个错误(它没有 unsigned int 强制转换)。问题是~0 >> n
在前面加了1而不是0。
是的,这种方法有一个很大的缺点。它假定您知道默认整数类型的位数,或者换句话说,它假定您确实知道 k,而其他解决方案与 k 无关。这使得我的版本不那么便携,或者至少更难移植。 (它还使用了 3 次移位,以及加法和按位否定运算符,这是两个额外的操作。)
因此,您最好使用其他示例之一。
这是一个由 Jonathan Leffler 完成的小测试应用程序,用于比较和验证不同解决方案的输出:
#include <stdio.h>
#include <limits.h>
enum ULONG_BITS = (sizeof(unsigned long) * CHAR_BIT) ;
static unsigned long set_mask_1(int k, int m, int n)
return ~(~0 << m) << n;
static unsigned long set_mask_2(int k, int m, int n)
return ((1 << m) - 1) << n;
static unsigned long set_mask_3(int k, int m, int n)
return ((~((unsigned long)0) << k) >> (k + n)) << n;
static int test_cases[][2] =
1, 0 ,
1, 1 ,
1, 2 ,
1, 3 ,
2, 1 ,
2, 2 ,
2, 3 ,
3, 4 ,
3, 5 ,
;
int main(void)
size_t i;
for (i = 0; i < 9; i++)
int m = test_cases[i][0];
int n = test_cases[i][1];
int k = ULONG_BITS - (m + n);
printf("%d/%d/%d = 0x%08lX = 0x%08lX = 0x%08lX\n", k, m, n,
set_mask_1(k, m, n),
set_mask_2(k, m, n),
set_mask_3(k, m, n));
return 0;
【讨论】:
假设这个答案可以工作,与其他两个相比明显的缺点是存在第三班操作,这使得它更耗时。 另一个问题是它使用了其他两个解决方案可以忽略的参数k(虽然它不使用m,所以它仍然只使用三个参数中的两个)。 确实有一个错误,我现在修复了它并添加了其他解决方案更可取的评论。我还没有完全删除它,也许有人可以从我的错误中吸取教训,丢失你漂亮的测试代码会很遗憾:)。 您应该能够使用“0U”来表示无符号零,或者使用“0UL”来表示无符号长整数,而不是强制转换。我同意保留您的答案 - 以及您所做的编辑。 将此设为宏或内联函数,编译器将在编译时生成常量而不是代码。【参考方案4】:所以,您要求 m 个设置位,以 k 个复位位为前缀,然后是 n 个复位位?我们可以忽略 k,因为它在很大程度上会受到整数类型选择的限制。
mask = ((1 << m) - 1) << n;
【讨论】:
它们都有效,但我发现乔纳森的答案更简单、更清晰。大流士的回答对我来说有点太倒退了。 Robert,我喜欢位掩码的 ~0 习语,因为它不依赖于 2 的补码,并且在这个意义上更简单,但它确实不太为人所知。尽我所能来改变它! @Darius:如果您使用无符号算术/类型,就像在这些情况下应该使用的那样,2 的补码、1 的补码和符号幅度算术之间的区别是否无关紧要? @Darius,你不应该首先对有符号类型执行按位算术,如果你这样做了,你的解决方案每次都会调用未定义的行为! 是否未定义?我手头没有规范,但我认为它是实现定义的,即允许编译器按照他的意愿进行操作,但他必须始终以相同的方式进行操作。因此,当您知道(编译器的)处理方式时,您可以依赖它。【参考方案5】:~(~0
【讨论】:
这很漂亮。但是,最好对这一行进行注释,以便 -next- 程序员处理它。 如果这被编码为一个函数(@quinmar 的答案中的 set_mask_n 函数),会有一行注释说明函数的作用(并且没有参数“k”),并且用户该函数的名称将作为文档。作为一段代码中的随机表达式,它无疑是坏的! 而且,我会加快(非常缓慢地)添加,如果它在一些代码中显示为未注释的表达式,我的解决方案将同样难以理解。~(~0 << m)
位于 Brian Kernighan 和 Dennis Ritchie 的“C 编程语言,第二版”的第 2.9 段“位运算符”中。它也在 Brian W. Kernighan 和 Rob Pike 的“编程实践”的第 7.5 段“空间效率”中。
这种方法不能创建一个包含最长无符号整数类型的最高位的掩码,即通常用integer overflow in preprocessor expression
之类的警告表示。以上是关于C中的位掩码的主要内容,如果未能解决你的问题,请参考以下文章