目录
- MatrixOne数据库是什么?
- Go汇编介绍
为什么应用Go汇编?
- 为什么不必CGO?
Go汇编语法特点
- 操作数程序
- 寄存器宽度标识
- 函数调用约定
对写Go汇编代码有帮忙的工具
- avo
- text/template
- 在Go汇编代码中应用宏
在MatrixOne数据库中的Go语言汇编利用
- 根本向量运算减速
- Go语言无奈间接调用的指令
- 编译器无奈达到的非凡优化成果
- MatrixOne社区
MatrixOne数据库是什么?
MatrixOne是一个新一代超交融异构数据库,致力于打造繁多架构解决TP、AP、流计算等多种负载的极简大数据引擎。MatrixOne由Go语言所开发,并已于2021年10月开源,目前曾经release到0.3版本。在MatrixOne已公布的性能报告中,与业界当先的OLAP数据库Clickhouse相比也不落下风。作为一款Go语言实现的数据库,能够达到C++实现的数据库一样的性能,其中一个很重要的优化就是利用Go语言自带的汇编能力,来通过调用SIMD指令进行硬件加速。本文就将对Go汇编及在MatrixOne的利用做具体介绍。
Github地址:https://github.com/matrixorig... 有趣味的读者欢送star和fork。
Go汇编介绍
Go是一种较新的高级语言,提供诸如协程、疾速编译等激动人心的个性。然而在数据库引擎中,应用纯正的Go语言会无力所未逮的时候。例如,向量化是数据库计算引擎罕用的减速伎俩,而Go语言无奈通过调用SIMD指令来使向量化代码的性能最大化。又例如,在平安相干代码中,Go语言无奈调用CPU提供的密码学相干指令。在C/C++/Rust的世界中,解决这类问题可通过调用CPU架构相干的intrinsics函数。而Go语言提供的解决方案是Go汇编。本文将介绍Go汇编的语法特点,并通过几个具体场景展现其应用办法。
本文假设读者曾经对计算机体系架构和汇编语言有根本的理解,因而罕用的名词(比方“寄存器”)不做解释。如不足相干准备常识,能够寻求网络资源进行学习,例如这里。
如无非凡阐明,本文所指的汇编语言皆针对x86(amd64)架构。对于x86指令集,Intel和AMD官网都提供了残缺的指令集参考文档。想疾速查阅,也能够应用这个列表。Intel的intrinsics文档也能够作为一个参考。
为什么应用Go汇编?
维基百科把应用汇编语言的理由概括成3类:
- 间接操作硬件
- 应用非凡的CPU指令
- 解决性能问题
Go程序员应用汇编的理由,也不外乎这3类。如果你面对的问题在这3个类别外面,并且没有现成的库可用,就能够思考应用Go汇编。
为什么不必CGO?
- 微小的函数调用开销
- 内存治理问题
- 突破goroutine语义 若协程里运行CGO函数,会占据独自线程,无奈被Go运行时失常调度。
- 可移植性差 穿插编译须要目标平台的全套工具链。在不同平台部署须要装置更多依赖库。
假使在你的场景中以上几点无奈承受,无妨尝试一下Go汇编。
Go汇编语法特点
依据Rob Pike的The Design of the Go Assembler,Go应用的汇编语言并不严格与CPU指令一一对应,而是一种被称作Plan 9 assembly的“伪汇编”。
The most important thing to know about Go's assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load. Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.
咱们不必关怀Plan 9 assembly与机器指令的对应关系,只须要理解Plan 9 assembly的语法特点。网络上有一些可取得的文档,如这里和这里。
一例胜千言,上面咱们以最简略的64位整数加法为例,从不同方面来看Go汇编语法的特点。
// add.gofunc Add(x, y int64) int64
//add_amd64.s#include "textflag.h"TEXT ·Add(SB), NOSPLIT, $0-24 MOVQ x+0(FP), AX MOVQ y+8(FP), CX ADDQ AX, CX MOVQ CX, ret+16(FP) RET
这四条汇编代码所做的顺次是:
- 第一个操作数x放入寄存器AX
- 第二个操作数y放入寄存器CX
- CX加上AX,后果放回CX
- CX放入返回值所在栈地址
操作数程序
x86汇编最罕用的语法有两种,AT&T语法和Intel语法。AT&T语法后果数放在最初,其余操作数放在后面。Intel语法后果数放最后面,其余操作数在前面。
Go的汇编在这方面靠近AT&T语法,后果数放最初。
一个容易写错的例子是CMP指令。从成果上来看,CMP相似于SUB指令只批改EFLAGS标记位,不批改操作数。而在Go汇编中,CMP是以第一个操作数减去第二个操作数(与SUB相同)的后果来设置标记位。
寄存器宽度标识
局部指令反对不同的寄存器宽度。以64位操作数的ADD为例,按AT&T语法,指令名要加上宽度后缀变成ADDQ,寄存器也要加上宽度前缀变成RAX和RCX。按Intel语法,指令名不变,只给寄存器加上前缀。
下面例子能够看出,Go汇编跟两者都不同:指令名须要加宽度后缀,寄存器不变。
函数调用约定
编程语言在函数调用中传递参数的形式,称做函数调用约定(function calling convention)。x86-64架构上的支流C/C++编译器,都默认应用基于寄存器的形式:调用者把参数放进特定的寄存器传给被调用函数。而Go的调用约定,简略地讲,在最新的Go 1.18上,Go本人的runtime库在amd64与arm64与ppc64架构上应用基于寄存器的形式,其余中央(其余的CPU架构,以及非runtime库和用户写的库)应用基于栈的形式:调用者把参数顺次压栈,被调用者通过传递的偏移量去栈中拜访,执行完结后再把返回值压栈。
在下面代码中,FP是一个虚构寄存器,指向第一个参数在栈中的地址。多个参数和返回值会按程序对齐寄存,因而x,y,返回值在栈中地址别离是FP加上偏移量0,8,16。
对写Go汇编代码有帮忙的工具
avo
相熟汇编语言的读者应该晓得,手写汇编语言,会有抉择寄存器、计算偏移量等繁琐且易出错的步骤。avo库就是为解决此类问题而生。如欲了解avo的具体用法,请参见其repo中给出的样例。
text/template
这是Go语言自带的一个库。在写大量反复代码时会有帮忙,例如在向量化代码中为不同类型实现雷同根本算子。具体用法参见官网文档,这里不占用篇幅。
在Go汇编代码中应用宏
Go汇编代码反对跟C语言相似的宏,也能够用在代码大量反复的场景。外部库中就有很多例子,比方这里。
在MatrixOne数据库中的Go语言汇编利用
根本向量运算减速
在OLAP数据库计算引擎中,向量化是必不可少的减速伎俩。通过向量化,打消了大量简略函数调用带来的不必要开销。而为了达到最大的向量化性能,应用SIMD指令是非常天然的抉择。
咱们以8位整数向量化加法为例。将两个数组的元素两两相加,把后果放入第三个数组。这样的操作在某些C/C++编译器中,能够主动优化成应用SIMD指令的版本。而以编译速度见长的Go编译器,不会做这样的优化。这也是Go语言为了保障编译速度所做的被动抉择。在这个例子中,咱们介绍如何应用Go汇编以AVX2指令集实现int8类型向量加法(假如数组曾经按32字节填充)。
因为AVX2一共有16个256位寄存器,咱们心愿在循环展开中把它们全副应用上。如果齐全手写的话,反复列举寄存器十分繁琐且容易出错。因而咱们应用avo来简化一些工作。avo的向量加法代码如下:
package mainimport ( . "github.com/mmcloughlin/avo/build" . "github.com/mmcloughlin/avo/operand" . "github.com/mmcloughlin/avo/reg")var unroll = 16var regWidth = 32func main() { TEXT("int8AddAvx2Asm", NOSPLIT, "func(x []int8, y []int8, r []int8)") x := Mem{Base: Load(Param("x").Base(), GP64())} y := Mem{Base: Load(Param("y").Base(), GP64())} r := Mem{Base: Load(Param("r").Base(), GP64())} n := Load(Param("x").Len(), GP64()) blocksize := regWidth * unroll blockitems := blocksize / 1 regitems := regWidth / 1 Label("int8AddBlockLoop") CMPQ(n, U32(blockitems)) JL(LabelRef("int8AddTailLoop")) xs := make([]VecVirtual, unroll) for i := 0; i < unroll; i++ { xs[i] = YMM() VMOVDQU(x.Offset(regWidth*i), xs[i]) } for i := 0; i < unroll; i++ { VPADDB(y.Offset(regWidth*i), xs[i], xs[i]) } for i := 0; i < unroll; i++ { VMOVDQU(xs[i], r.Offset(regWidth*i)) } ADDQ(U32(blocksize), x.Base) ADDQ(U32(blocksize), y.Base) ADDQ(U32(blocksize), r.Base) SUBQ(U32(blockitems), n) JMP(LabelRef("int8AddBlockLoop")) Label("int8AddTailLoop") CMPQ(n, U32(regitems)) JL(LabelRef("int8AddDone")) VMOVDQU(x, xs[0]) VPADDB(y, xs[0], xs[0]) VMOVDQU(xs[0], r) ADDQ(U32(regWidth), x.Base) ADDQ(U32(regWidth), y.Base) ADDQ(U32(regWidth), r.Base) SUBQ(U32(regitems), n) JMP(LabelRef("int8AddTailLoop")) Label("int8AddDone") RET()}
运行命令
go run int8add.go -out int8add.s
之后生成的汇编代码如下:
// Code generated by command: go run int8add.go -out int8add.s. DO NOT EDIT.#include "textflag.h"// func int8AddAvx2Asm(x []int8, y []int8, r []int8)// Requires: AVX, AVX2TEXT ·int8AddAvx2Asm(SB), NOSPLIT, $0-72 MOVQ x_base+0(FP), AX MOVQ y_base+24(FP), CX MOVQ r_base+48(FP), DX MOVQ x_len+8(FP), BXint8AddBlockLoop: CMPQ BX, $0x00000200 JL int8AddTailLoop VMOVDQU (AX), Y0 VMOVDQU 32(AX), Y1 VMOVDQU 64(AX), Y2 VMOVDQU 96(AX), Y3 VMOVDQU 128(AX), Y4 VMOVDQU 160(AX), Y5 VMOVDQU 192(AX), Y6 VMOVDQU 224(AX), Y7 VMOVDQU 256(AX), Y8 VMOVDQU 288(AX), Y9 VMOVDQU 320(AX), Y10 VMOVDQU 352(AX), Y11 VMOVDQU 384(AX), Y12 VMOVDQU 416(AX), Y13 VMOVDQU 448(AX), Y14 VMOVDQU 480(AX), Y15 VPADDB (CX), Y0, Y0 VPADDB 32(CX), Y1, Y1 VPADDB 64(CX), Y2, Y2 VPADDB 96(CX), Y3, Y3 VPADDB 128(CX), Y4, Y4 VPADDB 160(CX), Y5, Y5 VPADDB 192(CX), Y6, Y6 VPADDB 224(CX), Y7, Y7 VPADDB 256(CX), Y8, Y8 VPADDB 288(CX), Y9, Y9 VPADDB 320(CX), Y10, Y10 VPADDB 352(CX), Y11, Y11 VPADDB 384(CX), Y12, Y12 VPADDB 416(CX), Y13, Y13 VPADDB 448(CX), Y14, Y14 VPADDB 480(CX), Y15, Y15 VMOVDQU Y0, (DX) VMOVDQU Y1, 32(DX) VMOVDQU Y2, 64(DX) VMOVDQU Y3, 96(DX) VMOVDQU Y4, 128(DX) VMOVDQU Y5, 160(DX) VMOVDQU Y6, 192(DX) VMOVDQU Y7, 224(DX) VMOVDQU Y8, 256(DX) VMOVDQU Y9, 288(DX) VMOVDQU Y10, 320(DX) VMOVDQU Y11, 352(DX) VMOVDQU Y12, 384(DX) VMOVDQU Y13, 416(DX) VMOVDQU Y14, 448(DX) VMOVDQU Y15, 480(DX) ADDQ $0x00000200, AX ADDQ $0x00000200, CX ADDQ $0x00000200, DX SUBQ $0x00000200, BX JMP int8AddBlockLoopint8AddTailLoop: CMPQ BX, $0x00000020 JL int8AddDone VMOVDQU (AX), Y0 VPADDB (CX), Y0, Y0 VMOVDQU Y0, (DX) ADDQ $0x00000020, AX ADDQ $0x00000020, CX ADDQ $0x00000020, DX SUBQ $0x00000020, BX JMP int8AddTailLoopint8AddDone: RET
能够看到,在avo代码中,咱们只须要给变量指定寄存器类型,生成汇编的时候会主动帮咱们绑定相应类型的可用寄存器。在很多场景下这的确可能带来不便。不过avo目前只反对x86架构,给arm CPU写汇编无奈应用。
Go语言无奈间接调用的指令
除了SIMD,还有很多Go语言自身无奈应用到的CPU指令,比方密码学相干指令。如果是用C/C++,能够应用编译器内置的intrinsics函数(gcc和clang皆提供)来调用,还算不便。遗憾的是Go语言并不提供intrinsics函数。遇到这样的场景,汇编是惟一的解决办法。Go语言本人的crypto官网库里就有大量的汇编代码。
这里咱们以CRC32C指令作为例子。在MatrixOne的哈希表实现中,整数key的哈希函数只应用一条CRC32指令,达到了实践上的最高性能。代码如下:
TEXT ·Crc32Int64Hash(SB), NOSPLIT, $0-16 MOVQ -1, SI CRC32Q data+0(FP), SI MOVQ SI, ret+8(FP) RET
理论代码中,为了打消汇编函数调用带来的指令跳转开销,以及参数进出栈开销,应用的是批量化的版本。这里为了节约篇幅,咱们用简化版举例。
编译器无奈达到的非凡优化成果
上面是MatrixOne应用的两个有序64位整数数组求交加的算法的一部分:
...loop: CMPQ DX, DI JE done CMPQ R11, R8 JE done MOVQ (DX), R10 MOVQ R10, (SI) CMPQ R10, (R11) SETLE AL SETGE BL SETEQ CL SHLB $0x03, AL SHLB $0x03, BL SHLB $0x03, CL ADDQ AX, DX ADDQ BX, R11 ADDQ CX, SI JMP loopdone:...
CMPQ R10, (R11)
这一行,是比拟两个数组以后指针地位的元素。前面几行依据这个比拟的后果,来挪动对应操作数数组及后果数组的指针。文字解释不如比照上面等价的C语言代码来得分明:
while (true) { if (a == a_end) break; if (b == b_end) break; *c = *a; if (*a <= *b) ++a; if (*a >= *b) ++b; if (*a == *b) ++c;}
汇编代码中,循环体内只做了一次比拟运算,并且没有任何的分支跳转。高级语言编译器达不到这样的优化成果,起因是任何高级语言都不提供“依据一个比拟运算的3种不同后果,别离批改3个不同的数”这样间接跟CPU指令集相干的语义。
这个例子算是对汇编语言威力的一个展现。编程语言一直倒退,抽象层次越来越高,然而在性能最大化的场景下,依然须要间接与CPU指令打交道的汇编语言。
MatrixOne社区
对MatrixOne有趣味的话能够关注矩阵起源公众号或者退出MatrixOne社群。
微信公众号 矩阵起源
MatrixOne社区群 技术交换