C/C++比特字段和位运算符哪个更快、更好、更易于移植,用于单独提取位?

15

我需要以以下方式将一些比特打包到一个字节中:

struct  
{  
  char bit0: 1;  
  char bit1: 1;  
} a;  

if( a.bit1 ) /* etc */

或者:

if( a & 0x2 ) /* etc */
从源代码的清晰度来看,对我来说位域更加整洁。但哪个选项更快?我知道速度差异不会太大甚至没有,但如果可以使用任何一个选项,如果有一个更快,那就更好。
另一方面,我已经了解到,位域不能保证在各个平台上以相同的顺序排列位,而我希望我的代码具有可移植性。
注:如果你打算回答“分析”,没问题,但我很懒,如果已经有人有答案了,那就更好了。代码可能有误,如果你想,你可以纠正我,但请记住这个问题的重点,并请尽量回答它。

你可能会对https://dev59.com/AHNA5IYBdhLWcg3wPLL-感兴趣。我记得至少还有另一个非常相似的问题,我在那里发表了评论(但这似乎不是那个问题)。 - Pascal Cuoq
位域:你被淘汰了!感谢大家。 - Petruza
哇,我们在这里教了一个宝贵的课程。 - Richard Pennington
7个回答

13

如果适当使用,位域可使代码更清晰。我只会将位域作为节省空间的工具使用。我经常见到它们在编译器中使用:通常类型或符号信息由一组真/假标志组成。位域在此处非常理想,因为一个典型的程序在编译时会创建数千个这些节点。

对于常见的嵌入式编程任务——读写设备寄存器,我不会使用位域。我更喜欢在这里使用移位和掩码,因为您可以获得文档所需的确切位,并且不必担心各种编译器实现之间的差异。

至于速度,良好的编译器将为位域提供与掩码相同的代码。


1
使用位域的问题在于您无法保证它会节省任何空间。编译器可能会为每个位域分配32位,就像它是int一样。为什么不这样做呢?在大多数情况下,这样会更快。 - jalf
对。但它可以节省空间。许多聪明人都依靠位域来实现这一点。 - Richard Pennington
1
@Niel:如果你试图依赖于位域的排序,那么它们是不可移植的,正如我一直所说的那样。它们本身并不是不可移植的。 - Richard Pennington
1
@Neil:这不是C标准所说的。 "实现可以分配任何可寻址的存储单元,足够大以容纳位域。如果有足够的空间,紧随结构中另一个非位域之后的位域应打包到同一单元的相邻位中。" - Richard Pennington
2
@Jalf:你错了。标准规定,“存储在位域中的值由 m 位组成,其中 m 是指定的位域大小。” 实现不允许忽略 m 值。 - Richard Pennington
显示剩余10条评论

8
我更倾向于使用第二个示例,因为它具有最大的可移植性。正如Neil Butterworth所指出的那样,使用位域只适用于本地处理器。好吧,想想看,如果英特尔的x86明天倒闭了,代码就会被卡住,这意味着必须为另一个处理器(比如RISC芯片)重新实现位域。
你必须考虑更大的问题,并问OpenBSD如何使用一个代码库将其BSD系统移植到许多平台上?好吧,我承认这有点过头了,是有争议和主观的,但从现实角度来看,如果你想将代码移植到另一个平台,使用你在问题中使用的第二个示例是正确的方法。
不仅如此,不同平台的编译器会有自己的填充方式,对齐位域以适应编译器所在的处理器。而且,处理器的字节序又怎么办?
永远不要依赖位域作为一个神奇的解决方案。如果你想为处理器提供速度并且不打算移植,那么请随意使用位域。你不能两者兼得!

1
+1:问题不仅在于你没有移植的意图,而且在于你没有打算在任何情况下将变量的整个内容作为整数(而非单独的位)使用,除了在一个处理器的内存中。如果你将位域结构的整数值发送到磁盘、网络或某些嵌入式系统外设,你必须确保编译器按正确的顺序放置位域。 - Jason S
7
请问您能否解释一下,使用内置位域比手动定义的位域更具可移植性的原因?如果您依赖于结构体的内存布局被设置为特定的方式,我可以相信这一点,但是如果您只是通过内置语法访问每个字段,我看不出任何可移植性问题。 - Thomas Eding
父级错误,这不是CPU可移植性问题,而是编译器可移植性问题。每个编译器都可以自由地对齐位域,这意味着使用两个不同编译器编译的代码无法正确链接到彼此并共享包含位域的数据。自己进行掩码和移位可以消除这个问题。 - naasking
1
抱歉,但这个答案是无意义的 - 首先x86不会在一夜之间“破产”,突然间数百万台笔记本电脑和处理器就会消失。其次,位域是语言的一部分,所以代码不会“卡住”,只需为新处理器重新编译即可。如果您谈论大/小端序,那么这是一个序列化问题,您还需要注意使用char、short和许多其他数据类型,因此与位域具体相关的内容很少。而且高效地实现RISC处理器等是编译器供应商的工作。 - QuadrupleA

6

C语言位段从诞生之初就注定要失败了——原因不明。人们不喜欢它们,而是使用位运算符。您必须预计其他开发人员不理解C语言位段代码。

至于哪个更快:无关紧要。任何优化编译器(这意味着实际上全部)都会使代码在任何符号中执行相同的操作。C程序员常有一个误解,即编译器只是在源代码中搜索和替换关键字到汇编代码中。现代编译器会将源代码作为实现目标的蓝图,然后生成看起来非常不同但实现了预期结果的代码。


5
如果您想要可移植性,请避免使用位域(bitfields)。如果您对特定代码的性能感兴趣,编写自己的测试是无可替代的。请记住,位域在底层将使用处理器的位运算指令来执行。

3
还存在打包问题。而且仅仅因为处理器有特殊的指令并不意味着编译器会使用它,这就是我说测试很重要的原因。 - anon
2
啊,但是一个位域可能会根据实现使用更多或更少的空间,并不意味着它是不可移植的。如果编译器没有利用处理器的功能,那么应该进行改进或替换。 - Richard Pennington
@Richard:如果那些指令没有使用的价值怎么办?例如,x86指令集中充斥着AMD和Intel优化手册中只用“为了获得最佳性能,请不要使用这些指令”的指令。关于位域(bitfields)的几乎所有内容都是实现定义的,所以依赖它们在不同平台上具有相同的布局几乎是自杀式的。 - jalf
取决于我们所讨论的可移植性类型。当然,如果您在另一个平台上编译和运行代码,它将正常工作,但是在C++中使用位字段的结构的布局不能保证在不同平台之间一致。也不能保证节省任何空间。 - jalf
2
一些编译器可以识别位域操作并使用处理器的位域操作。我知道GCC在AVR上做到了这一点,以至于AVRlibc的人们只提供用汇编实现的位域函数作为对遗留代码的支持,并建议新代码使用REG |= 0x08;REG &= ~0x08;(例如)构造。这种编译器优化类似于编译器通过位移替换整数乘法和除法的2的幂。 - Mike DeSimone
显示剩余3条评论

5
第一个是明确的,无论速度如何,第二个表达式容易出错,因为对结构体的任何更改都可能使第二个表达式错误。所以使用第一个。

1
不。第一个选项使用了大多数程序员从未接触过且不理解的不寻常特性,而第二个选项则是纯粹的惯用C语言。因此,第二个选项更清晰地表达了其意图,并且不太可能被粗心的维护人员破坏。 - Porculus
1
第二个表达式唯一的问题是0x02需要被定义为常量,这样开发人员才知道它的含义。第一个表达式的问题在于编译器可以将两个位放置在8位字节的任何位置。Linux内核定义了一整套常量来声明位顺序和打包方式,这导致了难以维护的#ifdef块。而且这还假设平台将char定义为8位;我曾经为将char定义为16位的平台开发过应用程序,因为DSP无法寻址字节,只能寻址字。 - Mike DeSimone

2

我认为C程序员会倾向于使用位掩码和逻辑运算来推断每个位的值,而不是让代码充斥着十六进制值。通常会设置枚举或宏来获取/设置特定位,尤其是涉及更复杂操作时。据我所知,使用结构体实现的位域比较慢。


更重要的是,位域在不同平台上的顺序不能保证一致。因此,如果您在硬件寄存器中使用位域,您将会非常困难地调试为什么您的硬件没有按照预期工作。 - Sam Post

1

不要过于关注“非可移植位域”的细节。位域有两个方面是实现定义的:符号和布局,还有一个未指定的方面:它们被打包到的分配单元的对齐方式。如果你只需要打包效果,使用它们就像函数调用一样具有可移植性(前提是在必要时明确指定了signed关键字)。

关于性能,剖析是你能得到的最好答案。在完美的世界里,这两种写法之间没有区别。但在实践中,可能会有一些差异,但我可以想到同样多的理由支持两种写法。而且它可能非常敏感于上下文(例如无意义的逻辑差异,如无符号和有符号之间的差异),因此需要在上下文中进行测量...

总之,区别在于当你真正有选择的时候(即不重要的精确布局),这主要是一种风格上的差异。在这些情况下,它是一种优化(在大小方面,而不是速度方面),因此我首先会编写没有它的代码,并在需要时添加它。因此,位域是显而易见的选择(要做的修改最小,以实现结果,并且仅包含在定义的唯一位置中,而不是分散到所有使用的地方)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接