我有8个bool
变量,想要将它们"合并"成一个字节。
有没有简单/首选的方法可以做到这一点?
另一种方式如何,将一个字节解码为8个单独的布尔值?
我认为这不是一个不合理的问题,但由于我在Google上找不到相关的文档,可能是另一种那种"不行,你所有的直觉都是错的"情况。
我有8个bool
变量,想要将它们"合并"成一个字节。
有没有简单/首选的方法可以做到这一点?
另一种方式如何,将一个字节解码为8个单独的布尔值?
我认为这不是一个不合理的问题,但由于我在Google上找不到相关的文档,可能是另一种那种"不行,你所有的直觉都是错的"情况。
较为困难的方法:
unsigned char ToByte(bool b[8])
{
unsigned char c = 0;
for (int i=0; i < 8; ++i)
if (b[i])
c |= 1 << i;
return c;
}
而且:
void FromByte(unsigned char c, bool b[8])
{
for (int i=0; i < 8; ++i)
b[i] = (c & (1<<i)) != 0;
}
struct Bits
{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
};
union CBits
{
Bits bits;
unsigned char byte;
};
Bits
中位的顺序是实现定义的。memcpy
或 C++20 std::bit_cast
是在可移植的 C++ 中执行类型转换的安全方式。char
中的位域的位顺序是实现定义的,位域成员之间可能存在填充。)<<
。但是你可以混合两种解决方案。 - rodrigoinline uint8_t pack8bools(bool* a)
{
uint64_t t;
memcpy(&t, a, sizeof t); // strict-aliasing & alignment safe load
return 0x8040201008040201ULL*t >> 56;
// bit order: a[0]<<7 | a[1]<<6 | ... | a[7]<<0 on little-endian
// for a[0] => LSB, use 0x0102040810204080ULL on little-endian
}
void unpack8bools(uint8_t b, bool* a)
{
// on little-endian, a[0] = (b>>7) & 1 like printing order
auto MAGIC = 0x8040201008040201ULL; // for opposite order, byte-reverse this
auto MASK = 0x8080808080808080ULL;
uint64_t t = ((MAGIC*b) & MASK) >> 7;
memcpy(a, &t, sizeof t); // store 8 bytes without UB
}
sizeof(bool) == 1
。a [0]
(就像下面的pext / pdep
版本一样),而不是使用主机字节序的相反,可以在两个版本中都使用htole64(0x0102040810204080ULL)
作为魔法乘数。 (htole64
来自BSD / GNU<endian.h>)。这将排列乘数字节以匹配bool数组的小端顺序。使用相同常量的htobe64
给出另一个顺序,最先使用MSB,例如在二进制中打印数字时。alignas(8)
)以获得更好的性能,并且编译器知道这一点。memcpy
始终对任何对齐方式都安全,但在需要对齐的ISAs上,仅当编译器知道指针足够对齐时,才能将memcpy
内联为单个加载或存储指令。 *(uint64_t *)a
会承诺对齐,但也违反了严格别名规则。即使在允许不对齐加载的ISAs上,当自然对齐时它们可能更快。但编译器仍然可以在编译时内联memcpy而不看到该保证。
假设我们有8个布尔值 b[0]
到 b[7]
,它们的最低有效位分别命名为 a-h,我们想将它们打包成一个单字节。将这8个连续的 bool
视为一个64位字,并加载它们,我们将在小端机器上以相反的顺序得到位。现在我们将进行乘法运算(这里点表示零位)
| b7 || b6 || b4 || b4 || b3 || b2 || b1 || b0 |
.......h.......g.......f.......e.......d.......c.......b.......a
× 1000000001000000001000000001000000001000000001000000001000000001
────────────────────────────────────────────────────────────────
↑......h.↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑.....g..↑....f...↑...e....↑..d.....↑.c......↑b.......a
↑....f...↑...e....↑..d.....↑.c......↑b.......a
+ ↑...e....↑..d.....↑.c......↑b.......a
↑..d.....↑.c......↑b.......a
↑.c......↑b.......a
↑b.......a
a
────────────────────────────────────────────────────────────────
= abcdefghxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
0b1000000001000000001000000001000000001000000001000000001000000001
或0x8040201008040201
。如果您使用的是大端机器,则需要使用类似方式计算出的魔术数字0x0102040810204080
。 | b7 || b6 || b4 || b4 || b3 || b2 || b1 || b0 |
abcdefgh
× 1000000001000000001000000001000000001000000001000000001000000001
────────────────────────────────────────────────────────────────
= h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh
& 1000000010000000100000001000000010000000100000001000000010000000
────────────────────────────────────────────────────────────────
= h0000000g0000000f0000000e0000000d0000000c0000000b0000000a0000000
在乘法之后,我们需要将所需的位数放置在最高有效位上,因此我们需要屏蔽不相关的位并将其余位移动到最低有效位。输出将是包含a到h的字节的小端格式。
在具备BMI2的新型x86 CPU上,有PEXT和PDEP指令可用于此目的。上面的pack8bools
函数可以被替换为
_pext_u64(*((uint64_t*)a), 0x0101010101010101ULL);
而 unpack8bools
函数可以实现为
_pdep_u64(b, 0x0101010101010101ULL);
这将映射LSB -> LSB,就像0x0102040810204080ULL
乘数常量一样,相反的是0x8040201008040201ULL
。 x86是小端:memcpy
后,a [0] =(b>> 0)&1;
。
不幸的是在Zen 3之前,这些指令在AMD上非常慢,因此您可能需要与上面的乘法方法进行比较,以确定哪种方法更好。
x86 SIMD具有一个操作,可以获取向量寄存器中每个字节(或浮点数或双精度浮点数)的高位,并将其作为整数提供给您。 字节的指令是pmovmskb
。 如果您有大量需要处理的数据,则可以使用相同数量的指令处理16个字节,因此它比乘法技巧更好。
#include <immintrin.h>
inline uint8_t pack8bools_SSE2(const bool* a)
{
__m128i v = _mm_loadl_epi64( (const __m128i*)a ); // 8-byte load, despite the pointer type.
// __m128 v = _mm_cvtsi64_si128( uint64 ); // alternative if you already have an 8-byte integer
v = _mm_slli_epi32(v, 7); // low bit of each byte becomes the highest
return _mm_movemask_epi8(v);
}
pext
肯定是(除了在像Zen 3之前的AMD这样灾难性缓慢的CPU上)。to_ullong
和 unsigned long long
的构造函数无法使用 PEXT 和 PDEP,因为它们只是直接复制 unsigned long long
值到/从内部数组。bitset 中的位被存储为数组中的位而不是每个字节的一位。所以 to_ullong
提供了 64 位而不是 8 位。 - phuclv-fno-strict-aliasing
或使用联合体。或者只需将bool
更改为char
,因为char*
可以别名其他类型。 - phuclvuint64_t *
读取 char []
对象会违反严格别名规则。 ISO C 只允许使用 char*
读取最初是 uint64_t
的对象。 如果对 char 对象的唯一访问是通过 char *
,那么就可以,但如果有任何自动或静态存储 char[]
的使用,则会产生 UB。 此答案应该使用 memcpy
或 C++20 的 std::bitcast
,或者 GNU C 的 typedef uint64_t unaligned_aliasing_u64 __attribute__((aligned(1), may_alias));
来进行类型装换;指针转换永远不安全。 - Peter Cordesnew
或malloc
获取的一些内存的处理方式,那么对它的所有char
访问都将通过char*
进行,因此是别名安全的。但是,如果您在某个地方声明了一个真正的char array[8]
变量,则对它的访问可能不是通过char*
进行的。 - Peter Cordesstruct foo{ int a; char b[8];};
,那么你可以通过指向它的指针进行整个结构体的结构赋值,例如foo *p=..., *q=...;
和*p = *q
。在这种情况下,对char b[8]
的访问并不是通过char*
进行的,因此编译器肯定会假设某些uint64_t*
的加载或存储与其无关。(实际上,你可能需要这个结构体才能使UB真正发生,或者至少在实践中成为一个问题,而不仅仅是一个char arr[8]
局部变量:[]
访问通过衰减到char*来完成) - Peter Cordes#include <stdint.h> // to get the uint8_t type
uint8_t GetByteFromBools(const bool eightBools[8])
{
uint8_t ret = 0;
for (int i=0; i<8; i++) if (eightBools[i] == true) ret |= (1<<i);
return ret;
}
void DecodeByteIntoEightBools(uint8_t theByte, bool eightBools[8])
{
for (int i=0; i<8; i++) eightBools[i] = ((theByte & (1<<i)) != 0);
}
eightBools[i]
是一个 bool
类型,检查它时使用 == true
也可以直接写成 (eightBools[i] == true) == true
或者 ((eightBools[i] == true) == true) == true
,但是这种方式何时停止呢?是的,这并不值得点赞。 - Christian Raubool a,b,c,d,e,f,g,h;
//do stuff
char y= a<<7 | b<<6 | c<<5 | d<<4 | e <<3 | f<<2 | g<<1 | h;//merge
我想指出,在C++中,通过union
进行类型游戏是UB的(正如rodrigo在他的回答中所做的那样)。最安全的方法是使用memcpy()
struct Bits
{
unsigned b0:1, b1:1, b2:1, b3:1, b4:1, b5:1, b6:1, b7:1;
};
unsigned char toByte(Bits b){
unsigned char ret;
memcpy(&ret, &b, 1);
return ret;
}
正如其他人所说的那样,编译器足够聪明,可以优化掉memcpy()
。
顺便说一下,这就是Boost实现类型转换的方式。
无法将8个 bool
变量压缩成一个字节。 但是可以使用 位掩码 将8个逻辑 true/false 状态打包到单个字节中。
bool
对象,但所有8个值都在一个char
或uint8_t
的位中。当期望的行为很明确时,这个答案似乎没有帮助性和迂腐。 - Peter Cordesunsigned char toByte(bool *bools)
{
unsigned char byte = \0;
for(int i = 0; i < 8; ++i) byte |= ((unsigned char) bools[i]) << i;
return byte;
}
感谢Christian Rau的纠正s!
short
(可能是 1 个字节,但很可能是 2 个字节) 而不是 char
(保证为 1 个字节) 的原因是什么?并且你应该使用无符号类型并正确初始化 byte
。修复这些问题,答案更有可能是正确的。但我不是投票反对者,至少还不是。 - Christian Rauunsigned char byte = \0;
is invalid. Either use 0
or '\0'
- phuclvbool a,b,c,d,e,f,g,h; # assuming you want [a] at MSB and
# [h] at LSB
char y = a << 7 | b<< 6 | c << 5 | d << 4 | e << 3 | f << 2 | g << 1 | h
char y = h|(g|(f|(e|(d|(c|(b|a << 1)<< 1)<< 1)<< 1)<< 1)<< 1)<< 1
...而不是使用7个不同的移位量,嵌套形式允许反复使用相同的移位常数,
同时还有一个额外的好处,即将所有的布尔值/数字放在一边,将所有的移位量放在另一边
算术等价表达式如下(它们都是相同的)-
char y = h + (g + (f + (e + (d + (c + (b + a * 2) * 2) * 2) * 2) * 2) * 2) * 2
char y = 2 * (2 * (2 * (2 * (2 * (2 * (2 * a + b) + c) + d) + e) + f) + g) + h
char y = 2 * ( 2 * ( 2 * ( 2 * ( 2 * ( 2 * ( 2 * (\
a ) + b ) + c ) + d ) + e ) + f ) + g ) + h