与std :: bitset相比,c风格的位操作有什么优势吗?


16

我几乎只在C ++ 11/14中工作,通常在看到这样的代码时会畏缩:

std::int64_t mArray;
mArray |= someMask << 1;

这只是一个例子。我说的是一般的按位操作。在C ++中,真的有什么意义吗?上面的内容容易引起误解且容易出错,而使用std::bitset可以使您:

  1. std::bitset通过调整模板参数并让实现负责其余工作,可以更轻松地根据需要修改的大小,以及
  2. 花更少的时间弄清楚正在发生的事情(并可能犯错误),并std::bitset以类似于std::array或其他数据容器的方式编写。

我的问题是;除了向后兼容以外,还有什么理由使用std::bitset原始类型吗?


a的大小std::bitset在编译时固定。这是我能想到的唯一严重的缺点。
rwong

1
@rwong我在谈论std::bitsetvs c风格的位操作(例如int),它也固定在编译时。
2015年

原因可能是遗留代码:该代码是在std::bitset不可用(或作者不知道)的时候编写的,没有理由重写要使用的代码std::bitset
Bart van Ingen Schenau,2015年

我个人认为,如何使每个人都容易理解“对二进制变量的集合/映射/数组进行的操作”的问题在很大程度上仍未解决,因为在实践中有许多操作不能简化为简单的操作。表示这样的集合的方法也太多了,其中bitset一个是一个,但是较小的向量或ints 集(具有位索引)也可能是合法的。C / C ++的原理并没有向程序员隐藏这些选择的复杂性。
rwong 2015年

Answers:


12

从逻辑(非技术)角度来看,没有优势。

任何普通的C / C ++代码都可以包装在适当的“库构造”中。在这样的包装之后,“这是否比它更具优势”的问题成为一个有争议的问题。

从速度的角度来看,C / C ++应该允许库构造生成与其包装的普通代码一样有效的代码。但是,这要遵守:

  • 功能内联
  • 编译时检查并消除不必要的运行时检查
  • 消除死代码
  • 许多其他代码优化...

使用这种非技术性的论据,任何人都可以添加任何“缺失功能”,因此不算作不利条件。

但是,内置的要求和限制无法通过其他代码来克服。下面,我认为的大小std::bitset是一个编译时常量,因此尽管不算不利,但它仍然会影响用户的选择。


从美学的角度(可读性,易于维护等)来看,存在差异。

但是,尚不明显std::bitset代码立即胜过普通C代码。人们必须查看更大的代码段(而不是一些玩具样本)来说明是否使用std::bitset可以提高源代码的人工质量。


位操作的速度取决于编码样式。编码样式会影响C / C ++的位操作,并且同样适用于编码样式std::bitset,如下所述。


如果编写的代码一次使用operator []来读取和写入一位,那么如果要操纵的位超过一个,则必须多次执行此操作。C风格的代码也是如此。

但是,bitset也有其他的运营商,诸如operator &=operator <<=等,这对位集的整个宽度进行操作。因为底层机器通常可以一次(在相同的CPU周期数内)一次在32位,64位,有时甚至在128位(使用SIMD)上运行,所以设计用于利用这种多位操作的代码可以比“循环”位处理代码更快。

一般概念称为SWAR(寄存器内的SIMD),是位操作下的子主题。


一些C ++供应商可能bitset使用SIMD 实现64位和128位之间的转换。一些供应商可能不会(但最终可能会)。如果需要知道C ++供应商的库在做什么,那么唯一的方法就是查看反汇编。


至于是否std::bitset有局限性,我可以举两个例子。

  1. 的大小std::bitset必须在编译时知道。要制作具有动态选择大小的位数组,必须使用std::vector<bool>
  2. 当前的C ++规范std::bitset没有提供从bitsetM位中的较大位提取N位连续切片的方法。

第一个是基本的,这意味着对于需要动态大小的位集的人,他们必须选择其他选项。

可以克服第二个问题,因为即使标准不可扩展,也可以编写某种适配器来执行任务bitset


某些现成的高级SWAR操作未从中直接提供std::bitset。可以在本网站上阅读有关位排列的这些操作。像往常一样,可以在之上运行自己实现这些功能std::bitset


关于性能的讨论。

一个告诫:很多人问为什么(东西)从标准库比一些简单的C风格的代码要慢得多。在这里,我不会重复进行微基准测试的先决知识,但是我只有以下建议:确保在“发布模式”(启用优化)中进行基准测试,并确保未消除代码(消除死代码)吊离循环(循环不变代码运动)

由于一般而言,我们无法分辨(互联网上)某人是否在正确地进行微基准测试,因此,我们可以得出可靠结论的唯一方法是自己做微基准测试,记录详细信息,并提交给公众审查和批评。重做其他人以前做过的微基准测试并没有什么坏处。


问题2还意味着在任何并行设置中,每个线程都应在该位集的子集上工作,则不能在任何并行设置中使用该位集。
user239558 '19

@ user239558我怀疑有人想并行化同一对象std::bitset。没有内存一致性保证(中的std::bitset),这意味着不应在内核之间共享它。需要跨内核共享它的人将倾向于构建自己的实现。当不同内核之间共享数据时,习惯上将它们与缓存行边界对齐。不这样做会降低性能,并暴露出更多非原子性陷阱。我没有足够的知识来概述如何构造的可并行实现std::bitset
rwong

数据并行编程通常不需要任何内存一致性。您只能在阶段之间进行同步。我绝对想并行处理位集,我想任何人都bitset愿意。
user239558 '19

听起来像@ user239558意味着要进行复制(在处理开始之前,要复制每个内核要处理的相关位集范围)。我同意这一点,尽管我认为任何考虑并行化的人都已经考虑过推出自己的实现。通常,提供了许多C ++标准库工具作为基线实现。任何有更严重需求的人都将自己实施。
rwong

不,没有复制。它只是访问静态数据结构的不同部分。则不需要同步。
user239558 '19

2

当然,这并非在所有情况下都适用,但是有时算法可能取决于C样式位纠缠的效率来提供显着的性能提升。这在我脑海的第一个例子是使用位棋盘,棋盘游戏位置的巧妙整数编码,以加快国际象棋引擎等。在这里,整数类型的固定大小没有问题,因为棋盘总是8 * 8。

举一个简单的例子,考虑以下功能(本杰克逊(Ben Jackson)的回答),该功能测试四通的获胜位置:

// return whether newboard includes a win
bool haswon2(uint64_t newboard)
{
    uint64_t y = newboard & (newboard >> 6);
    uint64_t z = newboard & (newboard >> 7);
    uint64_t w = newboard & (newboard >> 8);
    uint64_t x = newboard & (newboard >> 1);
    return (y & (y >> 2 * 6)) | // check \ diagonal
           (z & (z >> 2 * 7)) | // check horizontal -
           (w & (w >> 2 * 8)) | // check / diagonal
           (x & (x >> 2));      // check vertical |
}

2
您认为std::bitset会变慢吗?
2015年

好吧,从源头看一眼,libc ++位集是基于单个size_t或它们的数组,因此可能会编译成本质上等效/相同的东西,尤其是在sizeof(size_t)== 8的系统上-所以不,它可能不会变慢。
Ryan Pavlik
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.