将 32 个 0/1 值打包到单个 32 位变量的位中的最快方法是什么?

What's the fastest way to pack 32 0/1 values into the bits of a single 32-bit variable?

本文关键字:变量 方法 是什么 单个      更新时间:2023-10-16

我正在x86或x86_64机器上工作。我有一个数组unsigned int a[32]其所有元素的值均为 0 或 1。我想设置单个变量unsigned int b以便(b >> i) & 1 == a[i]a的所有 32 个元素都成立。我正在Linux上使用GCC(我猜应该无关紧要(。

在 C 语言中执行此操作的最快方法是什么?

在最近的x86处理器上,最快的方法可能是利用MOVMSKB系列指令,该指令提取SIMD字的MSB并将它们打包到普通的整数寄存器中。

我担心 SIMD 内在函数并不是我的菜,但如果你有一个配备 AVX2 的处理器,那么这些东西应该可以工作:

uint32_t bitpack(const bool array[32]) {
    __mm256i tmp = _mm256_loadu_si256((const __mm256i *) array);
    tmp = _mm256_cmpgt_epi8(tmp, _mm256_setzero_si256());
    return _mm256_movemask_epi8(tmp);
}

假设sizeof(bool) = 1.对于较旧的 SSE2 系统,您必须将一对 128 位操作串在一起。在 32 字节边界上对齐数组,应该可以节省另一个周期左右。

如果sizeof(bool) == 1,那么您可以使用此处讨论的技术将一次 8 bool 打包为 8 位(更多是 128 位乘法(,在具有像这样的快速乘法的计算机中

inline int pack8b(bool* a)
{
    uint64_t t = *((uint64_t*)a);
    return (0x8040201008040201*t >> 56) & 0xFF;
}
int pack32b(bool* a)
{
    return (pack8b(a +  0) << 24) | (pack8b(a +  8) << 16) |
           (pack8b(a + 16) <<  8) | (pack8b(a + 24) <<  0);
}
<小时 />

解释:

假设布尔值

a[0] a[7]的最小有效位分别命名为 a-h。将这 8 个连续的 bool s 视为一个 64 位字并加载它们,我们将在小端机器中以相反的顺序获得这些位。现在我们将做一个乘法(这里的点是零位(

  |  a7  ||  a6  ||  a4  ||  a4  ||  a3  ||  a2  ||  a1  ||  a0  |
  .......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个最低有效位已被放入顶部字节中,我们只需要屏蔽剩余

因此,通过使用幻数0b10000000010000000010000000010000000010000000010000000010000000010x8040201008040201,我们得到了上面的代码

当然,您需要确保 bool 数组正确对齐 8 字节。您还可以展开代码并对其进行优化,例如仅移位一次,而不是向左移动 56 位

<小时 />

抱歉,我忽略了这个问题,看到了 doynax 的布尔数组以及误读了"32 0/1 值">,并认为它们是 32 bool s。当然,同样的技术也可以用来同时打包多个uint32_tuint16_t值(或其他位分布(,但它的效率比打包字节要低得多。

在带有 BMI2 的较新的 x86 CPU 上,可以使用 PEXT 指令。上面的pack8b函数可以替换为

_pext_u64(*((uint64_t*)a), 0x0101010101010101ULL);

并包装 2 uint32_t因为问题需要使用

_pext_u64(*((uint64_t*)a), (1ULL << 32) | 1ULL);

其他答案包含明显的循环实现。

这是第一个变体:

unsigned int result=0;
for(unsigned i = 0; i < 32; ++i)
    result = (result<<1) + a[i];

在现代 x86 CPU 上,我认为寄存器中任何距离的偏移都是恒定的,而且这种解决方案不会更好。 您的 CPU 可能不是那么好;此代码最大限度地减少了长途班次的成本;它执行每个 CPU 都可以执行的 32 个 1 位移位(您可以随时将结果添加到自身以获得相同的效果(。其他人展示的明显循环实现通过移动等于循环索引的距离,执行了大约 900 次(32 次总和(1 位移位。 (请参阅@Jongware对注释差异的测量;x86 上的长班次不是单位时间(。

让我们尝试一些更激进的东西。

假设你可以以某种方式将 m 个布尔值打包到一个 int 中(很简单,你可以为 m==1 执行此操作(,并且你有两个实例变量 i1i2 包含这样的 m 个打包位。

然后,以下代码将 m*2 布尔值打包成一个 int:

 (i1<<m+i2)

使用它我们可以打包 2^n 位,如下所示:

 unsigned int a2[16],a4[8],a8[4],a16[2], a32[1]; // each "aN" will hold N bits of the answer
 a2[0]=(a1[0]<<1)+a2[1];  // the original bits are a1[k]; can be scalar variables or ints
 a2[1]=(a1[2]<<1)+a1[3];  //  yes, you can use "|" instead of "+"
 ...
 a2[15]=(a1[30]<<1)+a1[31];
 a4[0]=(a2[0]<<2)+a2[1];
 a4[1]=(a2[2]<<2)+a2[3];
 ...
 a4[7]=(a2[14]<<2)+a2[15];
 a8[0]=(a4[0]<<4)+a4[1];
 a8[1]=(a4[2]<<4)+a4[3];
 a8[1]=(a4[4]<<4)+a4[5];
 a8[1]=(a4[6]<<4)+a4[7];
 a16[0]=(a8[0]<<8)+a8[1]);
 a16[1]=(a8[2]<<8)+a8[3]);
 a32[0]=(a16[0]<<16)+a16[1];

假设我们友好的编译器将 an[k] 解析为(标量(直接内存访问(如果没有,您可以简单地将变量 an[k] 替换为 an_k(,上面的代码(抽象地(进行了 63 次获取、31 次写入、31 次移位和 31 次添加。(有一个明显的扩展到64位(。

在现代 x86 CPU 上,我认为寄存器中任何距离的偏移都是恒定的。如果没有,此代码将最大限度地降低长途班次的成本;它实际上执行 64 个 1 位移位。

在 x64 机器上,除了原始布尔值 a1[k] 的获取之外,我希望编译器可以调度所有其他标量以适应寄存器,因此 32 个内存获取、31 个移位和 31 个加法。 很难避免获取(如果原始布尔值分散在周围(,并且移位/添加与明显的简单循环相匹配。 但是没有循环,所以我们避免了 32 个增量/比较/索引操作。

如果起始布尔值确实在数组中,则每个位占用字节的底部位,否则为零:

bool a1[32];

然后我们可以滥用我们对内存布局的了解,一次获取几个:

a4[0]=((unsigned int)a1)[0]; // picks up 4 bools in one fetch
a4[1]=((unsigned int)a1)[1];
...
a4[7]=((unsigned int)a1)[7];
a8[0]=(a4[0]<<1)+a4[1];
a8[1]=(a4[2]<<1)+a4[3];
a8[2]=(a4[4]<<1)+a4[5];
a8[3]=(a8[6]<<1)+a4[7];
a16[0]=(a8[0]<<2)+a8[1];
a16[0]=(a8[2]<<2)+a8[3];
a32[0]=(a16[0]<<4)+a16[1];

在这里,我们的成本是 8 次(4 组(布尔值的获取、7 个班次和 7 个加法。同样,没有循环开销。(再次对 64 位有明显的概括(。

为了获得比这更快的速度,您可能必须进入汇编程序并使用那里可用的许多奇妙而奇怪的指令中的一些(向量寄存器可能具有可能很好地工作的分散/收集操作(。

与往常一样,这些解决方案需要进行性能测试。

我可能会这样做:

unsigned a[32] =
{
    1, 0, 0, 1, 1, 1, 0 ,0, 1, 0, 0, 0, 1, 1, 0, 0
    , 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1
};
int main()
{
    unsigned b = 0;
    for(unsigned i = 0; i < sizeof(a) / sizeof(*a); ++i)
        b |= a[i] << i;
    printf("b: %un", b);
}

编译器优化可能会很好地展开它,但以防万一您可以随时尝试:

int main()
{
    unsigned b = 0;
    b |= a[0];
    b |= a[1] << 1;
    b |= a[2] << 2;
    b |= a[3] << 3;
    // ... etc
    b |= a[31] << 31;
    printf("b: %un", b);
}

要确定最快的方法是什么,请对所有各种建议进行计时。这是一个很可能最终成为"最快"的(使用标准 C,没有处理器依赖的 SSE 等(

unsigned int bits[32][2] = {
    {0,0x80000000},{0,0x40000000},{0,0x20000000},{0,0x10000000},
    {0,0x8000000},{0,0x4000000},{0,0x2000000},{0,0x1000000},
    {0,0x800000},{0,0x400000},{0,0x200000},{0,0x100000},
    {0,0x80000},{0,0x40000},{0,0x20000},{0,0x10000},
    {0,0x8000},{0,0x4000},{0,0x2000},{0,0x1000},
    {0,0x800},{0,0x400},{0,0x200},{0,0x100},
    {0,0x80},{0,0x40},{0,0x20},{0,0x10},
    {0,8},{0,4},{0,2},{0,1}
};
unsigned int b = 0;
for (i=0; i< 32; i++)
     b |= bits[i][a[i]];

数组中的第一个值是最左边的位:可能的最高值。

用一些粗略的时序测试概念验证表明,这确实并不比带有b |= (a[i]<<(31-i))的直接循环好多少:

Ira                   3618 ticks
naive, unrolled       5620 ticks
Ira, 1-shifted       10044 ticks
Galik                10265 ticks
Jongware, using adds 12536 ticks
Jongware             12682 ticks
naive                13373 ticks

(相对计时,具有相同的编译器选项。

("adds"例程是我的,索引替换为指针指向和两个索引数组的显式添加。它慢了 10%,这意味着我的编译器正在有效地优化索引访问。很高兴知道。

unsigned b=0;
for(int i=31; i>=0; --i){
    b<<=1;
    b|=a[i];
}

您的问题是使用 --> 的好机会,也称为 downto 运算符:

unsigned int a[32];
unsigned int b = 0;
for (unsigned int i = 32; i --> 0;) {
    b += b + a[i];
}

使用 --> 的优点是它适用于有符号和无符号循环索引类型。

这种方法是可移植且可读的,它可能不会生成最快的代码,但clang确实展开循环并产生不错的性能,请参阅 https://godbolt.org/g/6xgwLJ