You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

统计整数数组各比特位置位次数的高效算法与位运算技巧

问题背景

我有大量32位(32b)数值,需要统计全量数据中每个第n位为真(置1)的出现总次数。由于该操作是整个仿真程序的性能瓶颈,我需要尽可能快的实现方案。我编写了一个针对8位数值的朴素C++实现用于举例说明问题:

#include <iostream>
#include <vector>
#include <cstdint>

std::vector<uint32_t> vertical_popcount(std::vector<uint8_t>& data) {
    std::vector<uint32_t> result({0, 0, 0, 0, 0, 0, 0, 0});
    for (auto i = 0; i < data.size(); i++) {
        result[0] += (data[i] & 0b10000000) > 0;
        result[1] += (data[i] & 0b01000000) > 0;
        result[2] += (data[i] & 0b00100000) > 0;
        result[3] += (data[i] & 0b00010000) > 0;
        result[4] += (data[i] & 0b00001000) > 0;
        result[5] += (data[i] & 0b00000100) > 0;
        result[6] += (data[i] & 0b00000010) > 0;
        result[7] += (data[i] & 0b00000001) > 0;
    }
    return result;
}

int main() {
    std::vector<uint8_t> data({0b00000001, 0b00000100, 0b00000101});
    auto result = vertical_popcount(data);
    std::cout << "occurrence of bits: " << result[0] << ", " << result[1] << ", " << result[2] << ", " << result[3] << ", " << result[4] << ", " << result[5] << ", " << result[6] << ", " << result[7] << "\n";
    return 0;
}

是否存在可以实现相同功能、且运行速度大幅提升的算法?

优化方案

你的朴素实现性能差的核心原因是存在冗余的比较操作,且没有利用现代CPU的位操作能力和数据级并行特性,从易到难有几档优化,性能可以提升3~30倍不等:

第一档:无分支标量优化(性能提升2~4倍)

首先去掉所有>0的判断逻辑:位与操作得到的非零值不需要比较,直接把对应位右移到最低位,和1做与操作就能直接得到0/1的计数值,编译器可以直接把这部分逻辑编译成无分支的指令序列,完全消除分支预测开销。
优化后的8位数据处理代码如下:

#include <cstdint>
#include <vector>

std::vector<uint32_t> vertical_popcount_scalar(const std::vector<uint8_t>& data) {
    uint32_t cnt[8] = {0};
    const size_t n = data.size();
    const uint8_t* ptr = data.data();
    for (size_t i = 0; i < n; i++) {
        const uint8_t x = ptr[i];
        cnt[0] += (x >> 7) & 1;
        cnt[1] += (x >> 6) & 1;
        cnt[2] += (x >> 5) & 1;
        cnt[3] += (x >> 4) & 1;
        cnt[4] += (x >> 3) & 1;
        cnt[5] += (x >> 2) & 1;
        cnt[6] += (x >> 1) & 1;
        cnt[7] += x & 1;
    }
    return {cnt, cnt + 8};
}

如果要处理32位数据,只需要把cnt数组长度改成32,循环内依次移位取位即可,逻辑完全一致。打开O2/O3优化时,这段代码的效率已经比朴素实现高很多。

第二档:分块popcount批量统计(性能提升8~15倍)

利用CPU原生的popcount指令(x86下的popcnt,ARM下的cnt),每64个元素分块处理:把64个元素的同一比特位拼成一个64位整数,直接用一次popcount指令得到这64个元素中该位的置1总数,不需要逐元素累加。
32位数据的分块实现示例:

#include <cstdint>
#include <vector>
#include <immintrin.h> // 包含硬件popcnt指令声明

std::vector<uint64_t> vertical_popcount_block(const uint32_t* data, size_t n) {
    uint64_t cnt[32] = {0};
    size_t i = 0;
    // 每次处理64个32位元素
    for (; i + 63 < n; i += 64) {
        for (int b = 0; b < 32; b++) {
            uint64_t bit_block = 0;
            for (int k = 0; k < 64; k++) {
                bit_block |= static_cast<uint64_t>((data[i + k] >> b) & 1) << k;
            }
            cnt[b] += _mm_popcnt_u64(bit_block);
        }
    }
    // 处理剩余不足64个的尾部元素
    for (; i < n; i++) {
        const uint32_t x = data[i];
        for (int b = 0; b < 32; b++) {
            cnt[b] += (x >> b) & 1;
        }
    }
    return {cnt, cnt + 32};
}

编译器会自动把内层拼bit_block的循环优化成高效的位提取序列,配合硬件popcnt指令,吞吐量比标量版本高一个量级。

第三档:SIMD向量化(性能提升20~30倍)

如果你的CPU支持AVX2/AVX-512指令集,可以直接用512位宽的寄存器一次处理16个32位整数,通过SIMD移位、掩码、按字节累加指令批量统计每一位的计数,不需要逐位循环。这一档优化可以把性能拉到内存带宽上限,也就是数据从内存读到CPU的速度有多快,统计速度就有多快,是目前量产CPU上的最优实现方案。

性能注意点
  • 编译时一定要打开O2/O3优化,以及对应平台的指令集选项(比如x86下GCC/Clang加-march=native,MSVC下加/arch:AVX2),否则所有优化都不会生效。
  • 尽量直接访问数组的底层指针,避免在热循环中调用std::vector的[]运算符做冗余的边界检查(开O2后多数情况会被优化掉,但手动写更稳妥)。
  • 计数值根据数据总量选择合适的位宽:如果数据量不超过42亿,用uint32_t存计数即可,超过的话换uint64_t,避免溢出。

内容的提问来源于stack exchange,提问作者toothgoblin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 13:48:12