Bitset 和字节序

Bitset 和字节序

近日,笔者希望通过 Bitset 来完成部分繁琐的 bit 级 io 操作,在测试后发现 Bitset 与自己所想有较大的落差.最为重要的问题在于 Bitset 会受到字节序的影响.

字节序

请在继续阅读本文前回想字节序的相关知识.一个多字节的对象将被存储在连续的内存中,但存储的顺序需要区分「小端序」与「大端序」.
例如:一个 4 字节的整形变量 0x12345678 以小端序存储在内存中的存储方法是 0x78 0x56 0x34 0x12(按照每个字节的地址递减顺序排列),而以大端序存储的方式是 0x12 0x34 0x56 0x78(按照每个字节的地址递减顺序排列).
换句话说:

  • 小端序:数据的低位字节存储在变量的高位地址
  • 大端序:数据的高位字节存储在变量的低位地址

日常中,广泛使用的 x86_64 架构采取小端序存储变量.

Bitset

Bitset 中(但不限)以下成员是笔者将在本文中着重讨论的:

  • 构造函数 constexpr bitset( unsigned long long val ) noexcept;
  • to_ulong()to_ullong()
  • to_string()

下标 与 to_ulong()to_ullong()

请在考虑这个问题的时候不要忽略字节序的问题.
在进行测试之前,笔者认为 bitset<64> 的下标将与一个 uint64_t 进行从高位至低位或者从低位至高位的依次对应.
bitset 的却按照字节序和二进制运算中进位顺序排列.
例如:
std::bitset<32> test(0x12345678); 在内存中存储的值实际上是 01111000 01010110 00110100 00010010,而且其为 true 的值对应的下标是 3、4、5、6、9、10、12、14、18、20、21、25、28.这与笔者的预期完全不符.也就是说 bitset 的编号顺序为:7、6、5、4、3、2、1、0、15、14、13、12、11、10、9、8、23、22、21、20、19、18、17、16、31、30、29、28、27、26、25、24、39、38、37、36、35、34、33、32、47、46、45、44、43、42、41、40、55、54、53、52、51、50、49、48、63、62、61、60、59、58、57、56……
依笔者之见,这种编号顺序极难满足通常的需求.

对于一个 uint64_t 而言,笔者将其最高位(即 uint64_t中表示 2^7 的 bit)记为第0位按照内存中的顺序依次编号即可通过 8 * (i / 8 * 2 + 1) - 1 - i 将其笔者对 bit 的编号 i 转换为 bitset 的正确下标.

下标与 to_string

回顾刚才的例子:

1
2
std::bitset<32> test(0x12345678);
std::cout << test << std::endl;

本例的输出为 00010010001101000101011001111000,可以看到输出并不是按照 bitset 的下标顺序输出,而是按照大端顺序输出.

测试环境

  • OS: Arch Linux
  • Kernel: 5.11.7
  • GCC: 10.2.0
  • clang: 11.1.0