聊聊ClickHouse向量化执行引擎-过滤操作

简介: 聊聊ClickHouse向量化执行引擎-过滤操作

俄罗斯Yandex开发的ClickHouse是一款性能黑马的OLAP数据库,其对SIMD的灵活运用给其带来了难以置信的性能。本文我们聊聊它如何对过滤操作进行SIMD优化。


基本思想


1、有一个数组data,即ColumnVector::data,存放数据2、使用uint8类型,即1个字节类型的filter数组:ICloumn::Filter。他的大小是data数组大小,里面存放布尔值,标记data数组里面哪些数据满足过滤条件,应该筛选出来3、最终生成一个新的数组res,根据filter数组,对data数组进行筛选,满足条件的拷贝到res数组中。标量逻辑的简单伪码:

    let res = []
    for (let i = 0; i < data.size(); i ++) {
        if (filter[i] != 0) {
            res.append(data[i])
        }
    }

    Clickhouse如何实现的呢?4、上面代码耗时因素在于循环次数非常多,等于data数组的大小5、如果可以降低循环次数,同时保证单次循环耗时变化不大,总体执行效率更高。要满足上面条件,需要在同一个指令周期做更多运算,SIMD指令可以做这样的运算。6、SIMD指令目前最大支持512位数据,而filter本身一个值为8位,单词循环处理数据量为512 / 8 = 64个7、每次取出来64个filter数组项(64字节),将其组成一个64位无符号整数值mask,这样每个filter数组项占用一个比特位8、有两种特殊情况:1)mask 64位比特位都是1,本次循环中,64个data项都应该拷贝到res中。2)mask 64位比特位都是0,可以直接跳过循环。当然,这两种特殊情况经常出现在业务常见中9、第三中情况是有一部分满足条件,此时是否需要循环64次?有没有进一步的优化方法?看看clickhouse咋处理10、有多少项需要拷贝到新数组,取决于mask中比特位为1的个数,通过__builtin_clzll内置函数得到入参(64位)二进制表示形式中开头0的个数,从而得到最高比特位为1的索引,继而知道哪项数据需要拷贝。11、最高1比特位的数据项拷贝后,需要将它置成0,这里有2个比较高效的方法blsr函数:一个是_blsr_u64指令,另一个是mask & (mask-1)12、循环设置最高1的比特位,直到mask中所有比特位都为0


    ColumnVector<T>::filter函数


    过滤函数主要是filter函数。其实分为3部分,AVX512VBMI2指令集、默认的操作和尾部数据处理。其中尾部数据处理是指处理数据不够64个时,剩余的部分处理方式,这种方式无法使用SIMD,沿用标量处理方式。先看下默认操作方式:doFilterAligned即:模板函数

     

    这部分其实是对有一部分值满足条件场景的优化,主要有3个方面:1)前导0个数,即data数组data[0]--data[i]都满足条件,需要拷贝到结果中2)后导0个数,即data数组data[i]--data[63]都满足条件,需要拷贝到结果中3)其他场景,比如0011 1100的场景,即两边都有不满足条件的,那就需要特殊处理了:计算出最低位起0的个数index,然后将data_pos[index]拷贝到结果中,即该数组满足条件,最后将index位置为0。前缀和后缀拷贝的判断:

    蓝色框表示的意义:其实是去除前导0后,剩余的都是1,即mask值。也就是从0的索引开始,到64 - leading_zeroes都需要拷贝到结果中。蓝框计算效果,以2个字节大小为例,前导5个0: 

    后导0的处理:其实可以调用__buitlin_ctzll函数


    uint8_t suffixToCopy(UInt64 mask)
    {
      const auto prefix_to_copy = prefixToCopy(~mask);//mask值取反
      return prefix_to_copy >= 64 ? prefix_to_copy : 64 - prefix_to_copy;//需要拷贝个数
    }

    效果如下图所示:

    64字节值转换成64位掩码值的计算函数Bytes64MaskToBits64Mask实现也很有讲究:

      /// Transform 64-byte mask to 64-bit mask
      inline UInt64 bytes64MaskToBits64Mask(const UInt8 * bytes64)
      {
      #if defined(__AVX512F__) && defined(__AVX512BW__)
          const __m512i vbytes = _mm512_loadu_si512(reinterpret_cast<const void *>(bytes64));
          UInt64 res = _mm512_testn_epi8_mask(vbytes, vbytes);
      #elif defined(__AVX__) && defined(__AVX2__)
          const __m256i zero32 = _mm256_setzero_si256();
          UInt64 res =
              (static_cast<UInt64>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(
              _mm256_loadu_si256(reinterpret_cast<const __m256i *>(bytes64)), zero32))) & 0xffffffff)
              | (static_cast<UInt64>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(
              _mm256_loadu_si256(reinterpret_cast<const __m256i *>(bytes64+32)), zero32))) << 32);
      #elif defined(__SSE2__)
          const __m128i zero16 = _mm_setzero_si128();
          UInt64 res =
              (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(
              _mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64)), zero16))) & 0xffff)
              | ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(
              _mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 16)), zero16))) << 16) & 0xffff0000)
              | ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(
              _mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 32)), zero16))) << 32) & 0xffff00000000)
              | ((static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpeq_epi8(
              _mm_loadu_si128(reinterpret_cast<const __m128i *>(bytes64 + 48)), zero16))) << 48) & 0xffff000000000000);
      #elif defined(__aarch64__) && defined(__ARM_NEON)
          const uint8x16_t bitmask = {0x01, 0x02, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80};
          const auto * src = reinterpret_cast<const unsigned char *>(bytes64);
          const uint8x16_t p0 = vceqzq_u8(vld1q_u8(src));
          const uint8x16_t p1 = vceqzq_u8(vld1q_u8(src + 16));
          const uint8x16_t p2 = vceqzq_u8(vld1q_u8(src + 32));
          const uint8x16_t p3 = vceqzq_u8(vld1q_u8(src + 48));
          uint8x16_t t0 = vandq_u8(p0, bitmask);
          uint8x16_t t1 = vandq_u8(p1, bitmask);
          uint8x16_t t2 = vandq_u8(p2, bitmask);
          uint8x16_t t3 = vandq_u8(p3, bitmask);
          uint8x16_t sum0 = vpaddq_u8(t0, t1);
          uint8x16_t sum1 = vpaddq_u8(t2, t3);
          sum0 = vpaddq_u8(sum0, sum1);
          sum0 = vpaddq_u8(sum0, sum0);
          UInt64 res = vgetq_lane_u64(vreinterpretq_u64_u8(sum0), 0);
      #else
          UInt64 res = 0;
          for (size_t i = 0; i < 64; ++i)
              res |= static_cast<UInt64>(0 == bytes64[i]) << i;
      #endif
          return ~res;
      }

      我们看到,按照优先级尽可能利用位数多的指令集进行计算,同时在所有指令集都不可用及未64字节对齐(align)的部分进行了保底处理。其利用了以下指令集:    AVX512F / AVX512BW    AVX/AVX2    SSE2其中,_mm512_testn_epi8_mask函数功能:计算a和b两个入参值按8位整数逐位与(AND),产生中间8位值,如果中间值为0,则在结果掩码k中设置相应位:FOR j := 0 to 63  i := j*8  k[j] := ((a[i+7:i] AND b[i+7:i]) == 0) ? 1 : 0ENDFOR所以,bytes64MaskToBits64Mask最终计算出的值需要取反。另外,其他指令集,比如AVX下,_mm256_cmpeq_epi8比较32位是否等于0,等于0表示不满足条件,当然等于零时该函数返回0xFF,所以同样最终结果需要取反。另外一种操作方式:doFilterAligned即:模板函数

      主要是通过_mm512_mask_compressstoreu_epi8类似函数分别对1248字节长度类型针对掩码进行数据拷贝,这里不再赘述。


      参考


      https://zhuanlan.zhihu.com/p/454657709https://blog.csdn.net/u010002184/article/details/113977586https://blog.51cto.com/u_15103025/2643507https://zhuanlan.zhihu.com/p/449154820https://www.zhihu.com/question/450069375/answer/1813516193https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html

      目录
      相关文章
      |
      2月前
      |
      存储 SQL 消息中间件
      ClickHouse(12)ClickHouse合并树MergeTree家族表引擎之AggregatingMergeTree详细解析
      AggregatingMergeTree是ClickHouse的一种表引擎,它优化了MergeTree的合并逻辑,通过将相同主键(排序键)的行聚合为一行并存储聚合函数状态来减少行数。适用于增量数据聚合和物化视图。建表语法中涉及AggregateFunction和SimpleAggregateFunction类型。插入数据需使用带-State-的聚合函数,查询时使用GROUP BY和-Merge-。处理逻辑包括按排序键聚合、在合并分区时计算、以分区为单位聚合等。常用于物化视图配合普通MergeTree使用。查阅更多资料可访问相关链接。
      117 4
      |
      2月前
      |
      存储 SQL 算法
      ClickHouse(13)ClickHouse合并树MergeTree家族表引擎之CollapsingMergeTree详细解析
      CollapsingMergeTree是ClickHouse的一种表引擎,它扩展了`MergeTree`,通过折叠行来优化存储和查询效率。当`Sign`列值为1和-1的成对行存在时,该引擎会异步删除除`Sign`外其他字段相同的行,只保留最新状态。建表语法中,`sign`列必须为`Int8`类型,用来标记状态(1)和撤销(-1)。写入时,应确保状态和撤销行的对应关系以保证正确折叠。查询时,可能需要使用聚合函数如`sum(Sign * x)`配合`GROUP BY`来处理折叠后的数据。使用`FINAL`修饰符可强制折叠,但效率较低。系列文章提供了更多关于ClickHouse及其表引擎的详细解析。
      63 1
      |
      2月前
      |
      传感器 存储 SQL
      ClickHouse(15)ClickHouse合并树MergeTree家族表引擎之GraphiteMergeTree详细解析
      GraphiteMergeTree是ClickHouse用于优化Graphite数据存储和汇总的表引擎,适合需要瘦身和高效查询Graphite数据的开发者。它基于MergeTree,减少存储空间并提升查询效率。创建表时需包括Path、Time、Value和Version列。配置涉及pattern、regexp、function和retention,用于指定聚合函数和数据保留规则。文章还提供了建表语句示例和相关资源链接。
      39 1
      |
      2月前
      |
      存储 SQL 关系型数据库
      ClickHouse(11)ClickHouse合并树MergeTree家族表引擎之SummingMergeTree详细解析
      `SummingMergeTree`是`MergeTree`引擎的变种,它合并相同主键的行并计算数值列的总和,从而节省存储空间和加速查询。通常与`MergeTree`配合使用,存储聚合数据以避免数据丢失。创建`SummingMergeTree`表时,可选参数`columns`指定要汇总的数值列。未指定时,默认汇总所有非主键数值列。注意,聚合可能不完整,查询时需用`SUM`和`GROUP BY`。文章还介绍了建表语法、数据处理规则以及对嵌套数据结构和`AggregateFunction`列的处理。查阅更多ClickHouse相关内容可访问相关链接。
      113 5
      |
      2月前
      |
      SQL Oracle 关系型数据库
      实时计算 Flink版产品使用问题之如何对ClickHouse进行操作
      实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
      |
      2月前
      |
      存储 SQL 算法
      ClickHouse(14)ClickHouse合并树MergeTree家族表引擎之VersionedCollapsingMergeTree详细解析
      VersionedCollapsingMergeTree是ClickHouse的一种优化引擎,扩展了MergeTree,支持多线程异步插入和高效的数据折叠。它通过Sign和Version列处理对象状态的变化,Sign表示行的状态(正向或撤销),Version追踪状态版本。引擎自动删除旧状态,减少存储占用。在查询时,需注意可能需使用GROUP BY和聚合函数确保数据折叠,因为ClickHouse不保证查询结果已折叠。文章还提供了建表语法、使用示例和相关资源链接。
      63 0
      |
      3月前
      |
      SQL 消息中间件 关系型数据库
      ClickHouse(10)ClickHouse合并树MergeTree家族表引擎之ReplacingMergeTree详细解析
      `ReplacingMergeTree`是ClickHouse的一种表引擎,用于数据去重。与`MergeTree`不同,它在合并分区时删除重复行,但不保证无重复。去重基于`ORDER BY`列,在ver列未指定时保留最新行,否则保留ver值最大者。数据处理策略包括延迟合并导致的不确定性及按分区去重。`CREATE TABLE`语法中,`ReplacingMergeTree`需要指定可选的`ver`列。相关系列文章提供了更深入的解析。
      109 0
      |
      3月前
      |
      存储 SQL 关系型数据库
      ClickHouse(09)ClickHouse合并树MergeTree家族表引擎之MergeTree详细解析
      ClickHouse的MergeTree系列引擎是其高性能大数据存储的核心,特别适合大量数据的快速插入。数据按主键排序,支持分区和数据副本,提供数据采样功能。建表时,通过`ENGINE = MergeTree()`指定引擎,`ORDER BY`指定排序键,可选`PARTITION BY`分区,`SAMPLE BY`进行采样。此外,MergeTree支持多种索引和设置,如`index_granularity`控制索引粒度。查询时,ClickHouse利用主键和索引来高效检索数据,尤其在使用等值或范围条件时。
      30 0
      |
      3月前
      |
      SQL 关系型数据库 MySQL
      实时计算 Flink版产品使用合集之sql读取mysql写入clickhouse,该如何操作
      实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStreamAPI、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
      |
      10月前
      |
      SQL 缓存 大数据
      大数据技术之Clickhouse---入门篇---SQL操作、副本
      大数据技术之Clickhouse---入门篇---SQL操作、副本