精华内容
下载资源
问答
  • 文章目录 什么是向量化查询执行 列式存储 Spark向量化查询执行 Hive向量化查询执行 参考 什么是向量化查询执行 在标准的查询执行系统中,每次只处理一行数据,每次处理都要走过较长的代码路径和元数据解释,从而导致...

    什么是向量化查询执行

    在标准的查询执行系统中,每次只处理一行数据,每次处理都要走过较长的代码路径和元数据解释,从而导致CPU使用率非常低。

    而在向量化查询执行中,每次处理包含多行记录的一批数据,每一批数据中的每一列都会被存储为一个向量(一个原始数据类型的数组),这就极大地减少了执行过程中的方法调用、反序列化和不必要的if-else操作,大大减少CPU的使用时间。

    如下图所示:
    vector array
    上图中可以看出,数据加载出来之后,每批数据中的每一列都会转成一个向量,在后续的执行过程中,数据是一个批批从一个操作符流经另一个操作符,而不是一行行的。
    batch
    但是,向量化查询执行有一个限制,就是我们必须把要查询的数据存储为列式格式。比如

    • 磁盘列式存储格式:ORC、Parquet
    • 内存列式存储格式:Arrow

    列式存储

    列式存储与行式存储将数据按行进行存储不同的是,它将数据以每列进行组织。如下图:
    在这里插入图片描述
    列式存储的优势:

    1. 相同类型的数据放在一起进行存储,可以有更高的压缩率,减少了磁盘占用。
    2. 只返回指定查询的列,而不用返回整行记录之后再取出指定查询的列,最小化了I/O操作。

    了解了列式存储的存储格式,我们就很容易明白,在使用列式存储格式的情况下,向量化查询才能方便地将每批数据中的每列值进行向量化。

    Spark向量化查询执行

    Spark支持三种形式的向量化查询:

    1. 内存列式存储
      spark.sql.inMemoryColumnarStorage.enableVectorizedReader:为内存列式存储(比如前面提到的Apache Arrow)启用向量化读取,默认为true,从Spark 2.3.1开始支持此配置。
    2. Parquet格式
      spark.sql.parquet.enableVectorizedReader:为parquet格式启用向量化查询,默认为true,从Spark 2.0.0开始支持此配置。
    3. ORC格式
      spark.sql.orc.enableVectorizedReader:为ORC格式启用向量化查询,默认为true,从Spark 2.3.0开始支持此配置。

    Hive向量化查询执行

    Hive中,向量化查询执行在Hive 0.13.0及以后版本可用,默认情况下是关闭的,可通过下面配置启用:

    set hive.vectorized.execution.enabled = true;
    

    Hive向量化执行支持数据类型有:tinyint、smallint、int、bigint、boolean、float、double、decimal、date、timestamp、string。

    如果使用了其他数据类型,查询将不会使用向量化执行,而是每次只会查询一行数据。

    可以通过explain来查看查询是否使用了向量化,如果输出信息中Execution mode的值为vectorized,则使用了向量化查询:

    explain select count(*) from vectorizedtable;
    
    // explain输出信息
    STAGE PLANS:
      Stage: Stage-1
        Map Reduce
          Alias -> Map Operator Tree:
          ...
          Execution mode: vectorized
          Reduce Operator Tree
          ... 
    ... 
    

    参考

    展开全文
  • 具体用法 关闭向量化执行引擎: set hive.vectorized.execution.enabled = false; –开启向量化查询开关 ...开启hive.vectorized.execution.enabled操作,默认是关闭状态,将一个普通的查询转化为向量化查询执行.

    具体用法

    关闭向量化执行引擎: 

    set hive.vectorized.execution.enabled = false;

    –开启向量化查询开关
    set hive.vectorized.execution.enabled=true;
    select s_age, max(s_desc) num from student_tb_orc group by s_age;

    开启hive.vectorized.execution.enabled操作,默认是关闭状态,将一个普通的查询转化为向量化查询执行是一个Hive 特性。它大大减少了扫描、过滤器、聚合和连接等典型查询操作的CPU 使用。标准查询执行系统一次处理一行。矢量化查询执行可以一次性处理1024行的数据块,以减少底层操作系统处理数据时的指令和上下文切换 

     

    什么是向量化查询执行

    在标准的查询执行系统中,每次只处理一行数据,每次处理都要走过较长的代码路径和元数据解释,从而导致CPU使用率非常低。

    而在向量化查询执行中,每次处理包含多行记录的一批数据,每一批数据中的每一列都会被存储为一个向量(一个原始数据类型的数组),这就极大地减少了执行过程中的方法调用、反序列化和不必要的if-else操作,大大减少CPU的使用时间。

    如下图所示:

    上图中可以看出,数据加载出来之后,每批数据中的每一列都会转成一个向量,在后续的执行过程中,数据是一个批批从一个操作符流经另一个操作符,而不是一行行的。

    但是,向量化查询执行有一个限制,就是我们必须把要查询的数据存储为列式格式。比如

    • 磁盘列式存储格式:ORC、Parquet
    • 内存列式存储格式:Arrow

    列式存储

    列式存储与行式存储将数据按行进行存储不同的是,它将数据以每列进行组织。如下图:

    列式存储的优势:

    1. 相同类型的数据放在一起进行存储,可以有更高的压缩率,减少了磁盘占用。
    2. 只返回指定查询的列,而不用返回整行记录之后再取出指定查询的列,最小化了I/O操作。

    了解了列式存储的存储格式,我们就很容易明白,在使用列式存储格式的情况下,向量化查询才能方便地将每批数据中的每列值进行向量化。

    Spark向量化查询执行

    Spark支持三种形式的向量化查询:

    1. 内存列式存储
      spark.sql.inMemoryColumnarStorage.enableVectorizedReader:为内存列式存储(比如前面提到的Apache Arrow)启用向量化读取,默认为true,从Spark 2.3.1开始支持此配置。
    2. Parquet格式
      spark.sql.parquet.enableVectorizedReader:为parquet格式启用向量化查询,默认为true,从Spark 2.0.0开始支持此配置。
    3. ORC格式
      spark.sql.orc.enableVectorizedReader:为ORC格式启用向量化查询,默认为true,从Spark 2.3.0开始支持此配置。

    Hive向量化查询执行

    Hive中,向量化查询执行在Hive 0.13.0及以后版本可用,默认情况下是关闭的,可通过下面配置启用:

    set hive.vectorized.execution.enabled = true;
    

    Hive向量化执行支持数据类型有:tinyint、smallint、int、bigint、boolean、float、double、decimal、date、timestamp、string。

    如果使用了其他数据类型,查询将不会使用向量化执行,而是每次只会查询一行数据。

    可以通过explain来查看查询是否使用了向量化,如果输出信息中Execution mode的值为vectorized,则使用了向量化查询:

    explain select count(*) from vectorizedtable;
    
    // explain输出信息
    STAGE PLANS:
      Stage: Stage-1
        Map Reduce
          Alias -> Map Operator Tree:
          ...
          Execution mode: vectorized
          Reduce Operator Tree
          ... 
    ... 

     

    展开全文
  • 另外,ClickHouse为了最大限度地压榨硬件——尤其是CPU——的性能,实现了向量化查询执行(vectorized query execution)机制。这个名词相对于上面的那些可能没那么平易近人,但它毫无疑问是CK相对于传统OLAP引擎的...

    SIMD即"single instruction, multiple data"(单指令流多数据流),是Flynn分类法对计算机的四大分类之一。它本质上是采用一个控制器来控制多个处理器,同时对一组数据中的每一条分别执行相同的操作,从而实现空间上的并行性的技术。

    可见,“单指令流”指的是同时只能执行一种操作,“多数据流”则指的是在一组同构的数据(通常称为vector,即向量)上进行操作,如下图所示,PU=processing unit。

    在这里插入图片描述

    二、SIMD在现代计算机的应用甚广泛,最典型的则是在GPU的像素处理流水线中。举个例子,如果要更改一整幅图像的亮度,只需要取出各像素的RGB值存入向量单元(向量单元很宽,可以存储多个像素的数据),再同时将它们做相同的加减操作即可,效率很高。SIMD和MIMD流水线是GPU微架构的基础,就不再展开聊了。

    话说回来,CPU是如何实现SIMD的呢?答案是扩展指令集。Intel的第一版SIMD扩展指令集称为MMX,于1997年发布。后来至今的改进版本有SSE(Streaming SIMD Extensions)、AVX(Advanced Vector Extensions),以及AMD的3DNow!等。ClickHouse的向量化执行机制主要依赖于SSE指令集,下面简要介绍之。

    三、ClickHouse提供的检查CPU是否支持SSE4.2的命令如下。

    grep -q sse4_2 /proc/cpuinfo && echo “SSE 4.2 supported” || echo “SSE 4.2 not supported”

    SSE指令集以8个128位寄存器为基础,命名为XMM0XMM7。在AMD64(即64位扩展)指令集中,又新增了XMM8XMM15。一个XMM寄存器原本只能存储一种数据类型:

    4个32位单精度浮点数
    SSE2又扩展到能够存储以下类型:

    2个64位双精度浮点数
    2个64位/4个32位/8个16位整数
    16个字节或字符

    四、那么如何利用SSE指令集呢?主要有以下3种方法:

    1.直接编写(内嵌)汇编语句;
    2.利用厂商提供的扩展库函数。Intel将这类指令和函数统称为intrinsics,官方提供的速查手册见这里;
    3.开启编译器的优化(-msse、-msse2等等),编译器会自动将符合条件的情景(如数组相加、矩阵相乘等)编译为intrinsic指令。

    五、ClickHouse向量化执行示例

    计算Filter中1的数量
    在ClickHouse的底层,过滤器(Filter)是一个预分配空间的、无符号8位整形数的数组,用于表示WHERE和HAVING子句的条件及真值,每一位的取值为0或1,即表示条件为假或真。Filter和列(IColumn)是共生的,在ColumnsCommon.cpp中,提供了通用的计算Filter中1的数量的方法,代码如下。

     size_t countBytesInFilter(const IColumn::Filter & filt)
    {
    	size_t count = 0;
    
    /** NOTE: In theory, `filt` should only contain zeros and ones.
      * But, just in case, here the condition > 0 (to signed bytes) is used.
      * It would be better to use != 0, then this does not allow SSE2.
      */
    
    const Int8 * pos = reinterpret_cast<const Int8 *>(filt.data());
    const Int8 * end = pos + filt.size();
    
    #if defined(__SSE2__) && defined(__POPCNT__)
    const __m128i zero16 = _mm_setzero_si128();
    const Int8 * end64 = pos + filt.size() / 64 * 64;
    
    for (; pos < end64; pos += 64)
        count += __builtin_popcountll(
            static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos)),
                zero16)))
            | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 16)),
                zero16))) << 16)
            | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 32)),
                zero16))) << 32)
            | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 48)),
                zero16))) << 48));
    
          /// TODO Add duff device for tail?
         #endif
     
        for (; pos < end; ++pos)
            count += *pos > 0;
     
        return count;
    }
    

    defined(SSE2)说明当前环境支持SSE2指令集,而defined(POPCNT)说明支持硬件级位计数的POPCNT指令。下面根据手册简要介绍一下代码中涉及到的intrinsic函数:

    _mm_setzero_si128():初始化128位(16字节)的全0位图,即一个XMM寄存器。
    _mm_loadu_si128(mem_addr):从内存地址mem_addr处加载128位的整形数据。
    _mm_cmpgt_epi8(a, b):按8位比较a和b两个128位整形数,若a的对应8位比b的对应8位大,则填充对应位为全1,否则填充全0。
    _mm_movemask_epi8(a):根据128位整形数a的每个8位组的最高位创建掩码,一共16位长,返回int结果(高16位用0填充)。
    最后,__builtin_popcountll()函数相当于直接调用POPCNT指令算出64位数的汉明权重。

    由上可见,这个函数的每次循环都将连续64个Filter的真值数据(即Int8类型)压缩到一个UInt64中一起做位计数。其中每次调用上述指令都会处理16个Int8,正好是128位,SIMD的思想就是这样体现出来的。由于SSE指令集中没有真正的位运算指令,所以压缩的过程略显繁琐,但是仍然比笨方法(逐个遍历判断)效率高很多。

    六、经过多次测试,不使用SSE的版本的耗时总是使用SSE的版本的3倍多。鉴于ClickHouse在很多地方都渗透了SIMD和SSE,积少成多,效率提升自然就非常可观了。

    展开全文
  • 另外,ClickHouse为了最大限度地压榨硬件——尤其是CPU——的性能,实现了向量化查询执行(vectorized query execution)机制。这个名词相对于上面的那些可能没那么平易近人,但它毫无疑问是CK相对于传统OLAP引擎的...

    前言

    ClickHouse之所以会像闪电一样快("blazing fast"),是多方面优化的结果,包括且不限于:高效且磁盘友好的列式存储,高效的数据压缩,精心设计的各类索引,并行分布式查询,运行时代码生成等。

    另外,ClickHouse为了最大限度地压榨硬件——尤其是CPU——的性能,实现了向量化查询执行(vectorized query execution)机制。这个名词相对于上面的那些可能没那么平易近人,但它毫无疑问是CK相对于传统OLAP引擎的大杀器。鉴于现有资料中讲解CK向量化执行的内容很少,本文就来试图探究一下,先从基础知识SIMD说起。

    SIMD

    SIMD即"single instruction, multiple data"(单指令流多数据流),是Flynn分类法对计算机的四大分类之一。它本质上是采用一个控制器来控制多个处理器,同时对一组数据中的每一条分别执行相同的操作,从而实现空间上的并行性的技术。

    可见,“单指令流”指的是同时只能执行一种操作,“多数据流”则指的是在一组同构的数据(通常称为vector,即向量)上进行操作,如下图所示,PU=processing unit。

     

    SIMD在现代计算机的应用甚广泛,最典型的则是在GPU的像素处理流水线中。举个例子,如果要更改一整幅图像的亮度,只需要取出各像素的RGB值存入向量单元(向量单元很宽,可以存储多个像素的数据),再同时将它们做相同的加减操作即可,效率很高。SIMD和MIMD流水线是GPU微架构的基础,就不再展开聊了。

    话说回来,CPU是如何实现SIMD的呢?答案是扩展指令集。Intel的第一版SIMD扩展指令集称为MMX,于1997年发布。后来至今的改进版本有SSE(Streaming SIMD Extensions)、AVX(Advanced Vector Extensions),以及AMD的3DNow!等。ClickHouse的向量化执行机制主要依赖于SSE指令集,下面简要介绍之。

    SSE指令集

    SSE指令集是MMX的继任者,其第一版早在Pentium III时代就被引入了。随着新指令的扩充,又有了SSE2、SSE3、SSSE3、SSE4(包含4.1和4.2)等新版本。我们可以通过cpuid类软件获得处理器对SSE指令集的支持信息,下图以笔者自用MacBook Pro中的Intel Core i9-9880H为例。

     

    并不仅有Intel的处理器才支持SSE指令集,AMD的同样也支持。下图以笔者游戏PC中的AMD Ryzen 5 3600为例。

     

    ClickHouse提供的检查CPU是否支持SSE4.2的命令如下。

     

    grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 not supported"
    

    SSE指令集以8个128位寄存器为基础,命名为XMM0~XMM7。在AMD64(即64位扩展)指令集中,又新增了XMM8~XMM15。一个XMM寄存器原本只能存储一种数据类型:

    • 4个32位单精度浮点数

    SSE2又扩展到能够存储以下类型:

    • 2个64位双精度浮点数
    • 2个64位/4个32位/8个16位整数
    • 16个字节或字符

    SSE的指令分为两大类,一是标量(scalar)指令,二是打包(packed)指令。标量指令只对XMM寄存器中的最低位数据进行计算,打包指令则是对所有数据进行计算。下图示出SSE1中,单精度浮点数乘法的标量和打包运算。

     

    图来自http://www.songho.ca/misc/sse/sse.html,是一篇很好的SSE入门

    观察指令名称,mul表示乘法,接下来的s表示标量,p表示打包,最后一个s则表示类型为单精度浮点数(single-precision)。由图也可以发现,打包指令才是真正SIMD的,而标量指令是SISD的。

    再举个小栗子,如果我们要实现两个4维向量v1和v2的加法,只需要三条SSE指令就够了。

     movaps xmm0, [v1] ;xmm0 = v1.w | v1.z | v1.y | v1.x 
     addps xmm0, [v2]  ;xmm0 = v1.w+v2.w | v1.z+v2.z | v1.y+v2.y | v1.x+v2.x
     movaps [vec_res]  ;xmm0
    

    注意数据移动指令movaps中的a表示对齐(align)。第一条指令的意思就是通过[v1]直接寻址得到向量的起点,并分别按照0、4、8、16字节的偏移量写入XMM0寄存器的低到高四个域。在数据本身已经按照16字节对齐的情况下,调用这种指令效率非常高。从寄存器写入内存也是同理的,如下图。

     

    那么如何利用SSE指令集呢?主要有以下3种方法:

    • 直接编写(内嵌)汇编语句;
    • 利用厂商提供的扩展库函数。Intel将这类指令和函数统称为intrinsics,官方提供的速查手册见这里
    • 开启编译器的优化(-msse-msse2等等),编译器会自动将符合条件的情景(如数组相加、矩阵相乘等)编译为intrinsic指令。

    需要注意的是,SIMD和SSE虽然强大,但是对于那些严重依赖流程控制(flow-control-heavy)的任务,即有大量分支、跳转和条件判断的任务明显不太适用。也就是说,它们主要被用来优化可并行计算的简单场景,以及可能被频繁调用的基础逻辑。

    说了这么多,可以进入ClickHouse的世界了。

    ClickHouse向量化执行示例

    编译器优化对笔者这种水平不高的人来说显然太难,所以还是去ClickHouse源码中寻找向量化执行的蛛丝马迹比较好。我们可以查找形如#ifdef __SSE2__的宏定义,或者根据手册查找intrinsic函数对应的头文件,如SSE4.1的头文件是<smmintrin.h>,以此类推。

    为简单起见,选取两段应用了SSE2 intrinsics函数的示例来作分析。

    计算Filter中1的数量

    在ClickHouse的底层,过滤器(Filter)是一个预分配空间的、无符号8位整形数的数组,用于表示WHERE和HAVING子句的条件及真值,每一位的取值为0或1,即表示条件为假或真。Filter和列(IColumn)是共生的,在ColumnsCommon.cpp中,提供了通用的计算Filter中1的数量的方法,代码如下。

     

    size_t countBytesInFilter(const IColumn::Filter & filt)
    {
        size_t count = 0;
    
        /** NOTE: In theory, `filt` should only contain zeros and ones.
          * But, just in case, here the condition > 0 (to signed bytes) is used.
          * It would be better to use != 0, then this does not allow SSE2.
          */
    
        const Int8 * pos = reinterpret_cast<const Int8 *>(filt.data());
        const Int8 * end = pos + filt.size();
    
    #if defined(__SSE2__) && defined(__POPCNT__)
        const __m128i zero16 = _mm_setzero_si128();
        const Int8 * end64 = pos + filt.size() / 64 * 64;
    
        for (; pos < end64; pos += 64)
            count += __builtin_popcountll(
                static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                    _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos)),
                    zero16)))
                | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                    _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 16)),
                    zero16))) << 16)
                | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                    _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 32)),
                    zero16))) << 32)
                | (static_cast<UInt64>(_mm_movemask_epi8(_mm_cmpgt_epi8(
                    _mm_loadu_si128(reinterpret_cast<const __m128i *>(pos + 48)),
                    zero16))) << 48));
    
        /// TODO Add duff device for tail?
    #endif
    
        for (; pos < end; ++pos)
            count += *pos > 0;
    
        return count;
    }
    

    defined(__SSE2__)说明当前环境支持SSE2指令集,而defined(__POPCNT__)说明支持硬件级位计数的POPCNT指令。下面根据手册简要介绍一下代码中涉及到的intrinsic函数:

    • _mm_setzero_si128():初始化128位(16字节)的全0位图,即一个XMM寄存器。
    • _mm_loadu_si128(mem_addr):从内存地址mem_addr处加载128位的整形数据。
    • _mm_cmpgt_epi8(a, b):按8位比较a和b两个128位整形数,若a的对应8位比b的对应8位大,则填充对应位为全1,否则填充全0。
    • _mm_movemask_epi8(a):根据128位整形数a的每个8位组的最高位创建掩码,一共16位长,返回int结果(高16位用0填充)。

    最后,__builtin_popcountll()函数相当于直接调用POPCNT指令算出64位数的汉明权重。

    由上可见,这个函数的每次循环都将连续64个Filter的真值数据(即Int8类型)压缩到一个UInt64中一起做位计数。其中每次调用上述指令都会处理16个Int8,正好是128位,SIMD的思想就是这样体现出来的。由于SSE指令集中没有真正的位运算指令,所以压缩的过程略显繁琐,但是仍然比笨方法(逐个遍历判断)效率高很多。

    字符串大小写转换

    ClickHouse的函数中也充分应用了SSE指令集,特别是字符串函数。以ASCII拉丁字符大小写转换函数(即toLower()toUpper())为例,其源码如下,位于LowerUpperImpl.h文件中。

     

    template <char not_case_lower_bound, char not_case_upper_bound>
    struct LowerUpperImpl
    {
    // 略去
    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;
        }
    };
    

    原理比较简单,就是利用flip_case_mask这个掩码来翻转字符的大小写(大小写字母的ASCII码值相差32)。not_case_lower_bound和not_case_lower_bound则用来标定转换的字符范围,比如a~z或A~Z。不过在SSE2的加持下,就可以一次性加载16个字符进行转换,同样是SIMD的思想,效率自然比普通的遍历高。由于每句话都有官方注释,就不再多废话了。

    测试一下

    将上面的LowerUpperImpl结构体拆出来,测试代码如下。

     

    #include <iostream>
    #include <string>
    #include <vector>
    #include <chrono>
    using namespace std;
    using namespace std::chrono;
    
    #ifdef __SSE2__
    #include <emmintrin.h>
    #endif
    
    template <char not_case_lower_bound, char not_case_upper_bound>
    struct LowerUpperImpl {
      public:
      static void arraySSE(const char * src, const char * src_end, char * 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) {
          const auto chars = _mm_loadu_si128(reinterpret_cast<const __m128i *>(src));
    
          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));
    
          const auto xor_mask = _mm_and_si128(v_flip_case_mask, is_not_case);
    
          const auto cased_chars = _mm_xor_si128(chars, xor_mask);
    
          _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;
      }
    
      static void arrayNoSSE(const char * src, const char * src_end, char * dst) {
        const auto flip_case_mask = 'A' ^ 'a';
        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;
      }
    };
    
    int main() {
      char src[261] = {'\0'};
      char dst[261] = {'\0'};
    
      for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 26; j++) {
          src[i * 26 + j] = 'a' + j;
        }
      }
    
      LowerUpperImpl<'a', 'z'> lowerUpper;
    
      auto start1 = system_clock::now();
      for (int i = 0; i < 100; i++) {
        lowerUpper.arraySSE(&src[0], &src[261], &dst[0]);
      }
      auto end1 = system_clock::now();
      cout << dst << endl;
      auto duration1 = duration_cast<nanoseconds>(end1 - start1);
      cout << "Time cost: " << duration1.count() << " ns" << endl;
    
      auto start2 = system_clock::now();
      for (int i = 0; i < 100; i++) {
        lowerUpper.arrayNoSSE(&src[0], &src[261], &dst[0]);
      }
      auto end2 = system_clock::now();
      cout << dst << endl;
      auto duration2 = duration_cast<nanoseconds>(end2 - start2);
      cout << "Time cost: " << duration2.count() << " ns" << endl;
    }
    

    很简单,就是将a~z重复10遍作为源字符串,将其分别用SSE和普通方式转换成大写100次,结果如下。

     

    /Users/lmagic/workspace/CLionProjects/ssetest/cmake-build-debug/ssetest
    ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
    Time cost: 18000 ns
    ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
    Time cost: 59000 ns
    

    经过多次测试,不使用SSE的版本的耗时总是使用SSE的版本的3倍多。鉴于ClickHouse在很多地方都渗透了SIMD和SSE,积少成多,效率提升自然就非常可观了。

    The End

    笔者并非精通C++和微处理器方面,写这篇文章只是觉得很有意思而已,看官随意看看就行了。

    民那晚安晚安。


    链接:https://www.jianshu.com/p/c36f4e142b8a
     

    展开全文
  • 通过研究我们发现Greenplum虽然提供了列存储的能力,但是实现没有向量化执行引擎提供向量化处理能力,在处理列存表的时候,是分别读取一条Tuple的各个字段内容,拼装完成后返回,后续的执行过程还是行存执行引擎完成...
  • 在三种常见的数据库查询引擎执行模型中我们讲到了向量化执行引擎本质上是一种批处理模型。批处理思想在计算机的世界里经常闪闪发光。高并发场景中,可以把大量的请求合并,改为调用批量接口;大数据下读取分布式文件...
  • 规范向量查找和向量索引扫描具有较好的代码执行效率,并且阶段的处理模型更好地适应不同的计算平台,将计算阶段分配给最适合的计算平台。同时,Array OLAP是一种面向数据仓库模式特点的设计,向量处理模型设计...
  • hive参数调优

    2020-09-21 22:37:19
    1 思路 1.1 查看explain,使用spark引擎的执行计划) 1.2 查看yarn日志 1.3 向量化查询 –开启向量化查询开关 set hive.vectorized.execution.enabled=true;...询转化为向量化查询执行是一个Hive 特性。它
  • 您必须参数化您的测试-SQL,因为在构建和执行字符串时缺少参数是一个经典的结构化查询语言注入攻击向量。 由于结构化查询语言注入,而不是因为某种东西使一种方法比另一种方法更快,所以使用直接的临时测试结构化查询...
  • 一、迭代模型/火山模型(Iterator Model) 二、物化模型(Materialization Model) 三、向量化/批处理模型(Vectorized / Batch Model)
  • 1.概述 官方原文链接:走起 点我 ...因此被称为”向量化查询执行”,相对于实际的数据处理成本,向量化处理具有更低的转发成本。 这个设计思路并不是新的思路理念。历史可以追溯到APL编程语言时代:A+, J,
  • 1 clickhouse简介 Clickhouse 是一个开源的面向联机分析处理(OLAP, On-Line Analytical Processing)的列式存储数据库管理系统。 优点 缺点 写入快、查询快 ...向量化查询执行 并非查询资源控制不好处理
  • 1 clickhouse简介 Clickhouse 是一个开源的面向联机分析处理(OLAP, On-Line Analytical Processing)的列式存储数据库管理系统。 优点 缺点 写入快、查询快 ...向量化查询执行 并非查询资源控制不好处理
  • 1 clickhouse简介 Clickhouse 是一个开源的面向联机分析处理(OLAP, On-Line Analytical Processing)的列式存储数据库管理系统。 优点 缺点 写入快、查询快 不支持事务 ... 向量化查询执行
  • Clickhouse 简介 Clickhouse 是一个开源的面向联机分析处理(OLAP, ...- 向量化查询执行 - 数据压缩 - 并行和分布式查询 - 实时数据更新 Clickhouse 的不足: - 不支持事务 - 不适合典型的 K/V 存储 - 不适合 Blob/Do..
  • clickhouse简单了解及使用

    万次阅读 2018-11-30 10:36:21
    一、clickhouse:日处理记录数:十亿级 1.开源的列式存储的数据管理系统 2.支持线性扩展 3.简单方便 4.高可靠性 5.容错(支持多主机异步复制,可以跨多个数据中心...向量化查询执行 电信 数据压缩 数据压缩 ...
  • 支持用于安全值转义的参数化查询。 可以定制以构建您选择的任何查询或命令。 使用方法链接以便于使用。 小型:〜7 KB压缩并压缩 还有更多信息, 警告:永远不要将客户端生成的查询传递给Web服务器执行。 这样的...
  • 它实现了向量化执行和SIMD指令,对内存中的列式数据,一个batch调用一次SIMD指令,大幅缩短了计算耗时,带来数倍的性能提升。目前国内社区火热,各大厂也纷纷进入该技术领域的探索。引言本文主要讨论京东黄金眼业务...
  • 向量化是随着列存数据库一起成熟的新查询执行模型,诸如Hive,Vertica,Vectorwise,Clickhouse等都使用了该技术。向量化也是 TiDB 正在进行的优化之一。本次分享将从为何进行向量化,块执行,SIMD和晚期物化等多个...
  • clickhouse_zh.pdf

    2020-05-19 17:25:00
    矢量化查询执行不是特别实用,因为它涉及必须写到缓存并读回的临时向量。如果 L2 缓存容纳不下临时数据,那么这将成为一个问题。但矢量化查询执行更容易利用 CPU 的 SIMD 功能。朋友写的一篇研究论文表明,将两种...
  • 计算依照速度和内存需求而高效的执行。从科学开发者的观点,MDP是一个模块框架,它能够被容易地扩展。新算法的实现是容易且直观的。新实现的单元然后被自动地与程序库的其余部件进行整合。MDP在神经科学的理论研究...
  • 前言ClickHouse 是一款开源列式存储的分析型数据库,相较业界OLAP数据库系统,其最核心优势就是极致的查询性能。它实现了向量化执行和SIMD指令,对内存中的列式数据,一个batc...
  • 第7章: MySQL数据库结构化查询SQL与存储过程 任务62: 新建数据库 任务63: 创建数据表 任务64: insert_into 任务65: delete_from 任务66: Day0605_update 任务67: select 任务68: where 任务69: orderby 任务...

空空如也

空空如也

1 2 3 4 5
收藏数 84
精华内容 33
关键字:

向量化查询执行