前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >popcnt也能向量化?

popcnt也能向量化?

作者头像
王很水
发布2024-07-30 15:22:43
780
发布2024-07-30 15:22:43
举报
文章被收录于专栏:C++ 动态新闻推送

popcnt指令本身也支持sse向量化了,但如果序列非常大 popcnt只能处理8B,怎么办

直观的方法就是把序列按照8B拆开,分段popcnt,或者,向量化?大块?

回忆一下popcnt实现

代码语言:javascript
复制
int count ( uint64_t x) {
    int v = 0;
    while (x != 0) {
        x &= x - 1;
        v ++;
    }
    return v;
}

当然编译器会优化成popcnt

考虑avx512,这个足够大了吧

chatgpt很快就能帮咱们实现一个

代码语言:javascript
复制
#include <immintrin.h>

// Function to perform popcnt using AVX-512 for 64-bit integers
void avx512_popcnt_epi64(const uint64_t *input, uint64_t *output, size_t size) {
    for (size_t i = 0; i < size; i += 8) { // Process 8 x 64-bit integers at a time
        __m512i data = _mm512_loadu_si512(&input[i]); // Load 512 bits (8 x 64-bit integers)
        __m512i result = _mm512_popcnt_epi64(data);   // Perform popcnt on each 64-bit integer
        _mm512_storeu_si512(&output[i], result);      // Store the results
    }
}

但很多硬件是不支持avx512的(比如arm), 怎么办?模拟,只需要avx2就行

但数字大于512呢,怎么拆分呢?

Harley-Seal算法 和 Faster Population Counts Using AVX2 Instructions[1]

如果没有avx512也可以avx2的话类似_mm256_shuffle_epi8也可以利用上

借助 PSHUFB可以多组popcnt 甚至可以自己主动划分组搞流水线 这里引入Harley-Seal算法

核心思想就是 Carry-Save Adder(CSA):

给定三个数 a b c 那他们和可以分成两部分

代码语言:javascript
复制
void CSA (uint64_t * h , uint64_t * l, uint64_t a , uint64_t b , uint64_t c) {
    uint64_t u = a ˆ b;
    *h = (a & b) | (u & c);
    *l = u ˆ c;
}

具体实现类似这样

代码语言:javascript
复制
uint64_t harley_seal ( uint64_t * d , size_t size ) {
    uint64_t total = 0, ones = 0, twos = 0,
    fours = 0, eights = 0, sixteens = 0;
    uint64_t twosA , twosB , foursA , foursB , eightsA , eightsB ;
    for ( size_t i = 0; i < size - size % 16; i += 16) {
        CSA (& twosA , & ones , ones , d[i +0] , d[i +1]) ;
        CSA (& twosB , & ones , ones , d[i +2] , d[i +3]) ;
        CSA (& foursA , & twos , twos , twosA , twosB );
        CSA (& twosA , & ones , ones , d[i +4] , d[i +5]) ;
        CSA (& twosB , & ones , ones , d[i +6] , d[i +7]) ;
        CSA (& foursB , & twos , twos , twosA , twosB );
        CSA (& eightsA , & fours , fours , foursA , foursB );
        CSA (& twosA , & ones , ones , d[i +8] , d[i +9]) ;
        CSA (& twosB , & ones , ones , d[i +10] , d[i +11]) ;
        CSA (& foursA , & twos , twos , twosA , twosB );
        CSA (& twosA , & ones , ones , d[i +12] , d[i +13]) ;
        CSA (& twosB , & ones , ones , d[i +14] , d[i +15]) ;
        CSA (& foursB , & twos , twos , twosA , twosB );
        CSA (& eightsB , & fours , fours , foursA , foursB );
        CSA (& sixteens , & eights , eights , eightsA , eightsB );
        total += count ( sixteens );
    }
    total = 16 * total + 8 * count ( eights ) + 4 * count ( fours ) + 2 * count ( twos ) + count ( ones );
    for ( size_t i = size - size % 16 ; i < size ; i ++)
    total += count (d[i ]) ;
    return total ;
}

这个算法可以三个数驱动,那自然可以pipeline话 另外这个CSA也可以用avx2或者avx512重写

比如 avx2

代码语言:javascript
复制
#include <immintrin.h>
#include <stdint.h>
#include <stddef.h>

// Carry-Save Adder (CSA) implementation using AVX2
void CSA(__m256i* h, __m256i* l, __m256i a, __m256i b, __m256i c) {
    __m256i u = _mm256_xor_si256(a, b);
    *h = _mm256_or_si256(_mm256_and_si256(a, b), _mm256_and_si256(u, c));
    *l = _mm256_xor_si256(u, c);
}

__m256i count ( __m256i v) {
    __m256i lookup =
        _mm256_setr_epi8 (0 , 1, 1, 2, 1, 2, 2, 3, 1, 2,
        2, 3, 2, 3, 3, 4, 0, 1, 1, 2, 1, 2, 2, 3,
        1, 2, 2, 3, 2, 3, 3, 4) ;
    __m256i low_mask = _mm256_set1_epi8 (0 x0f );
    __m256i lo = = _mm256_and_si256 (v , low_mask );
    __m256i hi = _mm256_and_si256 ( _mm256_srli_epi32
    (v , 4) , low_mask );
    __m256i popcnt1 = _mm256_shuffle_epi8 ( lookup ,
    lo );
    __m256i popcnt2 = _mm256_shuffle_epi8 ( lookup ,
    hi );
    __m256i total = _mm256_add_epi8 ( popcnt1 , popcnt2
    );
    return _mm256_sad_epu8 ( total ,
        _mm256_setzero_si256 () );
}

uint64_t avx_hs ( __m256i * d , uint64_t size ) {
    __m256i total = _mm256_setzero_si256 () ;
    __m256i ones = _mm256_setzero_si256 () ;
    __m256i twos = _mm256_setzero_si256 () ;
    __m256i fours = _mm256_setzero_si256 () ;
    __m256i eights = _mm256_setzero_si256 () ;
    __m256i sixteens = _mm256_setzero_si256 () ;
    __m256i twosA , twosB , foursA , foursB ,
    eightsA , eightsB ;
    for ( uint64_t i = 0; i < size ; i += 16) {
        CSA (& twosA , & ones , ones , d[i], d[i +1]) ;
        CSA (& twosB , & ones , ones , d[i +2] , d[i +3]) ;
        CSA (& foursA , & twos , twos , twosA , twosB );
        CSA (& twosA , & ones , ones , d[i +4] , d[i +5]) ;
        CSA (& twosB , & ones , ones , d[i +6] , d[i +7]) ;
        CSA (& foursB ,& twos , twos , twosA , twosB );
        CSA (& eightsA ,& fours , fours , foursA , foursB );
        CSA (& twosA , & ones , ones , d[i +8] , d[i +9]) ;
        CSA (& twosB , & ones , ones , d[i +10] , d[i +11]) ;
        CSA (& foursA , & twos , twos , twosA , twosB );
        CSA (& twosA , & ones , ones , d[i +12] , d[i +13]) ;
        CSA (& twosB , & ones , ones , d[i +14] , d[i +15]) ;
        CSA (& foursB , & twos , twos , twosA , twosB );
        CSA (& eightsB , & fours , fours , foursA , foursB );
        CSA (& sixteens , & eights , eights , eightsA , eightsB );
        total = _mm256_add_epi64 ( total , count ( sixteens ));
    }
    total = _mm256_slli_epi64 ( total , 4) ;
    total = _mm256_add_epi64 ( total ,
        _mm256_slli_epi64 ( count ( eights ) , 3) );
    total = _mm256_add_epi64 ( total ,
        _mm256_slli_epi64 ( count ( fours ) , 2) );
    total = _mm256_add_epi64 ( total ,
        _mm256_slli_epi64 ( count ( twos ) , 1) );
    total = _mm256_add_epi64 ( total , count ( ones ));
    return _mm256_extract_epi64 ( total , 0)
        + _mm256_extract_epi64 ( total , 1)
        + _mm256_extract_epi64 ( total , 2)
        + _mm256_extract_epi64 ( total , 3) ;
}

好了,你大概已经明白这个算法了,avx512版本就不贴了,可以看libpopcnt代码

性能数据

libpopcnt[2]

直接贴性能数据吧

Algorithm

32 B

64 B

128 B

256 B

512 B

1024 B

2048 B

4096 B

lookup-8

1.00

1.00

1.00

1.00

1.00

1.00

1.00

1.00

bit-parallel-mul

1.41

1.54

1.63

1.78

1.60

1.62

1.63

1.64

builtin-popcnt

4.75

6.36

8.58

8.55

6.72

7.60

7.88

7.94

avx2-harley-seal

1.15

1.85

3.22

4.17

8.46

10.74

12.52

13.66

avx512-harley-seal

0.35

1.49

2.54

3.83

5.63

15.12

22.18

25.60

显然 avx512-harley-seal 非常快

sse-popcnt[3]的结论差不多,就不贴数据了

算法厉害,但是用的上吗?

bitmap是经典场景了,但是bitmap不方便管理特长数据

roaringbitmap方案就是截断,优化思路和上面的论文思路相同

另外就是压缩了 bitmagic 压缩bit[4]

吸收了harley-seal的思想

引用链接

[1] Faster Population Counts Using AVX2 Instructions: https://arxiv.org/pdf/1611.07612 [2] libpopcnt: https://github.com/kimwalisch/libpopcnt [3] sse-popcnt: https://github.com/WojciechMula/sse-popcount/blob/master/results/cannonlake/cannonlake-i3-8121U-gcc-8.3.1.rst [4] bitmagic 压缩bit: https://github.com/tlk00/BitMagic

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CPP每周推送 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 考虑avx512,这个足够大了吧
  • Harley-Seal算法 和 Faster Population Counts Using AVX2 Instructions[1]
  • 性能数据
  • 算法厉害,但是用的上吗?
    • 引用链接
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档