数仓向量化相关总结

403 阅读4分钟

向量化

SIMD

即 single instruction multiple data 英文首字母缩写,单指令流多数据流,也就是说一次运算指令可以执行多个数据流,一个简单的例子就是向量的加减。

ClickHouse原理解析与应用实践 中举的例子 image.png

SSE 与 SMID 关系

SSE(为Streaming SIMD Extensions的缩写)是由 Intel公司在1999年推出Pentium III处理器时,同时推出的新指令集。如同其名称所表示的,SSE是一种SIMD指令集。SSE有8个128位寄存器,XMM0 ~XMM7。可以用来存放四个32位的单精确度浮点数。可以看出,SSE 是一套专门为 SIMD(单指令多数据)架构设计的指令集。通过它,用户可以同时在多个数据片段上执行运算,实现数据并行(aka:矢量处理)。

SSE2是SSE指令的升级版,寄存器与指令格式都和SSE一致,不同之处在于其能够处理双精度浮点数等更多数据类。SSE3增加了13条新的指令。

SSE指令集实现了SIMD技术

C++使用SIMD编程的3种方法

SIMD指令集的使用,有如下三种方式:

  1. 编译器优化 即使用C/C++编写程序之后,带有SIMD优化选项编译,在CPU支持的情况下,编译器按照自己的规则去优化。
  2. 使用intrinsic指令 参考Intel手册,针对SIMD指令,可以在编程时直接使用其内置的某些库函数,编译的时候在cpu和编译器的支持下会生成对应的SIMD指令。比如:double _mm_cvtsd_f64 该函数编译时就会翻译成指令:movsd
  3. 嵌入式汇编 内联汇编直接在程序中嵌入对应的SIMD指令。

一般数据库中使用第二种方法,直接在编程的时候使用向量化技术

使用intrinsic指令

例如,要使用SSE3,则

#include <tmmintrin.h>

如果不关心使用那个版本的SSE指令,则可以包含所有

#include <intrin.h>
File描述VSVisualStudio
intrin.hAll Architectures8.02005
mmintrin.hMMX intrinsics6.06.0 SP5+PP5
xmmintrin.hStreaming SIMD Extensions intrinsics6.06.0 SP5+PP5
emmintrin.hWillamette New Instruction intrinsics (SSE2)6.06.0 SP5+PP5
pmmintrin.hSSE3 intrinsics9.02008
tmmintrin.hSSSE3 intrinsics9.02008
smmintrin.hSSE4.1 intrinsics9.02008
nmmintrin.hSSE4.2 intrinsics.9.02008
wmmintrin.hAES and PCLMULQDQ intrinsics.10.02010
immintrin.hIntel-specific intrinsics(AVX)10.02010 SP1
ammintrin.hAMD-specific intrinsics (FMA4, LWP, XOP)10.02010 SP1
mm3dnow.hAMD 3DNow! intrinsics6.06.0 SP5+PP5

使用SIMD办法

  1. 利用优点 频繁调用的基础函数,大量的可并行计算•
  2. 尽量避免: SSE指令集对分支处理能力非常的差,而且从128位的数据中提取某些元素数据的代价又非常的大,因此不适合有复杂逻辑的运算。

doris实现向量化

介绍

为什么要介绍doris,因为doris刚开始向量化改造,所以可以看到很多思路 相关文档如下

  1. www.bilibili.com/video/BV11L… 思路视频
  2. www.modb.pro/doc/53025 ppt相关
  3. github.com/apache/incu… 代码

实现方式

image.png

  1. 为什么是列式存储 因为向量化计算就是一次取多个数据,列式存储特别适合,如果不适配,加速效果很不好
  2. 向量化之后函数的编写方式都会改变,因为分支预测会变少,使用的底层函数也不同 image.png

image.png

代码分析

starrocks向量化

也是通过 算子、表达式、存储三个方面来进行向量化,starrocks的完成度比doris高很多,大家有兴趣可以比较分析下代码

blog.bcmeng.com/post/starro…

ck实现向量化

介绍

向量化的集大成者就是clickhouse olap数据库 这也是clickhouse为什么这么快的原因之一

例子

下面是 clickhouse/src/Functions/LowerUpperImpl.h 文件

#pragma once
#include <Columns/ColumnString.h>


namespace DB
{

template <char not_case_lower_bound, char not_case_upper_bound>
struct LowerUpperImpl
{
    static void vector(const ColumnString::Chars & data,
        const ColumnString::Offsets & offsets,
        ColumnString::Chars & res_data,
        ColumnString::Offsets & res_offsets)
    {
        res_data.resize(data.size());
        res_offsets.assign(offsets);
        array(data.data(), data.data() + data.size(), res_data.data());
    }

    static void vectorFixed(const ColumnString::Chars & data, size_t /*n*/, ColumnString::Chars & res_data)
    {
        res_data.resize(data.size());
        array(data.data(), data.data() + data.size(), res_data.data());
    }

private:
    static void array(const UInt8 * src, const UInt8 * src_end, UInt8 * dst)
    {
        const auto flip_case_mask = 'A' ^ 'a';

#ifdef __SSE2__
        const auto bytes_sse = sizeof(__m128i);
        const auto src_end_sse = src_end - (src_end - src) % bytes_sse;

        const auto v_not_case_lower_bound = _mm_set1_epi8(not_case_lower_bound - 1);
        const auto v_not_case_upper_bound = _mm_set1_epi8(not_case_upper_bound + 1);
        const auto v_flip_case_mask = _mm_set1_epi8(flip_case_mask);

        for (; src < src_end_sse; src += bytes_sse, dst += bytes_sse)
        {
           ** /// load 16 sequential 8-bit characters
            const auto chars = _mm_loadu_si128(reinterpret_cast<const __m128i *>(src));

            /// find which 8-bit sequences belong to range [case_lower_bound, case_upper_bound]
            const auto is_not_case
                = _mm_and_si128(_mm_cmpgt_epi8(chars, v_not_case_lower_bound), _mm_cmplt_epi8(chars, v_not_case_upper_bound));

            /// keep `flip_case_mask` only where necessary, zero out elsewhere
            const auto xor_mask = _mm_and_si128(v_flip_case_mask, is_not_case);

            /// flip case by applying calculated mask
            const auto cased_chars = _mm_xor_si128(chars, xor_mask);

            /// store result back to destination
            _mm_storeu_si128(reinterpret_cast<__m128i *>(dst), cased_chars);**
        }
#endif

        for (; src < src_end; ++src, ++dst)
            if (*src >= not_case_lower_bound && *src <= not_case_upper_bound)
                *dst = *src ^ flip_case_mask;
            else
                *dst = *src;
    }
};

}

是ck中的一个算子,我们可以看到其中使用了向量化技术,主要体现在内部实现上

_mm_loadu_si128表示:Loads 128-bit value;即加载128位值
_mm_and_si128(a,b)表示:将ab进行与运算,即r=a&b   
...

可以看到,内部实现用了上面这些函数, 这些都是 SSE2 Intrinsics的函数 有个这个文档

blog.csdn.net/fengbingchu…

从这个方面来看,这个实现是非常麻烦的,因为需要改写每个算子,而一个数仓中,大概有几百个算子,每个都得改写,,工作量可想而知,这也是为什么数据仓库软件动不动就 百万行代码的原因之一。

引用

www.cnblogs.com/xidian-wws/…

www.jianshu.com/p/fc384f18f…

15721.courses.cs.cmu.edu/spring2019/…

15721.courses.cs.cmu.edu/spring2019/…

to be continued