使用 AVX-512 或 AVX-2 对大数据进行 1 位计数(总体计数)

2024-03-07

我有一大块内存,比如说 256 KiB 或更长。我想计算整个块中 1 位的数量,或者换句话说:将所有字节的“总体计数”值相加。

我知道 AVX-512 有一个VPOPCNTDQ指令 https://en.wikipedia.org/wiki/AVX-512#New_instructions_in_AVX-512_VPOPCNTDQ_and_BITALG它计算 512 位向量中每个连续 64 位中 1 位的数量,并且 IIANM 应该可以在每个周期发出其中一个(如果有适当的 SIMD 向量寄存器可用) - 但我没有任何编写 SIMD 代码的经验(我更喜欢 GPU)。另外,我不能 100% 确定编译器对 AVX-512 目标的支持。

在大多数 CPU 上,仍然不(完全)支持 AVX-512;但 AVX-2 已广泛使用。我一直无法找到类似于 VPOPCNTDQ 的小于 512 位向量化指令,因此即使理论上我也不确定如何使用支持 AVX-2 的 CPU 快速计算位;也许这样的事情存在,但我只是以某种方式错过了它?

不管怎样,我很欣赏一个简短的 C/C++ 函数 - 要么使用一些 intritics-wrapper 库,要么使用内联汇编 - 对于两个指令集中的每一个。签名是

uint64_t count_bits(void* ptr, size_t size);

Notes:

  • 相关如何在 Sandy Bridge 上将一系列整数中的位快速计数到单独的容器中? https://stackoverflow.com/questions/7793997/,但不是骗子。
  • 如果重要的话,我们可以假设输入是对齐良好的。
  • 忘记多个核心或套接字,我想要单个(单个线程)核心的代码。

AVX-2

@HadiBreis 的评论链接到article http://0x80.pl/articles/sse-popcount.html#core-i7-skylake关于使用 SSSE3 进行快速人口统计,作者:Wojciech Muła;文章链接到这个 GitHub 存储库 https://github.com/WojciechMula/sse-popcount/;和存储库has https://github.com/WojciechMula/sse-popcount/blob/master/popcnt-avx2-lookup.cpp以下 AVX-2 实现。它基于矢量化查找指令,并使用 16 值查找表来计算半字节的位数。

#   include <immintrin.h>
#   include <x86intrin.h>

std::uint64_t popcnt_AVX2_lookup(const uint8_t* data, const size_t n) {

    size_t i = 0;

    const __m256i lookup = _mm256_setr_epi8(
        /* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
        /* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
        /* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
        /* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4,

        /* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
        /* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
        /* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
        /* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
    );

    const __m256i low_mask = _mm256_set1_epi8(0x0f);

    __m256i acc = _mm256_setzero_si256();

#define ITER { \
        const __m256i vec = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(data + i)); \
        const __m256i lo  = _mm256_and_si256(vec, low_mask); \
        const __m256i hi  = _mm256_and_si256(_mm256_srli_epi16(vec, 4), low_mask); \
        const __m256i popcnt1 = _mm256_shuffle_epi8(lookup, lo); \
        const __m256i popcnt2 = _mm256_shuffle_epi8(lookup, hi); \
        local = _mm256_add_epi8(local, popcnt1); \
        local = _mm256_add_epi8(local, popcnt2); \
        i += 32; \
    }

    while (i + 8*32 <= n) {
        __m256i local = _mm256_setzero_si256();
        ITER ITER ITER ITER
        ITER ITER ITER ITER
        acc = _mm256_add_epi64(acc, _mm256_sad_epu8(local, _mm256_setzero_si256()));
    }

    __m256i local = _mm256_setzero_si256();

    while (i + 32 <= n) {
        ITER;
    }

    acc = _mm256_add_epi64(acc, _mm256_sad_epu8(local, _mm256_setzero_si256()));

#undef ITER

    uint64_t result = 0;

    result += static_cast<uint64_t>(_mm256_extract_epi64(acc, 0));
    result += static_cast<uint64_t>(_mm256_extract_epi64(acc, 1));
    result += static_cast<uint64_t>(_mm256_extract_epi64(acc, 2));
    result += static_cast<uint64_t>(_mm256_extract_epi64(acc, 3));

    for (/**/; i < n; i++) {
        result += lookup8bit[data[i]];
    }

    return result;
}

AVX-512

同一存储库还有基于 VPOPCNT 的 AVX-512 实现。在列出它的代码之前,这里是简化且更易读的伪代码:

  • 对于每个连续的 64 字节序列:

    • 将序列加载到 64x8 = 512 位的 SIMD 寄存器中
    • 在该寄存器上执行 8 个并行填充计数,每个计数 64 位
    • 将 8 个人口计数结果并行添加到保存 8 个和的“累加器”寄存器中
  • 将累加器中的 8 个值相加

  • 如果尾部小于 64 字节,则以一些更简单的方式计算那里的位

  • 返回主和加上尾和

现在是真正的交易:

#   include <immintrin.h>
#   include <x86intrin.h>

uint64_t avx512_vpopcnt(const uint8_t* data, const size_t size) {
    
    const size_t chunks = size / 64;

    uint8_t* ptr = const_cast<uint8_t*>(data);
    const uint8_t* end = ptr + size;

    // count using AVX512 registers
    __m512i accumulator = _mm512_setzero_si512();
    for (size_t i=0; i < chunks; i++, ptr += 64) {
        
        // Note: a short chain of dependencies, likely unrolling will be needed.
        const __m512i v = _mm512_loadu_si512((const __m512i*)ptr);
        const __m512i p = _mm512_popcnt_epi64(v);

        accumulator = _mm512_add_epi64(accumulator, p);
    }

    // horizontal sum of a register
    uint64_t tmp[8] __attribute__((aligned(64)));
    _mm512_store_si512((__m512i*)tmp, accumulator);

    uint64_t total = 0;
    for (size_t i=0; i < 8; i++) {
        total += tmp[i];
    }

    // popcount the tail
    while (ptr + 8 < end) {
        total += _mm_popcnt_u64(*reinterpret_cast<const uint64_t*>(ptr));
        ptr += 8;
    }

    while (ptr < end) {
        total += lookup8bit[*ptr++];
    }

    return total;
}

The lookup8bit是一个针对字节而不是位的 popcnt 查找表,并且定义为here https://github.com/WojciechMula/sse-popcount/blob/b4905d6ecd9d01243b6aa8ddccc8a5bf496c7969/popcnt-lookup.cpp. edit:正如评论者所指出的,最后使用 8 位查找表并不是一个好主意,可以改进。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 AVX-512 或 AVX-2 对大数据进行 1 位计数(总体计数) 的相关文章

随机推荐