简介: 向量化引擎为PolarDB-X的表达式计算带来了显著的性能提高。
PolarDB-X是阿里巴巴自研的云原生分布式数据库,采用了计算-存储分离的架构,其中计算节点承担着大量的表达式计算任务。这些表达式计算涉及到SQL执行的各个环节,对性能有着重要的影响。为此PolarDB-X引入向量化执行引擎,为表达式计算带来了几十倍的性能提高。数据库
传统数据库执行器的缺陷数组
现代数据库系统的执行引擎,大多采用一次计算一行数据(Tuple-at-a-time)的处理方式,而且须要在运行时对数据类型进行解析和判断,来适应复杂的表达式结构。咱们称之为“标量(scalar)表达式”。这种方式虽然易于实现、结构清晰,可是当须要处理的数据量增大时,它具备显著的缺陷:数据结构
为了适应复杂的表达式结构,计算一条表达式每每须要引入大量的指令;对于行式执行来讲,处理单条数据须要算子树从新进行指令解释(instruction interpretation),从而带来了大量的指令解释开销。据论文《MonetDB/X100: Hyper-Pipelining Query Execution》统计,在MySQL执行TPC-H测试集的 Query1 时,指令解释就耗费了90%的执行时间。架构
此外,在最初的Volcano结构设计中,算子内部逻辑并无避免分支预测(branch prediction)。错误的分支预测须要CPU终止当前的流水线,将ELSE语句中的指令从新载入,咱们将这一过程称为pipeline flush或pipeline break。频繁的分支预测错误会严重影响数据库的执行性能。分布式
向量化执行系统函数
数据库向量化执行系统最先由论文《MonetDB/X100: Hyper-Pipelining Query Execution》提出,它有如下几个要点:oop
向量化引擎为PolarDB-X的表达式计算带来了显著的性能提高。在下图中,横轴为向量大小,纵轴为吞吐量,不一样标量表达式和向量化表达式的性能测试对比结果以下:性能
case表达式性能测试对比结果以下:测试
PolarDB-X中,向量化表达式的执行分为如下几个阶段:fetch
数据结构
在PolarDB-X向量化执行系统中,采用如下的数据结构来存放数据:
向量化表达式执行时,全部的数据都会存放在batch这一数据结构中。batch由许多向量(vector)和一个selection数组而组成。其中,向量vector包括一个存储特定类型的数值列表(values)和一个标识null值位置的null数组组成,它们在内存中都是连续存储的。null数组中的bit位以0和1来区分数值列表中的某个位置是否为空值。
咱们能够用vector(type, index)来标识batch中一个向量。每一个向量有其特定的下标位置(index),来表示向量在batch中的顺序;类型信息(type)来指定向量的类型。在进行向量化表达式求值以前,咱们须要遍历整个表达式树,根据每一个表达式的操做数和返回值来分配好下标位置,最后根据下标位置统一为向量分配内存。
延迟物化
selection数组的设计体现了延迟物化的思想,参考论文《Materialization Strategies in a Column-Oriented DBMS》。所谓延迟物化,就是尽量地将物化(matrialization)这一过程后推,减小内存访问带来的开销。在执行表达式计算时,每每会先通过Filter表达式过滤一部分数据,再对过滤后的数据执行求值处理;每次过滤都会影响到batch中全部的向量。以上图中的batch为例,若是咱们针对第0个向量设置 vector(int, 0) != 1这一过滤条件,假设vector(int, 0)中有90%的数据知足该过滤条件(选择率selectivity = 0.9),那么咱们须要将batch中全部向量90%的数据从新物化到另外一块内存中。而若是咱们只记录知足该过滤条件的位置,存入selection数组,咱们就能够避免这一物化过程。相应的,之后每次向量化求值过程当中,都须要参考此selection数组。
向量化原语
向量化原语是向量化执行系统中的执行单位,它最大程度限制了执行期间的自由度。原语不用关注上下文信息,也不用在运行时进行类型解析和函数调用,只须要关注传入的向量便可。它是类型特定(Type-Specific)的,即一类原语只能处理特定类型。
向量化原语的主体是Tight-Loop的代码结构。在一个循环体内部,只须要进行取值和运算便可,没有任何的分支运算和函数调用。一个简单的向量化原语结构以下所示:
map_plus_double_col_double_col(int n, double*__restrict__ res, double*__restrict__ vector1, double*__restrict__ vector2, int*__restrict__ selection) { if (selection) { for(int j=0;j<n; j++) { int i = selection[j]; res[i] = vector1[i] + vector2[i]; } } else { for(int i=0;i<n; i++) res[i] = vector1[i] + vector2[i]; } }
注:*左右滑动阅览
其运算过程利用了selection数组,逐步对向量进行取值、运算和存值,以下图所示:
向量化原语带来了如下优势:
咱们为各类标量化表达式提供相应的原语实现,从而完成从标量到向量化的转变。例如将加法运算 plus(Object, Object) 针对不一样操做数类型生成原语,包括plus(double,double),plus(long, long)等。
短路求值
在向量化原语的基础上,咱们能够进一步对分支运算(也称为控制流运算 Control-Flow)进行短路求值(short-circuit calculation)优化,提高表达式计算的性能。
例如,case 表达式由n个when表达式、n-1个then表达式、1个else表达式构成。对于表达式
select case when a > 1 then a * 2 when b > 1 then b * 2 else a * b
具备如下树形结构:
因为标量化表达式按照volcano结构编排,并提供了统一的next()的接口,case表达式必须执行完全部的子表达式a>1,a2,b>1,b2和a*b以后,将所有结果汇总到一块儿,最后作case语义处理。这种执行方式不能根据when表达式的处理结果及时终止计算过程,而是对所有子表达式无差异执行。
引入向量化执行器之后,咱们能够设计短路求值来优化此问题,每个子表达式须要被提供合适的selection数组,从而正确选择列中合适的位置来进行向量运算。
做者:君启
原文连接本文为阿里云原创内容,未经容许不得转载