popcnt的前世今生?
不是popcorn,更不是pornhub
本文感谢 Amnesia 赞助
最近群聊里传了一个面试题
实现统计1的个数(汉明权重 hammingWeight),使用popcnt的算法对硬件不友好,有无绕过的思路
显然这个哥们的第一个实现是
int hammingWeight_popcnt(uint64_t n) {
return __builtin_popcountll(n);
}
当然c++20也支持 https://en.cppreference.com/w/cpp/numeric/popcount
一行,觉得自己很帅,当然面试官不喜欢,提示不要用popcnt,所谓的对硬件不友好指的应该是部分硬件没有这个指令
又或者性能原因?难道GPU上的popcnt性能很差?按下不表
直接贴实现
int hammingWeight(uint64_t n) {
int ret = 0;
while (n) {
n &= n - 1;
ret++;
}
return ret;
}
其实开启 O2 加上 -march=native,大家都会生成相同的popcnt, 早在2016年Lemire大哥就发现了
https://lemire.me/blog/2016/05/23/the-surprising-cleverness-of-modern-compilers/
附上llvm检测的代码 https://github.com/llvm-mirror/llvm/blob/f36485f7ac2a8d72ad0e0f2134c17fd365272285/lib/Transforms/Scalar/LoopIdiomRecognize.cpp#L960
只开O2可能保守场景不会生成popcnt
如果不用popcnt,代码的性能和popcnt差距大吗?或者说,popcnt有危害吗?比如延迟高?
直接上llvm-mca分析 https://godbolt.org/z/odox8Wdr5
首先插入一个简单粗暴的教程,如何看懂llvm-mca https://llvm.org/docs/CommandGuide/llvm-mca.html
就是机器码分析器,模拟机器码执行效果,我们不用装llvm-mca,直接用godbolt内置的工具。代码已经生成好了
直接贴popcnt代码的结果吧
Iterations: 100
Instructions: 200
Total Cycles: 57
Total uOps: 200
Dispatch Width: 6
uOps Per Cycle: 3.51
IPC: 3.51
Block RThroughput: 0.5
Instruction Info:
[1]: #uOps
[2]: Latency
[3]: RThroughput
[4]: MayLoad
[5]: MayStore
[6]: HasSideEffects (U)
[1] [2] [3] [4] [5] [6] Instructions:
1 1 0.25 popcnt rax, rdi
1 5 0.50 U ret
Resources:
[0] - Zn3AGU0
[1] - Zn3AGU1
[2] - Zn3AGU2
[3] - Zn3ALU0
[4] - Zn3ALU1
[5] - Zn3ALU2
[6] - Zn3ALU3
[7] - Zn3BRU1
[8] - Zn3FPP0
[9] - Zn3FPP1
[10] - Zn3FPP2
[11] - Zn3FPP3
[12.0] - Zn3FPP45
[12.1] - Zn3FPP45
[13] - Zn3FPSt
[14.0] - Zn3LSU
[14.1] - Zn3LSU
[14.2] - Zn3LSU
[15.0] - Zn3Load
[15.1] - Zn3Load
[15.2] - Zn3Load
[16.0] - Zn3Store
[16.1] - Zn3Store
Resource pressure per iteration:
[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12.0] [12.1] [13] [14.0] [14.1] [14.2] [15.0] [15.1] [15.2] [16.0] [16.1]
0.33 0.33 0.34 0.50 0.33 0.33 0.34 0.50 - - - - - - - 0.33 0.33 0.34 0.33 0.33 0.34 - -
Resource pressure by instruction:
[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12.0] [12.1] [13] [14.0] [14.1] [14.2] [15.0] [15.1] [15.2] [16.0] [16.1] Instructions:
- - - - 0.33 0.33 0.34 - - - - - - - - - - - - - - - - popcnt rax, rdi
0.33 0.33 0.34 0.50 - - - 0.50 - - - - - - - 0.33 0.33 0.34 0.33 0.33 0.34 - - ret
warning: found a return instruction in the input assembly sequence.
note: program counter updates are ignored.
先记下这几个数字
Iterations: 100
Instructions: 200
Total Cycles: 57
Total uOps: 200
Dispatch Width: 6
uOps Per Cycle: 3.51
IPC: 3.51
Block RThroughput: 0.5
重点参数 是IPC, uOps Per Cycle, 和 Block RThroughput (Block Reciprocal Throughput).
- IPC就是模拟的总指令数字除以总cycle数 一般这个就表示吞吐了 Instructions / Total Cycles 显然这个值越高越好
- Block RThroughput (Block Reciprocal Throughput) 就是Block Throughput(每个cycle能运行几次block)的倒数 就是 Total Cycles / Iterations 每次运行能用几个cycle的意思,显然,这个值越低越好
-
Instructions / Iterations 代表每次迭代能执行几次指令 显然 Instructions / Iterations / Block RThroughput = IPC 这个数比直接算IPC大点(有误差。。。。)你就当他是无影响的最大IPC吧
- (循环数据引用问题可能导致影响。假设循环展开无影响)
- uOps Per Cycle,就是模拟的微指令数总和除以总cycle数字 Total uOps/ Total Cycles ,这个和IPC含义差不多,显然这个值越高越好,但是要关注Dispatch Width - uOps Per Cycle 差值, Dispatch Width 表示最大发射指令的并行带宽,相当于资源限制了,uOps Per Cycle表示实际模拟使用的带宽,显然越接近Dispatch Width越说明资源受限制,利用率太高了,相当于CPU高了,需要找到瓶颈来源
- 剩下的是执行模拟以及在哪里卡了,具体分析可以用-bottleneck-analysis,得本地搞了godbolt貌似不能玩
好了,根据上面的godbolt结果,直接把数据差异对比一下
另外 网上搜到了google的两个实现,把数据补充上 https://godbolt.org/z/9nsczeT5c
int hammingWeightV2(uint64_t n) {
n -= (n >> 1) & 0x5555555555555555ULL;
n = ((n >> 2) & 0x3333333333333333ULL) + (n & 0x3333333333333333ULL);
return (((n + (n >> 4)) & 0xF0F0F0F0F0F0F0FULL)
* 0x101010101010101ULL) >> 56;
}
这个实现在一些cpu上有问题 type mismatch。不过一般来说和buildin popcnt一样效果
int hammingWeight_popcntV2(uint64_t n) {
int64_t count = 0;
asm("popcnt %1,%0" : "=r"(count) : "rm"(n) : "cc");
return count;
}
实现 | 编译器版本 | Dispatch Width | uOps/Cycle | IPC | Block RThroughput |
---|---|---|---|---|---|
popcnt | gcc 13.2 | 6 | 3.67 | 1.83 | 1.0 |
普通实现 | gcc 13.2 | 4 | 3.94 | 3.54 | 2.5 |
popcnt | clang 17.0.1 | 6 | 4.59 | 2.75 | 1.0 |
普通实现 | clang 17.0.1 | 6 | 4.78 | 3.83 | 1.7 |
popcnt v2 | gcc 13.2 | 6 | 2.14 | 2.14 | 1.3 |
手写SWAR | gcc 13.2 | 6 | 4.09 | 3.72 | 3.8 |
popcnt v2 | clang 17.0.1 | 6 | 3.67 | 1.83 | 1.0 |
手写SWAR | clang 17.0.1 | 6 | 4.09 | 3.72 | 3.8 |
能看出popcnt的Block RThroughput 低,这显然说明性能更好
然后看IPC和uOps/Cycle clang的明显比gcc的要高,但汇编说实话一个两行一个一行,这个没啥比较的意义了
重点和普通实现比,clang生成的汇编要比gcc好一点,Block RThroughput 低 IPC高,且没有特别接近Dispatch Width瓶颈
但说实话就差一个汇编这点差距根本比不出什么。只能大概说一下popcnt的汇编更少,性能更好而已
感觉SWAR这种看起来很屌, 但看mca分析感觉不太行 我跑了个qb压测,但是网站挂了,还需要本地跑一下
https://github.com/wanghenshui/little_bm/blob/dev/hamming_weight/hamming_weight.cc
我的测试结果来看,SWAR性能反而比popcnt要好,即使Block RThroughput 很高,但IPC也很高,性能反而非常好
taskset -c 0 ./hamming_weight
2024-02-03T22:39:35+08:00
Running ./hamming_weight
Run on (16 X 3392.38 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x8)
L1 Instruction 32 KiB (x8)
L2 Unified 512 KiB (x8)
L3 Unified 16384 KiB (x1)
Load Average: 0.01, 0.02, 0.00
-----------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------
BM_hammingWeight_popcnt/0 26.6 ns 26.6 ns 26281110
BM_hammingWeight_popcnt/128 265 ns 265 ns 2622989
BM_hammingWeight_popcnt/256 526 ns 526 ns 1332695
BM_hammingWeight_popcnt/512 1048 ns 1048 ns 666562
BM_hammingWeight_popcnt/1024 2096 ns 2095 ns 334434
BM_hammingWeight/0 80.7 ns 80.7 ns 8689750
BM_hammingWeight/128 1643 ns 1642 ns 447638
BM_hammingWeight/256 3646 ns 3646 ns 195882
BM_hammingWeight/512 8099 ns 8097 ns 85508
BM_hammingWeight/1024 17193 ns 17190 ns 41208
BM_hammingWeightV2/0/0 11.8 ns 11.8 ns 58402778
BM_hammingWeightV2/128 118 ns 118 ns 5951445
BM_hammingWeightV2/256 233 ns 233 ns 3001257
BM_hammingWeightV2/512 463 ns 463 ns 1510925
BM_hammingWeightV2/1024 927 ns 927 ns 758631
不过还需要其他机器测试,我的nuc是r9 6950hx zen3+,性能不错
github CI机器,SWAR和popcnt就差不多了。
Running ./bm_hamming_weight
Run on (4 X 2868.73 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x2)
L1 Instruction 32 KiB (x2)
L2 Unified 512 KiB (x2)
L3 Unified 32768 KiB (x1)
Load Average: 1.98, 0.54, 0.19
-----------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------
BM_hammingWeight_popcnt/0 17.5 ns 17.5 ns 39446954
BM_hammingWeight_popcnt/128 173 ns 173 ns 4056917
BM_hammingWeight_popcnt/256 342 ns 342 ns 2051152
BM_hammingWeight_popcnt/512 679 ns 679 ns 1032384
BM_hammingWeight_popcnt/1024 1354 ns 1354 ns 517551
BM_hammingWeight/0 124 ns 124 ns 5638895
BM_hammingWeight/128 2394 ns 2394 ns 280377
BM_hammingWeight/256 5511 ns 5511 ns 123293
BM_hammingWeight/512 12036 ns 12036 ns 58336
BM_hammingWeight/1024 25711 ns 25710 ns 27149
BM_hammingWeightV2/0/0 17.8 ns 17.8 ns 39494382
BM_hammingWeightV2/128 182 ns 182 ns 3848768
BM_hammingWeightV2/256 360 ns 360 ns 1943778
BM_hammingWeightV2/512 709 ns 709 ns 988825
BM_hammingWeightV2/1024 1418 ns 1418 ns 493319
家人们,需要你们的补充测试,各种机器, 复现代码https://github.com/wanghenshui/little_bm 运行build.sh即可
话说回来,数1到底能干嘛?这里要引入汉明距离 编辑距离相关的概念
简单理解就是查diff 纠错码之类的效果
popcnt的来源 http://www.talkchess.com/forum3/viewtopic.php?t=38521
上个世纪60年代,计算机还属于大型机百花齐放的年代,Control Data Corporation公司的CDC 机器卖的不错,国际象棋也在用这个软件。他们的场景就是棋盘格确认位置,所以实现了popcnt类似的能力,算位置坐标,美国国家安全局(NSA)发现了他们有这个能力,他们的新机器CDC 6000,政府采购并要求加上这个功能,主要是为了类似汉明距离之类的信息统计,相当于变相hash,用来实现校对diff之类的能力,所以也被叫做NSA Instruction (NSA指令)
这个指令也是那个时代的特殊产物把,算力不行并没有高级的hash能力,只能通过数1模拟,后来CPU性能提升渐渐的都不支持了,然后后来部分CPU支持部分CPU不支持,到现代全都捡回来
现在的CPU也有很多不支持popcnt指令,以至于游戏客户端领域会有popcnt patch之类的玩意,给玩家打patch绕过popcnt https://github.com/ogurets/popcnt_emulator
还有什么能用到数1?
指纹?安全领域,这种更多是汉明距离场景的推广
能用到bitmap的地方,不过使用bitmap不一定非得算总数
比如 Hash Array Mapped Tries 结合tries的压缩优点 + bitmap定位槽,
bitmap浪费所以要压缩一下,位运算躲不了数1场景
再比如 Succinct Data Structures terarkdb的memtable用的就这玩意,压缩率高
关于popcnt的信息我就收集到这么多的,大家有其他见解/批评还可以补充一下
另外,跑一下压测代码!看看你的CPU结果是什么样子的
参考
- https://vaibhavsagar.com/blog/2019/09/08/popcount/ 一些资料汇总在这里搜到的。我一开始是根据群友聊的和关键字搜到hackernews上这篇文章的分享,介绍了背景和部分应用
- https://abseil.io/fast/9 这里说的话我很赞同,性能测试是个周期性的工作,可能旧的代码有时候快,后面时代/硬件进步,又慢了 还是要具体机器具体分析
- https://github.com/google/supersonic/blob/master/supersonic/utils/bits.h
- https://stackoverflow.com/questions/28802692/how-is-popcnt-implemented-in-hardware 这个没看,但感觉现代CPU popcnt代价已经很低了