向量化
SIMD
即 single instruction multiple data 英文首字母缩写,单指令流多数据流,也就是说一次运算指令可以执行多个数据流,一个简单的例子就是向量的加减。
ClickHouse原理解析与应用实践 中举的例子
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指令集的使用,有如下三种方式:
- 编译器优化 即使用C/C++编写程序之后,带有SIMD优化选项编译,在CPU支持的情况下,编译器按照自己的规则去优化。
- 使用intrinsic指令 参考Intel手册,针对SIMD指令,可以在编程时直接使用其内置的某些库函数,编译的时候在cpu和编译器的支持下会生成对应的SIMD指令。比如:double _mm_cvtsd_f64 该函数编译时就会翻译成指令:movsd
- 嵌入式汇编 内联汇编直接在程序中嵌入对应的SIMD指令。
一般数据库中使用第二种方法,直接在编程的时候使用向量化技术
使用intrinsic指令
例如,要使用SSE3,则
#include <tmmintrin.h>
如果不关心使用那个版本的SSE指令,则可以包含所有
#include <intrin.h>
| File | 描述 | VS | VisualStudio |
|---|---|---|---|
| intrin.h | All Architectures | 8.0 | 2005 |
| mmintrin.h | MMX intrinsics | 6.0 | 6.0 SP5+PP5 |
| xmmintrin.h | Streaming SIMD Extensions intrinsics | 6.0 | 6.0 SP5+PP5 |
| emmintrin.h | Willamette New Instruction intrinsics (SSE2) | 6.0 | 6.0 SP5+PP5 |
| pmmintrin.h | SSE3 intrinsics | 9.0 | 2008 |
| tmmintrin.h | SSSE3 intrinsics | 9.0 | 2008 |
| smmintrin.h | SSE4.1 intrinsics | 9.0 | 2008 |
| nmmintrin.h | SSE4.2 intrinsics. | 9.0 | 2008 |
| wmmintrin.h | AES and PCLMULQDQ intrinsics. | 10.0 | 2010 |
| immintrin.h | Intel-specific intrinsics(AVX) | 10.0 | 2010 SP1 |
| ammintrin.h | AMD-specific intrinsics (FMA4, LWP, XOP) | 10.0 | 2010 SP1 |
| mm3dnow.h | AMD 3DNow! intrinsics | 6.0 | 6.0 SP5+PP5 |
使用SIMD办法
- 利用优点 频繁调用的基础函数,大量的可并行计算•
- 尽量避免: SSE指令集对分支处理能力非常的差,而且从128位的数据中提取某些元素数据的代价又非常的大,因此不适合有复杂逻辑的运算。
doris实现向量化
介绍
为什么要介绍doris,因为doris刚开始向量化改造,所以可以看到很多思路 相关文档如下
实现方式
- 为什么是列式存储 因为向量化计算就是一次取多个数据,列式存储特别适合,如果不适配,加速效果很不好
- 向量化之后函数的编写方式都会改变,因为分支预测会变少,使用的底层函数也不同
代码分析
starrocks向量化
也是通过 算子、表达式、存储三个方面来进行向量化,starrocks的完成度比doris高很多,大家有兴趣可以比较分析下代码
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)表示:将a和b进行与运算,即r=a&b
...
可以看到,内部实现用了上面这些函数, 这些都是 SSE2 Intrinsics的函数 有个这个文档
从这个方面来看,这个实现是非常麻烦的,因为需要改写每个算子,而一个数仓中,大概有几百个算子,每个都得改写,,工作量可想而知,这也是为什么数据仓库软件动不动就 百万行代码的原因之一。
引用
15721.courses.cs.cmu.edu/spring2019/…
15721.courses.cs.cmu.edu/spring2019/…