inline 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
要便携地执行 LSB a[0](如下面的 pext/pdep 版本),而不是使用与主机字节序相反的方式,在两个版本中都使用 htole64(0x0102040810204080ULL) 作为魔法乘数。 (htole64 is from BSD / GNU <endian.h>)。这会安排乘数字节以匹配 bool 数组的 little-endian 顺序。 htobe64 具有相同的常数给出了另一个顺序,MSB 优先,就像您用于打印以 2 为底的数字一样。
您可能需要确保 bool 数组是 8 字节对齐 (alignas(8)) 以提高性能,并且编译器知道这一点。 memcpy 对于任何对齐总是安全的,但是在需要对齐的 ISA 上,如果编译器知道指针已充分对齐,则它只能将 memcpy 作为单个加载或存储指令内联。 *(uint64_t*)a 会承诺对齐,但也会违反严格混叠规则。即使在允许未对齐负载的 ISA 上,自然对齐时它们也可以更快。但是编译器仍然可以内联 memcpy 而不会在编译时看到该保证。
它们是如何工作的
假设我们有 8 个布尔值 b[0] 到 b[7],它们的最低有效位分别命名为 a-h,我们希望将它们打包成一个字节。将这 8 个连续的 bools 视为一个 64 位字并加载它们,我们将在 little-endian 机器中以相反的顺序获得这些位。现在我们将做一个乘法(这里的点是零位)
| 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
添加了箭头,以便更容易看到设置位在幻数中的位置。此时 8 个最低有效位已放入最高字节,我们只需将其余位屏蔽掉
所以包装的幻数是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 是 little-endian:a[0] = (b>>0) & 1; 在 memcpy 之后。)
不幸的是those instructions are very slow on AMD所以你可能需要和上面的乘法比较看看哪个更好