SIMD in Database.

#0. 前言背景

本文为本人在阅读由J.Zhou所著 《Implementing Database Operations Using SIMD》一文的读书笔记 。本文主要讨论了数据库中的一个重要问题,执行效率的问题:如何有效的提高系统的效率,尤其是执行效率。随着现代CPU架构中所提供的指令越来越丰富,使得我们可以利用这些指令所带来技术优势。例如:SIMD,单指令多数据。SIMD多在处理多媒体技术方面使用,SIMD充分的利用并行方式来提升我们处理数据的能力。在一条指令中可以同时处理对个数据。因此,我们可以利用该指令集来提升数据库中的执行效率。

通常情况下,SIMD指令用来提升内循环的效率。使用SIMD带来两个好处:(1)一定程度上的并行化,因此某些操作可以在一定程度上进行同时处理;(2)其会减少分支条件指令,从而可以减少由于条件分支指令错误预测所带来的损害。

其中:关于条件分支指令的优化。key words: elimination of conditional branch instructions.

考察了数据库中重要的操作:顺序扫描;聚集操作;索引操作;join操作;以观察使用simd指令是否能够对于这些操作带来相应的性能提升。

对于消除了条件分支的错误预测所带了性能呈现出线性加速的趋势。

现在cpu和内存的访问速度已经成为数据库的瓶颈。 例如许多关于cpu

cache的工作已被广泛的讨论。如下相关论文讨论cache在数据库中的应用情况。其对于B-tree索引的影响以及其对于join操作的影响。

1) Weaving relations for cache performance。

2)Improving index performance through prefetching

3)B-tree indexes and CPU caches

4)What happens during a join? Dissecting CPU and memory optimization effects

5)Making B+ trees cache conscious in main memory

通过使用simd算术操作符来减少或者避免使用分支预测指令,从而能够大幅度的提升系统的性能。

#3. SIMD技术概述.

SIMD技术运行一条指令在执行时候操作多个数据项。single instruction multi-data. 具有SIMD指令的包括:MMX, SSE,SSE2 (intel);VIS(Sun ultra-sparc); 3D now系列(AMD),还有MIPS,HP,Cyrix等等CPU提供商,详细信息见:技术报告:Multimedia extensions for general purpose microprocessors: A survey.

simd_arc

紧缩型单精度浮点数的操作。源操作数为:四个32bit的浮点数,目标操作数为包含操作结果的四个32bit的浮点数。如上图所示。
SIMD操作包括:比较操作,算术操作,移位操作,转换操作以及逻辑操作等等。

  • 对于是使用SIMD亦或是vector process(向量处理器)?

向量处理器提供里在向量上进行高层(相对应SIMD的指令系统层级而言)的并行操作,例如:线性数组的处理。一个典型的向量可以包括:64位-51264位的元素。在这些元素上,我们可以使用单独的一条指令即可对这些元素执行并行操作。

相比起向量寄存器,SIMD寄存器可以保存小数据量的元素,而向量寄存器则有一个最小数据量的限制(64 bits)。例如:在pentium 4上最多可以出来4个32bits的数据。因此,相比向量处理其而言其并行度将会变得小。但是,另一方面,由于我们所处理的数据量较小,是的在执行load指令的时候,其导致的延时将会变得小。具有较小的SIMD指令大小,使得其能够在超标量处理器中进行流水线方式执行。

  • 向量化编译器的限制

向量化编译器的作用就是自动检查出将普通的代码可以将其转为可以利用SIMD技术的指令的可能并其进行相应的转换。例如:icc。 但icc存在着:风格问题(stylistic issue),硬件问题(hardware issue), 以及复杂度问题(complexity issue)。

当我们代码中进程会出现:在内循环中进程伴有条件分支语句,而这些icc等编译器则无法正确将这些代码转换。因此,我们会尝试使用手工处理的方式,来处理类似的代码。 为了能够利用simd指令所带来的收益,那么需要我们数据库开发人员明确且正确的使用simd指令来提升相关数据库操作的性能。

  • 比较结果的格式

在intel和amd架构中,比较的结果是以元素掩码(element mask,em) 的形式表现出来。em是一个向量,每个packed element要么全是1,要么全是0. 例如在上图中如果该操作符是属于比较操作,则上述操作结果是在一个长度为128bit的4个32bit长度的 elmenet mask中,而每个em的值,要么是1,当相等时候;要么是0, 两个操作数不相等。

但是在像sun, mips等的架构中,其结果的形式是以bit vector的形式展现。由其中的每个bit来的值来代标识两个数之间的比较结果。 虽然上述说,intel和amd架构下采用的是element mask来表,但它们同样提供了bit vector模式,通过额外的指令来将element mask转换为bit vector模式。(例如:形如SIMD_bit_vector形式)。

  • 数据组织

在SIMD指令中的另外一个需要我们认真考虑的问题是:数据的对齐问题(Data alignment)。例如:VIS指令需要是8-byte的对齐数据。而SSE和SSE2指令操作的是128bits的集群器,因此,数据必须是16-bytes的对齐方式,只有这样方可最大化的利用SIMD指令的性能。虽然,我们可以利用mov指令将未对齐的数据拷贝进/出SIMD寄存器中。但是,由于未对齐数据所带来的性能上的下降是也值得引起我们的注意的。 为了能够最大化的利用SIMD所带来的性能提升,我们需要保证需要操作的源数据的连续性。

  • 条件分支误测

在现代的流水线化的cpu架构中,条件分支指令是一个非常令人头痛的问题,因为在流水线上的指令,是无法提取获取该条件分支指令的确切执行结果。因此,cpu一般采取预测的方式来,提取预测该分支指令的执行结果,如果该预测值与最后的实际结果相一致,则可以极大的提高指令的执行效率;但当该预测的值与实际值相反的时候,其所带来的性能损害也是巨大的。因为,此时条件语句之后的这些指令的执行结果将会被清空,并选择另外一条正确的指令执行线路执行。有测试表明,在pentium ii中一条错误的条件分支指令所带来大约17时钟周期的延时。 对于流水线指令系统,例如:pentium 4会带来大约20步的延时。当当前的处理器架构下,对于条件分支指令的5%错误预测将会导致20-30%的性能下降。

#4. SIMD实现.

假如我们想尝试使用simd指令,那么一种传统的方式是在我们的代价中直接使用simd指令来完成某些操作,但是直接使用simd指令进行编程不是一件容易的事情,且容易出现许多错误。为了解决这个现象,intel 编译提供了一种类似汇编功能的 c函数调用指令,instrinsics。至于gnu的g++等编译我们可以使用相关参数来来开启使用simd指令优化。 具体的使用如下链接所示。

http://stackoverflow.com/questions/661338/sse-sse2-and-sse3-for-gnu-c

  • scan-like运算符

在介绍完simd的基表情况后,下面我们就看看数据库中的那些常用的运算符是如何利用simd指令来加速系统性能。在实际的使用过程中需要我们注意一点:数据的对齐问题。我们需要对于输入数据的对齐问题引起注意。

术语:
1)其中S表示为加速比,
2)在本文中,一个基本的数值元素(整数或者浮点数)被称作是一个字(word)。
3)S通常是,一个simd寄存器所能装下的字的个数,例如:128bits的simd寄存器,其S=4;

例如对于如下的代码:

simd_code_1
其相应的使用SIMD指令的时候,形如下所示:
simd_code_2

典型地,simd指令有着丰富的功能,例如:比较操作,逻辑操作,算术操作等等。例如:对于上述第一段代码中的运算操作等,均有相应的simd指令与之相对应。 但是对于上述代码中存在着条件语句,因而对其并行化处理并非一件容易的事情。 同样对于可变长度的数据类型,如:字符串等,我们也不能对其进行很好的并行化处理。例如:对于 LIKE 语句。

需要注意的是,在SIMD算法中不能够含有if语句。如果我们能够在SIMD_Process中避免使用if语句的话,我们就可以避免条件分支语句以及由分支判定条件错误所带来的性能损害。

下面我们就来讨论一下上述代码片段中的process1以及process2函数。

  • 返回第一个匹配元素

如上述的代码片段所示,当我们的x[i]满足某个条件后返回y的第一个元素y[i]。即:process1(y) 所完成的功能。

simd_code_3
此时process2将不会被执行。 因此可以给出相应的simd版本 SIMD_Process:
simd_code_4

当我们的条件 condition 的选择较低时候,则意味着我们的mask中大部分值为0,那么(V != 0) 将可以节约大部分的工作。

存在着另外一种情况,即所谓的唯一匹配。当我们的匹配结果最多只要一个与搜索的关键字想匹配时候,我们就可以使用唯一匹配方式来进行处理。 例如,在实际的应用中的b+树。通常在其叶子节点中的数据是具有唯一性的。

例如:当 v!=0条件满足时候,在y中的S个字中只有一个是满足条件的。因此,通过上述的约束条件,我们知道有且只有一个满足条件的元组。故而,SIMD_Process的方法如下:

simd_code_5
  • 返回索引匹配元素

有指定的pos位置开始,将以pos为起点,返回所有的元素。 如下所示:

simd_code_6
其相应的SIMD版本所对应的代码如下:
simd_code_7
此外,当我们考虑到分支条件语句的时候,以及由于其内循环中的条件语句所带来的系统性能的损害。则可以SIMD_Process函数以如下方式实现:
simd_code_8

#5. SIMD版的标量聚集函数.

  • 求和以及计数

对于聚集函数来说,其对应的聚集函数都可以给出相应的SIMD版本。 例如:我们对于sum函而言,传统的求值方式如下:

simd_code_9
那么对于SIMD形式的代码如下:
simd_code_10

计数功能完全可以有上述的代码来完成,我们只要把SIMD_AND的第二参数设置为1,即可为完成计数功能。

  • 最大值和最小值

intel的simd指令集中,提供了相应的simd操作用来并行的方式计算两个simd单元对的最小值,同样也存在着最大值计算。我们初始化一个simd单元 ,inf用来表示最大值。同样我们也可以使用Inf来初始标识最小值的单元min。

因此,求解最小值的simd版本的代码如下:

simd_code_11
上述代码的基表做法是:将非匹配的元素转换为inf,因此其将不会影响到最小值的结果。 最后,在min[1…S]中包含了最小值。 同样,求解最大值的方法同求解最小值时候相类似。唯一不同的是我们使用-inf,而不是inf。同样,我们使用SIMD_MAX指令而非SIMD_MIN指令。

#6. 索引及连接处理.

  • 索引结构

现代系统架构对于的索引结构(例如:)有着非常大的影响,尤其是cache的性能。现有大量的文献均讨论了不同的索引组织形式与cpu cache性能之间的关系。使用simd指令可以提升索引遍历时候的速度。 将simd指令可以与cache-感知的索引结构结合起来,在现代大多数cpu架构上来获得提升索引操作的性能。 例如:我们可以使用并行方式计算hash值来提高创建hash索引的速度。 树型索引:ISAM, B+. 其中,的多维索引类型:Quad树,K-D-B 树, R树等。

我们在索引的变量及计算过程中经历需要避免使用if指令,因为错误的if指令会导致系统性能的极速下降。因为在现代流行线的指令系统下,错误的预测将会导致后续已执行的错误分支上的指令被废弃。

  • 索引的内部(中间)节点操作

使用simd指令来优化索引树中的查找算法。我们常用的是所谓的二分查找。与传统的每次比较一个元素而言,我们使用simd指令一次可以比较多个连续的值。 请相应的基表思想如下:我们从数组的中间部分开始进行搜索,如果我们的SIMD比较指令的比较结果全部为0,则我们在左部分进行继续搜索;当我们的比较结果全部位1的时候,我们在右部进搜索。当产生的结果为中间形式时候,则标识我们所要搜索的关键字在该simd单元中。

  • 叶子节点操作

叶子节点与中间节点相似,除了其仅仅含有N个指针,执行N个关键字。 且我们在叶子节点的查找是属于精确查找,即查找出满足条件的关键字而非是查找出关键字所在的某个区域范围。 同样,我们可以使用与中间节点相似的办法来完成该项工作。

  • 连接处理 join processing algorithm

嵌套循环连接作为处理连接操作的最简单的方式。其可以用来处理等式连接,也可以完成不等连接的处理。基本的操作方法在这里就不做详细讨论了。 使用simd技术,我们可以有三种处理嵌套循环的方法。 (1) 重复外循环的方式 duplicate-outer:在外循环中,我们取出一个外循环中的连接值 join key,并将其拷贝S份,因为我们SIMD每次处理的单元为S个。因此这样的处理,有利于一次处理内循环S个单元。 在内循环中,查找满足外循环条件的匹配值。

(2)重复内循环方式 duplicate-outer:在外循环中取出S个连接key值,并将其构成一个SIMD单元。在内循环中,取出一个连接key并将其复制S份,并将这S个也构成一个SIMD单元。然后将这两个SIMD单元进行比较,当然这里是进行并行的方式。并根据比较的结果进行后续处理。

(3)Rotate-Inner方式:在外循环中,取S个连接关键字,并将其构成一个SIMD单元,在内循环中,也取出S个并将其构成一个SIMD单元。将这两个SIMD单元进行比较S次,每次将内循环的单元rotate一个字的长度。并根据比较结果进行后续处理。

对于简单的整数或者浮点数的连接谓词来说,SIMD算法叫传统的连接算法快上4倍左右。但在SIMD算法中仍然有相应的条件分支指令,而这将会导致40%的性能损耗。