C++ 实现基于 CPU 指令级并行的位数组快速位计数算法 _ SIMD 优化【源码】

冬萱酱_9330

冬萱酱_9330

2026-05-22

278人浏览

原创

_popcnt64 单次快但整体慢因流水线中断;avx2通过32字节并行查表模拟popcnt(拆高低4位+双lut+add)提速3–4倍;真·_mm256_popcnt_epi8属avx-512vpopcntdq,需cpu支持、内核启用及gcc 11+ -mavx512vpopcntdq编译。

c++ 实现基于 cpu 指令级并行的位数组快速位计数算法 _ simd 优化【源码】

为什么 _popcnt64 比循环查表快,但还不是最快?

因为单条 _popcnt64 指令虽快,但它一次只处理一个 64 位整数;而实际位数组(如 1MB 的 bitmap)包含上万次调用,CPU 流水线常被分支和依赖打断。真正瓶颈不在单个计数,而在数据吞吐和指令级并行度。SIMD 不是“加速 popcnt”,而是让 32 个字节(__m256i)同时进多个 popcnt 单元——前提是硬件支持 AVX2 + POPCNT 指令集。

常见错误现象:__builtin_popcountll 在未开启 -mpopcnt 时退化为慢速查表;用 _mm256_popcnt_epi8 却编译失败——那是因为该 intrinsic 要求 ICC 或 GCC 11+ 且必须启用 -mavx512vpopcntdq,不是普通 AVX2。

  • 确认 CPU 支持:运行 cat /proc/cpuinfo | grep -E "(avx|popcnt)",需同时含 avx2 和 popcnt
  • GCC 编译必须加 -mavx2 -mpopcnt,否则 _mm_popcnt_u64 无法内联为硬件指令
  • 不要对未对齐内存直接 load:用 _mm256_loadu_si256 安全但略慢,若能保证 32 字节对齐,优先用 _mm256_load_si256

如何用 AVX2 把 256 位分组并行计数?

AVX2 本身不提供字节级 popcnt,但可用“查表 + 移位 + 掩码”在寄存器内模拟:把每个字节的低 4 位和高 4 位拆开,查两个 16 元素 LUT,再相加。这种方法比标量快 3–4 倍,且兼容所有 AVX2 CPU。

关键点在于避免跨寄存器依赖:不能等第一个 _mm256_shuffle_epi8 结果出来再做第二个——要并行发射 shuffle、and、add 指令。

  • 查表用 __m256i lut_lo = _mm256_setr_epi8(0,1,1,2,1,2,2,3,...) 静态初始化,避免运行时构造开销
  • 提取低 4 位:_mm256_and_si256(v, _mm256_set1_epi8(0x0F));高 4 位右移 4 位再 and
  • 两次 _mm256_shuffle_epi8 后,用 _mm256_add_epi8 合并结果,得到 32 字节各自的 bit count
  • 最后用 _mm256_sad_epu8(绝对差求和)把 32 字节 sum 成两个 64 位值,再水平加总

_mm256_popcnt_epi8 真实可用吗?什么条件下能写进生产代码?

它真实存在,但属于 AVX-512 子集 vpopcntdq,不是 AVX2。即使 CPU 支持(如 Ice Lake、Sapphire Rapids),Linux 内核默认禁用 AVX-512 以节省功耗,用户态需显式调用 prctl(PR_SET_XSAVE_OFFSET, ...) 并检查 xgetbv,否则会触发 #UD 异常。

C++14
C++14

C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。

下载

错误现象:程序在某些机器上 SIGILL,strace 显示 --- SIGILL {si_signo=SIGILL, si_code=SI_KERNEL, si_addr=NULL} ---,就是 AVX-512 被禁用或未检测。

  • 必须运行时检测:if (__builtin_ia32_cpu_supports("avx512vpopcntdq")) { ... }
  • GCC 需加 -mavx512vpopcntdq -mavx512f,且链接时确保 glibc ≥ 2.33(旧版无对应 runtime check)
  • 别把它当 fallback:AVX2 版本在老 CPU 上更稳,AVX-512 版本只应在明确部署环境可控时启用

位数组边界不对齐怎么安全处理剩余字节?

SIMD 主循环处理 len & ~31 字节后,剩下 0–31 字节。这里最容易错的是:用标量循环逐字节处理,却忘了剩余部分可能不足 8 字节,导致 _mm_popcnt_u64 读越界。

正确做法是统一用字节粒度处理尾部,并复用前面的 LUT 逻辑——只是换成 __m128i 或纯标量查表,避免引入新分支预测失败。

  • 剩余长度 r = len & 31,用 memcpy 拷到栈上 32 字节 buffer,再按 AVX2 方式处理(安全且无分支)
  • 如果 r _mm_loadu_si128 + 同套 shuffle/add 流程,比写 if-else 分支更高效
  • 绝对不要对原始指针做 (uint64_t*)ptr[r] 强转——未定义行为,Clang 可能优化掉边界检查

最易被忽略的是:popcnt 结果累加本身可能成为瓶颈。别用 sum += _mm256_extract_epi32(...) 循环提取——这强制串行化。应该用 4 个 64 位累加器寄存器,每轮加一个 _mm256_sad_epu8 的两路输出,最后才 horizontal add。寄存器重命名和指令级并行,得从累加器设计就开始考虑。

C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

2023.06.20

3289

3

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

2023.07.25

2348

9

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

2023.08.02

1280

5

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

2023.08.09

1238

4

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

2023.09.05

1376

5

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

2023.09.20

2218

7

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

2023.09.20

3580

8

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

2023.09.22

16495

6

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

2023.09.22

549

3

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C++ Core Guidelines
C++ Core Guidelines

共0课时 | 0人学习

C++ Reference
C++ Reference

共0课时 | 0人学习

C++ 官方标准说明
C++ 官方标准说明

共0课时 | 0人学习