SQL优化之火山模型、向量化、编译执行

article/2025/9/12 20:50:24

文章目录

    • 1.当代CPU特性
    • 2.查询执行模型
    • 3.向量化VS编译执行
    • 4.编译执行融合向量化
    • 5.优化方向

1.当代CPU特性

向量化执行和编译执行是目前主流的两种数据库执行引擎优化手段。

了解CPU特性可以让我们真正理解各种数据库执行引擎优化技术的动机。

影响数据库执行引擎执行效率的CPU特性主要有以下几点:

  • 超标量流水线与乱序执行、
  • 分支预测、
  • 多级存储与数据预取、
  • SIMD

超标量流水线与乱序执行

  • CPU指令执行可以分为多个阶段(如取址、译码、取数、运算等);
    (1)流水线的意思是指
    一套控制单元可以同时执行多个指令,只是每个指令处在不同的阶段,例如上一条指令处理到了取数阶段,下一条指令处理到了译码阶段。
    (2)超标量的意思是指
    一个CPU核同时有多套控制单元,因此可以同时有多个流水线并发执行。
    CPU维护了一个乱序执行的指令窗口,窗口中的无数据依赖的指令就会被取来并发执行。
  • 程序做好以下两个方面,可以提高超标量流水线的吞吐(IPC,每时钟周期执行指令数)。
    (1)流水线不要断,不需要等到上一条指令执行完,就可以开始执行下一条指令。这意味着程序分支越少越好(知道下一条指令在哪)。
    (2)并发指令越多越好。指令之间没有依赖,意味着更流畅的流水线执行,以及在多个流水线并发执行。

分支预测

  • 程序分支越少,流水线效率越高。
    但是程序分支是不可避免的。程序分支可以分为两种,条件跳转和无条件跳转。
    (1)条件跳转来自if或switch之类的语句。
    (2)无条件跳转又可根据指令的操作数分为跳转地址还是跳转地址指针,分为直接跳转和间接跳转。
    直接跳转一般为静态绑定的函数调用,间接跳转来自函数返回或虚函数之类动态绑定的函数调用。
  • 当执行一个跳转指令时,在得到跳转的目的地址之前,不知道该从哪取下一条指令,流水线就只能空缺等待。
    为了提高这种情况下的流水线效率,CPU引入了一组寄存器,用来专门记录最近几次某个地址的跳转指令的目的地址。
    这样,当再一次执行到这个跳转指令时,就直接从上次保存的目的地址出取指令,放入流水线。等到真正获取到目的地址的时候,再看如果取错了,则推翻当前流水线中的指令,取真正的指令执行。

多级存储与数据预取

  • 多级存储就不用解释了,当数据在寄存器、cache或内存中,CPU取数速度不在一个数量级。
    尤其cache和内存访问,相差两个数量级。CPU在内存取数的时候会首先从cache中查找数据是否存在。若不存在,则访问内存,在访问内存的同时将访问的数据所在的一个内存块一起载入cache。
  • 如果程序访问数据存在线性访问的模式,CPU会主动将后续的内存块预先载入cache,这就是CPU的数据预取。
    有时候程序访问数据并不是线性的,例如Hash表查找等。CPU也提供了数据预取指令,程序可以事先主动将会用到的数据载入cache,这就是Software Prefetch。
  • 如何利用好寄存器和cache是数据库查询执行非常重要的优化方向。

SIMD

  • 单指令多数据流,对于计算密集型程序来说,可能经常会需要对大量不同的数据进行同样的运算。SIMD引入之前,执行流程为同样的指令重复执行,每次取一条数据进行运算。
    例如有8个32位整形数据都需要进行移位运行,则由一条对32位整形数据进行移位的指令重复执行8次完成。
    SIMD引入了一组大容量的寄存器,一个寄存器包含832位,可以将这8个数据按次序同时放到一个寄存器。同时,CPU新增了处理这种832位寄存器的指令,可以在一个指令周期内完成8个数据的位移运算。

  • 如何利用好SIMD也是不少数据库的优化方向,尤其是向量化执行的策略下。

2.查询执行模型

火山模型

  • 数据库查询执行最著名的是火山模型,也是在各种数据库系统中应用最广泛的模型。

  • SQL查询在数据库中经过解析,会生成一棵查询树,查询数的每个节点为代数运算符(Operator)。
    火山模型把Operator看成迭代器,每个迭代器都会提供一个next() 接口。

  • 一般Operator的next() 接口实现分为三步
    (1)调用子节点Operator的next() 接口获取一行数据(tuple)
    (2)对tuple进行Operator特定的处理(如filter 或project 等)
    (3)返回处理后的tuple。

  • 因此,查询执行时会由查询树自顶向下的调用next() 接口,数据则自底向上的被拉取处理。

  • 火山模型的这种处理方式也称为拉取执行模型(Pull Based)。

  • 火山模型的优点是,处理逻辑清晰,每个Operator 只要关心自己的处理逻辑即可,耦合性低。

  • 火山模型的缺点:
    数据以行为单位进行处理,不利于CPU cache 发挥作用。且每处理一行需要调用多次next() 函数,而next()为虚函数,开销大。
    每次都是计算一个 tuple(Tuple-at-a-time),这样会造成多次调用 next ,也就是造成大量的虚函数调用,这样会造成 CPU 的利用率不高。
    (C++ 中用 virtual 标记的函数,而在 Java 中没有 final 修饰的普通方法(没有标记为 static、native)都是虚函数。)

  • 为了更好地理解一个 Operator 中发生了什么,下面通过伪代码来理解 Select Operator:

Tuple Select::next() {while (true) {Tuple candidate = child->next(); // 从子节点中获取 next tupleif (candidate == EndOfStream) // 是否得到结束标记return EndOfStream;if (condition->check(candidate)) // 是否满足过滤条件return candidate; // 返回 tuple}
}
  • eg:
SELECT Id, Name, Age, (Age - 30) * 50 AS Bonus
FROM People
WHERE Age > 30

对应火山模型如下:

  • User:客户端;
  • Project:垂直分割(投影),选择字段;
  • Select(或 Filter):水平分割(选择),用于过滤行,也称为谓词;
  • Scan:扫描数据。
  • 过程:
    这里包含了 3 个 Operator:
    首先 User 调用最上方的 Operator(Project)希望得到 next tuple;
    Project 调用子节点(Select);
    而 Select 又调用子节点(Scan);
    Scan 获得表中的 tuple 返回给 Select,Select 会检查是否满足过滤条件,如果满足则返回给 Project,如果不满足则请求 Scan 获取 next tuple。
    Project 会对每一个 tuple 选择需要的字段或者计算新字段并返回新的 tuple 给 User。当 Scan 发现没有数据可以获取时,则返回一个结束标记告诉上游已结束。
  • tuple含义:
    元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行
    在这里插入图片描述

编译执行

  • 考虑到火山模型大量虚函数调用导致的性能损失,推送执行模型(Push Based)很好的解决了这个问题。与拉取模型相反,推送模型自低向上的执行,执行逻辑由底层Operator开始,其处理完一个tuple之后,将tuple传给上层Operator处理

  • 前面CPU的多级存储介绍提到,数据访问速度最快的是寄存器。
    所以在执行查询树时最理想的情况就是数据一直留在寄存器中(假设寄存器的容量足以放下一个tuple),每个Operator直接处理寄存器中的数据。
    Operator之间从拉取模型的虚函数调用,变成了以数据为中心(data-centric)的顺序执行。

  • 当然,并不是所有的Operator的运算逻辑都可以处理完寄存器中的tuple之后,把tuple留在寄存器中,由下一个Operator 接着处理。
    例如Join的时候,需要构建hash表,tuple就必须写入内存了(整个hash表当然不可能放到寄存器)。

  • [3]论文的思想:
    每个Operator会根据规则拆分为两个代码块,一块对应Produce() ,一块对应consume()。代码生成的时候就可以根据这个规则生成代码。
    Produce()函数负责产生结果tuple;
    Consume()函数负责具体的tuple处理逻辑;
    好处:编译执行以数据为中心,消灭了火山模型中的大量虚函数调用开销。甚至使大部分指令执行,可以直接从寄存器取数,极大的提高了执行效率。

向量化执行

  • 向量化执行依然采用类似火山模型的拉取式模型,唯一的区别是其Operator的next()函数每次返回的是一批数据(如1000行)

  • 一般向量化特指列式存储系统中,按列聚合的一组数据;
    在行式系统中称为RowSet迭代,本文不严格区分这两种情况

  • eg:下图是一个JoinOperator的编译生成的伪代码和向量化执行的伪代码的对比。
    在这里插入图片描述

  • 图中(a)部分为编译执行模型生成的伪代码
    JoinOperator 的执行逻辑为,以左表的数据构建Hash表,然后以右表中的每行记录,分别去Hash表查找。
    这里的Hash表的冲突处理采用的是链地址法,伪代码中最后一个循环就是遍历链表,找到真正的匹配项。

  • 图中(b)部分向量化执行的伪代码
    与火山模型不同的,它一次处理一组数据。
    因此,可以看到这里面的变量都是Vector。
    由于变量为Vector,就需要事先定义一些专门处理Vector的元语(Primitives):例如为Vector中的每一个元素计算Hash值的proheHash_,以及图中的compareKeys_、buildGather_。

向量化执行模型有一下几个好处:

  • 1.大大减少火山模型中的虚函数调用数量;
  • 2.以块为处理单位数据,提供了cache命中率;
  • 3.多行并发处理,契合了CPU乱序执行与并发执行的特性。
  • 4.同时处理多行数据,使SIMD有了用武之地(虽然目前SIMD对大多数数据库查询起到的作用比较有限[1],本质上数据库查询都属于数据访问密集型应用,而不是SIMD最擅长的计算密集型应用)。

3.向量化VS编译执行

首先这两个模型是不相容的,二者只能取其一。

  • 因为编译执行强调的是以数据为中心,在一个Pipeline内是不会有Materialization的;
    但是向量化执行是拉取模型,每次经过next()调用,Vector的传递必须Materialize

  • Materialization Model,物化

  • [1]的团队在同一个系统(Peloton)里实现了这两种模型,以进行各方面对比。
    [1]选取了TPC-H中非常有代表性的5个SQL查询,Q1和Q18主要运算为定点数运算(Fixed-point arithmetic)和Aggregation,Q6主要运算为Filter,Q3和Q9主要运算为Join;
    从图中可以看出向量化执行对于Q3和Q9查询更高效,而编译执行对于Q1和Q18语句更高效。
    在这里插入图片描述

  • 原因以及分析:

  • 图中的Typer是编译执行引擎,Tectorwise是向量化执行引擎。Memory Stall指的是CPU执行指令时,内存取数的等待时间。
    (1)可以看出Q3和Q9,Typer主要就是慢在了Memory Stall。因为Hash表数据分布比较随机,Hash查找时cache命中率不高,经常需要访问内存。
    编译执行循环内部会包含多个Operator的运算,这些有依赖关系的指令占据了大部分的乱序执行窗口,并发度低,总的等待取数时间就长了
    向量化执行模型的循环较短,并发度高,可以同时有更多的指令等待取数,总的等待时间就短了。
    更复杂的循环也导致分支预测失败的代价更高。
    (2)对于Q1和Q18,由于运算过程中cache命中率比较高,没有Memory Stall的拖累,编译执行模型Pipeline执行无Materialization的优势就体现出来了
    在这里插入图片描述

4.编译执行融合向量化

[2]设计了一个原型系统 Relaxed Operator Fusion (ROF)来实现编译执行融合向量化

  • 把查询树分解一下,部分用向量化方式,部分用编译执行方式即可

  • 思想:
    编译执行的主要目标是减少Materialization,ROF则是在编译执行的基础上,主动在其中的Pipeline中插入Materialization,将Pipeline分割为Stage,在Stage内依然是tuple-at-a-time data-centric的推送模型,保留了编译执行数据停留在寄存器中的优点。
    而跨Stage或Pipeline时,则以Block(一组tuple)为单位传递数据,这个时候就可以利用上SIMD。
    另外,ROF还使用了Software Prefetch来优化编译执行当Hash表查找时cache命中率低Memory Stall过多的问题。

  • eg:下面的SQL是TPC-H Q19的简化版本。
    其中CLAUSE1~3分别是LineItem和Part两个表上的查询条件。

SELECT SUM(...) AS revenue
FROM LineItem JOIN Part ON l_partkey = p_partkey
WHERE (CLAUSE1) OR (CLAUSE2) OR (CLAUSE3)

这段SQL对应的编译执行的查询数和ROF的查询树如下图。
在这里插入图片描述

  • 可以看到ROF 相比原来的查询树,在P2这个Pipeline的第一个Filter后面插入了一个Operator(图中标红)。
    这个Operator表示Vector Output,把P2分成了两个Stage(我们称这种分割Stage的Operator为Stage boundary),在Stage内部为tuple-at-a-time,跨Stage则以Vector为单位传递数据。

  • P2对应ROF的伪代码如下图。
    Stage1进行TableScan和Filter,将VECTOR_SIZE数量的Tuple插入Vector。
    Stage2对Vector中的数据进行HashProbe、Filter以及Aggregate。
    在这里插入图片描述

  • 上面的例子展示了ROF是怎么把Pipeline分割成Stage的,但是到目前为止,我们并没有看到这么做有什么好处。

  • 这里最关键的问题是,应该在Pipeline的哪个位置插入Stage boundary,才能达到最优的效果。
    ROF按照两个规则分割Stage:
    R1. 可以使用SIMD的Operator的输入和输出点;
    R2. 需要对无规律地址数据(且数据量大于Cache)进行访问的Operator的输入点。
    R1是为了利用SIMD进行并发计算,R2是为了使用Software prefetch提高cache命中率。
    这是基本策略,在实现时还有一些技术点需要考虑。为了快速获取SIMD寄存器中Filter过后的数据,ROF利用一个Mask索引,将SIMD寄存器中的有效数据Shuffle到一起。
    当数据量不大时,数据预取反而会带来额外开销。ROF在编译时会生成两套执行路径,在运行时根据数据量决定是否需要预取。

5.优化方向

  • 其次,火山模型中一次只取一条数据,如果每次取多条数据呢?
    (1)可以将每次 next 带来的 CPU 开销被一组数据给分摊。这样当 CPU 访问元组中的某个列时会将该元组加载到 CPU Cache(如果该元组大小小于 CPU Cache 缓存行的大小), 访问后继的列将直接从 CPU Cache 中获取,从而具有较高的 CPU Cache 命中率
    (2)当然是同一列的时候,所以针对的是列存的场景,因为输入是同列的一组数据,面对的是相同的操作,这正是向量寄存器干的事情,这是 CPU 层面计算性能的优化,因此称为向量化。
    并且如果每次只取一列的部分数据,返回一个可以放到 CPU Cache 的向量,那么又可以利用到 CPU Cache。

  • 向量化
    向量化计算就是将一个循环处理一个数组的时候每次处理 1 个数据共处理 N 次,转化为向量化——每次同时处理 8 个数据共处理 N/8 次,其中依赖的技术就是 SIMD(Single Instruction Multiple Data,单指令流多数据流),SIMD 可以在一条 CPU 指令上处理 2、4、8 或者更多份的数据。

CPU 层面的优化方案的结论

  • 相比火山模型,编译执行和向量化都使数据库查询执行性能得到了极大的提升,这两者之间相比又如何呢。
    首先这两个模型是不相容的,二者只能取其一。
    因为编译执行强调的是以数据为中心,在一个 CPU Pipeline 内是不会有物化的;
    向量化执行是拉模型,每次经过 next() 调用,向量的传递必须要进行物化。

  • 相关论文
    [1] Kersten, Timo, et al. “Everything you always wanted to know about compiled and vectorized queries but were afraid to ask.” Proceedings of the VLDB Endowment 11.13 (2018): 2209-2222.
    [2] Menon, Prashanth, Todd C. Mowry, and Andrew Pavlo. “Relaxed operator fusion for in-memory databases: Making compilation, vectorization, and prefetching work together at last.” Proceedings of the VLDB Endowment 11.1 (2017): 1-13.
    [3] Neumann, Thomas. “Efficiently compiling efficient query plans for modern hardware.” Proceedings of the VLDB Endowment4.9 (2011): 539-550.
    [4] Boncz, Peter A., Marcin Zukowski, and Niels Nes. “MonetDB/X100: Hyper-Pipelining Query Execution.” Cidr. Vol. 5. 2005.

  • 参考:SQL 优化之火山模型,向量化与编译执行浅析


http://chatgpt.dhexx.cn/article/SGowrQSu.shtml

相关文章

UE4性能优化

UE4性能优化 参考文档:UE4性能优化GPU分析**CPU分析**一些相关工具 Time: 2021年10月19日16:46:22 Desc: UE4性能优化 参考文档: https://docs.unrealengine.com/4.27/zh-CN/TestingAndOptimization/PerformanceAndProfiling/https://blog.csdn.net/u01…

一文纵览向量检索

摘要:本文针对向量检索要解决的问题,梳理了主流向量检索相关的技术,分析了向量检索目前的一个趋势。 什么是向量检索 首先我们了解下什么是向量,所谓向量就是由n个数字(二值向量由n个比特组成)组成的数组&…

C/C++编译器并行优化技术:并行优化针对多核处理器和多线程环境进行优化,以提高程序的并行度

目录标题 引言数据并行:将数据集分割成多个子集,分配给多个线程或处理器并行处理。延迟执行与乱序执行:对指令的执行顺序进行调整,提高指令流水线的利用率和性能。延迟执行乱序执行 任务并行:将程序分解为多个独立的任…

离散与提炼——一些关于向量召回算法优化方法的思考

✏️ 作者介绍: 周语馨,高级云智能工程师 最近做的很多向量召回的相关工作,主要集中在优化 Faiss 里面常用的几个算法,包括 IVFFlat 和 IVFPQ,并且针对这两个算法都做出了专门的优化。 前一阵子灵光乍现,想…

java手动回收线程_性能优化:线程资源回收

本文来自: PerfMa技术社区 PerfMa(笨马网络)官网 一、问题 模型服务平台的排序请求出现较多超时情况,且不定时伴随空指针异常。 二、问题发生前后的改动 召回引擎扩大了召回量,导致排序请求的item数量增加了。 三、出问题的模型 基于XGBoost预测的全排序…

编译优化之 - 向量化优化入门

1. 介绍 2. Intel高级向量扩展 3. GCC中向量化 4. ICC中向量化 5. AOCC/LLVM中向量化 1. 介绍 什么是自动向量化? 自动向量化(automatic vectorization)是自动并行化(automatic parallelization)的一种特殊情况&#…

数据库向量化如何进行性能优化

数据库向量化如何进行性能优化 前面提到,数据库向量化是一个巨大的、系统的性能优化工程,两年来,我们实现了数百个大大小小的优化点。我将 StarRocks 向量化两年多的性能优化经验总结为 7 个方面 (注意,由于向量化执行…

simulink 状态空间加反馈报错

状态空间模型(可控)通过状态反馈或输出反馈可以自由配置极点和特征向量,得到理想的运动状态,通过计算得到的反馈增益矩阵K便可构建simulink模型,但常常报错,原因如下: 上图展示的是simulink模型…

状态空间方程MATLAB语句

1.连续系统 (1)使用系数矩阵获得传递函数 [num,den]ss2tf(A,B,C,D); (2)将传递函数写成因式分解(零极点)形式 [z,p,k]ss2zp(A,B,C,D) 或者 [z,p,k]tf2zp(num,den) (3)将给定形式…

基于matlab的系统状态空间转化

前段时间学习了一些关于通过系统状态方程判断系统可控性和可观测性,并由此求出其传递函数,基于传递函数判断其稳定性的一些知识。 一、常用的数学模型转换函数: 常用数学模型转换函数 ss2tf 将系统状态空间…

连续状态空间模型离散化

对于某状态空间模型: 其中: 将该连续模型离散化:(代码如下) clc;clear;close all A[-11.6028 7.1632 ;6.4909 -27.837 ]; B[3.086;5.4458]; [F,G]c2d(A,B,0.02) %0.02为采样周期 运行结果如下:&#xff0…

基于状态空间的数字控制器的设计

目录 一.目的... 2 二.内容... 2 三.实验过程与结果... 2 1.推算出控制对象的模型参数... 2 2. 状态空间的数字控制器的设计原理... 2 3、实验代码(借鉴PPT内容)... 3 4、仿真图像... 3 5、仿真结果…

人工智能:状态空间图(超详细经典例题讲解,通过例题教会你如何解决状态空间图问题)

1、定义 状态空间常记为三元组&#xff1a;<S&#xff0c;F&#xff0c;G>。 其中&#xff0c;S为初始状态的集合&#xff0c;F为操作的集合&#xff0c;G为目标状态的集合。 问题的状态空间图是一个描述该问题全部可能的状态及相互关系的有向图。 如考虑操作的代价&…

状态空间模型

&#xfeff;&#xfeff; 一、状态空间模型简述 状态空间模型是动态时域模型&#xff0c;以隐含着的时间为自变量。状态空间模型包括两个模型&#xff1a; 一是状态方程模型&#xff0c;反映动态系统在输入变量作用下在某时刻所转移到的状态&#xff1b; 二是输出或量测方程模…

什么是状态空间法

1 状态空间法 在经典控制理论中&#xff0c;在建立数学模型时是通过传递函数进行的&#xff0c;在这个过程中&#xff0c;只考虑输入和输出之间的关系&#xff0c;所以会将系统变成一个黑盒子&#xff0c;里面的内容被浓缩了。 而在现代控制理论中&#xff0c;会首先从系统中抽…

动态系统建模-状态空间方程

动态系统建模-状态空间方程 状态空间方程是现代控制理论的基础, 它以矩阵的形式表达系统状态变量、 输入及输出之间的关系。 它可以描述和处理多输入多输出(MultipleInput Multiple Output, MIMO) 的系统。 状态空间方程 单输入单输出(SingleInput Single Output,SISO) 系统…

人工智能——状态空间表示法

状态空间表示法 状态空间表示法引入问题状态空间的构成状态算符状态空间问题的解 状态空间法表示问题的步骤状态空间方法表示问题的步骤如下 利用状态空间求解问题的过程利用状态空间表示法解题示例状态空间表示法简要小结 状态空间表示法引入 状态空间表示法就是以 “ 状态空间…

传递函数与状态空间

传递函数与状态空间之间可相互转换&#xff0c;可以使用的matlab函数有 [A,B,C,D] tf2ss(NUM,DEN) [NUM,DEN] ss2tf(A,B,C,D,iu)传递函数的形式唯一&#xff0c;但状态空间的形式不唯一&#xff0c;可以有多种。 1、一阶惯性环节 时间常数为T&#xff0c;本身为低通滤波器&…

状态空间搜索

http://www.lencomputer.com/xk2008/lesson19/search_algorithm.htm 状态空间搜索是程序设计中的最基本方法之一。它通过在状态空间中的初始状态出发&#xff0c;按照一定的顺序和条件对空间中的状态进行遍历&#xff0c;最终找到目标状态。一般的状态空间搜索方法有枚举、深度…

状态空间

1. 定义 状态变量(state variables)是指在系统中所含变量个数最少的变量&#xff0c;也就是决定系统状态的最小数目的变量的有序集合&#xff0c;有时也称为状态向量&#xff08;state vector&#xff09;&#xff0c;例如表示天体运动状态的位置和速度的变量。状态变量表示系统…