filter range实现优化

基本代码都来自于 StarRocks

benchmark代码放在 Github

所谓filter range就是类似下面这样的代码:

普通实现版本如下:

for (auto i = start_offset; i < to; ++i) {
    if (f_data[i]) {
        *(data + result_offset) = *(data + i);
        result_offset++;
    }
}

在这个基础上,我们可以使用SIMD来做些优化:

完整代码如下,这个 `phmap::priv::BitMask` 其实就是上面那个实现:

        if (mask == 0) {
            // all no hit, pass
        } else if (mask == 0xffffffff) {
            // all hit, copy all
            memmove(data + result_offset, data + start_offset, kBatchNums * data_type_size);
            result_offset += kBatchNums;
        } else {
#define BITMASK_COPY(mask)                                            \
    {                                                                 \
        phmap::priv::BitMask<uint32_t, 32> bitmask(mask);             \
        for (auto idx : bitmask) {                                    \
            *(data + result_offset++) = *(data + start_offset + idx); \
        }                                                             \
    }
            BITMASK_COPY(mask);
        }

下面所有的改进都是基于bitmask那个的改进,整个外层框架还是非常好的。


一个实现是branchless. 可以想得到这种实现,如果mask里面bits比较多的话,还是比较合算的。如果bits比较少的话,那么就有许多重复次的拷贝和运算。

#define BRANCHLESS_COPY()                  \
    {                                      \
        int j = start_offset;              \
        for (int i = 0; i < 32; i++) {     \
            data[result_offset] = data[j]; \
            result_offset += f_data[j];    \
            j += 1;                        \
        }                                  \
    }

另外一个实现则需要CPU支持AVX512F特性,具体来说就是用到这个指令: `_mm512_mask_compress_epi32` 和 `_mm512_mask_compress_epi64`. 这两个指令可以支持32bit/64bit的压缩。如果需要支持epi8/epi16压缩的话,那么就需要AVX512_VBMI(icelake)特性,我看了下我们开发机器还不支持。

#define AVX512_COPY(SHIFT, MASK, WIDTH)                                         \
    {                                                                           \
        auto m = (mask >> SHIFT) & MASK;                                        \
        if (m) {                                                                \
            __m512i dst;                                                        \
            __m512i src = _mm512_loadu_epi##WIDTH(data + start_offset + SHIFT); \
            dst = _mm512_mask_compress_epi##WIDTH(dst, m, src);                 \
            _mm512_storeu_epi##WIDTH(data + result_offset, dst);                \
            result_offset += __builtin_popcount(m);                             \
        }                                                                       \
    }

如果要使用 intrinsic 的话,那么需要在编译参数里面增加 `-mavx512f`. 但是如果使用这个编译参数,如果代码里面使用了 `__AVX512F__` 的话那么就会生成avx512f代码。但是这里我们想根据运行时动态判断,这样的话可以使用内联汇编来搞,并且外部增加运行时CPU的特性判断。

#define AVX512_ASM_COPY(SHIFT, MASK, WIDTH, WIDTHX)               \
    {                                                             \
        auto m = (mask >> SHIFT) & MASK;                          \
        if (m) {                                                  \
            T* src = data + start_offset + SHIFT;                 \
            T* dst = data + result_offset;                        \
            __asm__ volatile("vmovdqu" #WIDTH                     \
                             " (%[s]), %%zmm1\n"                  \
                             "kmovw %[mask], %%k1\n"              \
                             "vpcompress" #WIDTHX                 \
                             " %%zmm1, %%zmm0%{%%k1%}%{z%}\n"     \
                             "vmovdqu" #WIDTH " %%zmm0, (%[d])\n" \
                             : [ s ] "+r"(src), [ d ] "+r"(dst)   \
                             : [ mask ] "r"(m)                    \
                             : "zmm0", "zmm1", "memory");         \
            result_offset += __builtin_popcount(m);               \
        }                                                         \
    }

运行时CPU特性判断可以参考 这里 的 `__builtin_cpu_supports` 函数(注意需要初始化调用 `__builtin_cpu_init` ,不过我测试了如果不调用好像也没有问题)


benchmark里面,我根据期望生成了不同的filter. 比如 `branchless/16` 就表示每个32个bit里面平均就有16个bit,想看看在不同的bitmask密度的情况下,各种实现的性能情况。

在8bit/16bit这个级别上,我对比了bitmask和branchless两个版本,有这个几个发现:

Pasted-Image-20231225105112.png

在32bit/64bit级别上,则对比了3个版本,有这么几个发现:

  1. branchless和bitmask差别和上麦差不多,branchless只有在 `bits=16` 的时候才更好。
  2. 32bit上,似乎只有 `bits=1` 的情况会稍差写,其他情况都比bitmask好。
  3. 64bit上,似乎要满足 `bits>=8` 才会比bitmask好。

Pasted-Image-20231225105108.png

Pasted-Image-20231225105124.png