淺析Go匯編語(yǔ)法和MatrixOne使用介紹
MatrixOne是一個(gè)新一代超融合異構(gòu)數(shù)據(jù)庫(kù),致力于打造單一架構(gòu)處理TP、AP、流計(jì)算等多種負(fù)載的極簡(jiǎn)大數(shù)據(jù)引擎。MatrixOne由Go語(yǔ)言所開(kāi)發(fā),并已于2021年10月開(kāi)源,目前已經(jīng)release到0.3版本。在MatrixOne已發(fā)布的性能報(bào)告中,與業(yè)界領(lǐng)先的OLAP數(shù)據(jù)庫(kù)Clickhouse相比也不落下風(fēng)。作為一款Go語(yǔ)言實(shí)現(xiàn)的數(shù)據(jù)庫(kù),可以達(dá)到C++實(shí)現(xiàn)的數(shù)據(jù)庫(kù)一樣的性能,其中一個(gè)很重要的優(yōu)化就是利用Go語(yǔ)言自帶的匯編能力,來(lái)通過(guò)調(diào)用SIMD指令進(jìn)行硬件加速。本文就將對(duì)Go匯編及在MatrixOne的應(yīng)用做詳細(xì)介紹。
MatrixOne數(shù)據(jù)庫(kù)是什么?
MatrixOne是一個(gè)新一代超融合異構(gòu)數(shù)據(jù)庫(kù),致力于打造單一架構(gòu)處理TP、AP、流計(jì)算等多種負(fù)載的極簡(jiǎn)大數(shù)據(jù)引擎。MatrixOne由Go語(yǔ)言所開(kāi)發(fā),并已于2021年10月開(kāi)源,目前已經(jīng)release到0.3版本。在MatrixOne已發(fā)布的性能報(bào)告中,與業(yè)界領(lǐng)先的OLAP數(shù)據(jù)庫(kù)Clickhouse相比也不落下風(fēng)。作為一款Go語(yǔ)言實(shí)現(xiàn)的數(shù)據(jù)庫(kù),可以達(dá)到C++實(shí)現(xiàn)的數(shù)據(jù)庫(kù)一樣的性能,其中一個(gè)很重要的優(yōu)化就是利用Go語(yǔ)言自帶的匯編能力,來(lái)通過(guò)調(diào)用SIMD指令進(jìn)行硬件加速。本文就將對(duì)Go匯編及在MatrixOne的應(yīng)用做詳細(xì)介紹。
Github地址:https://github.com/matrixorigin/matrixone 有興趣的讀者歡迎star和fork。
Go匯編介紹
Go是一種較新的高級(jí)語(yǔ)言,提供諸如協(xié)程、快速編譯等激動(dòng)人心的特性。但是在數(shù)據(jù)庫(kù)引擎中,使用純粹的Go語(yǔ)言會(huì)有力所未逮的時(shí)候。例如,向量化是數(shù)據(jù)庫(kù)計(jì)算引擎常用的加速手段,而Go語(yǔ)言無(wú)法通過(guò)調(diào)用SIMD指令來(lái)使向量化代碼的性能最大化。又例如,在安全相關(guān)代碼中,Go語(yǔ)言無(wú)法調(diào)用CPU提供的密碼學(xué)相關(guān)指令。在C/C++/Rust的世界中,解決這類(lèi)問(wèn)題可通過(guò)調(diào)用CPU架構(gòu)相關(guān)的intrinsics函數(shù)。而Go語(yǔ)言提供的解決方案是Go匯編。本文將介紹Go匯編的語(yǔ)法特點(diǎn),并通過(guò)幾個(gè)具體場(chǎng)景展示其使用方法。
本文假定讀者已經(jīng)對(duì)計(jì)算機(jī)體系架構(gòu)和匯編語(yǔ)言有基本的了解,因此常用的名詞(比如“寄存器”)不做解釋。如缺乏相關(guān)預(yù)備知識(shí),可以尋求網(wǎng)絡(luò)資源進(jìn)行學(xué)習(xí),例如這里。
如無(wú)特殊說(shuō)明,本文所指的匯編語(yǔ)言皆針對(duì)x86(amd64)架構(gòu)。關(guān)于x86指令集,Intel和AMD官方都提供了完整的指令集參考文檔。想快速查閱,也可以使用這個(gè)列表。Intel的intrinsics文檔也可以作為一個(gè)參考。
為什么使用Go匯編?
維基百科把使用匯編語(yǔ)言的理由概括成3類(lèi):
- 直接操作硬件
- 使用特殊的CPU指令
- 解決性能問(wèn)題
Go程序員使用匯編的理由,也不外乎這3類(lèi)。如果你面對(duì)的問(wèn)題在這3個(gè)類(lèi)別里面,并且沒(méi)有現(xiàn)成的庫(kù)可用,就可以考慮使用Go匯編。
為什么不用CGO?
- 巨大的函數(shù)調(diào)用開(kāi)銷(xiāo)
- 內(nèi)存管理問(wèn)題
- 打破goroutine語(yǔ)義 若協(xié)程里運(yùn)行CGO函數(shù),會(huì)占據(jù)單獨(dú)線程,無(wú)法被Go運(yùn)行時(shí)正常調(diào)度。
- 可移植性差 交叉編譯需要目的平臺(tái)的全套工具鏈。在不同平臺(tái)部署需要安裝更多依賴(lài)庫(kù)。
倘若在你的場(chǎng)景中以上幾點(diǎn)無(wú)法接受,不妨嘗試一下Go匯編。
Go匯編語(yǔ)法特點(diǎn)
根據(jù)Rob Pike的The Design of the Go Assembler,Go使用的匯編語(yǔ)言并不嚴(yán)格與CPU指令一一對(duì)應(yīng),而是一種被稱(chēng)作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.
我們不用關(guān)心Plan 9 assembly與機(jī)器指令的對(duì)應(yīng)關(guān)系,只需要了解Plan 9 assembly的語(yǔ)法特點(diǎn)。網(wǎng)絡(luò)上有一些可獲得的文檔,如這里和這里。
一例勝千言,下面我們以最簡(jiǎn)單的64位整數(shù)加法為例,從不同方面來(lái)看Go匯編語(yǔ)法的特點(diǎn)。
// add.go func 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這四條匯編代碼所做的依次是:
- 第一個(gè)操作數(shù)x放入寄存器AX
- 第二個(gè)操作數(shù)y放入寄存器
- CXCX加上AX,結(jié)果放回CX
- CX放入返回值所在棧地址
操作數(shù)順序
x86匯編最常用的語(yǔ)法有兩種,AT&T語(yǔ)法和Intel語(yǔ)法。AT&T語(yǔ)法結(jié)果數(shù)放在最后,其他操作數(shù)放在前面。Intel語(yǔ)法結(jié)果數(shù)放最前面,其他操作數(shù)在后面。
Go的匯編在這方面接近AT&T語(yǔ)法,結(jié)果數(shù)放最后。
一個(gè)容易寫(xiě)錯(cuò)的例子是CMP指令。從效果上來(lái)看,CMP類(lèi)似于SUB指令只修改EFLAGS標(biāo)志位,不修改操作數(shù)。而在Go匯編中,CMP是以第一個(gè)操作數(shù)減去第二個(gè)操作數(shù)(與SUB相反)的結(jié)果來(lái)設(shè)置標(biāo)志位。
寄存器寬度標(biāo)識(shí)
部分指令支持不同的寄存器寬度。以64位操作數(shù)的ADD為例,按AT&T語(yǔ)法,指令名要加上寬度后綴變成ADDQ,寄存器也要加上寬度前綴變成RAX和RCX。按Intel語(yǔ)法,指令名不變,只給寄存器加上前綴。
上面例子可以看出,Go匯編跟兩者都不同:指令名需要加寬度后綴,寄存器不變。
函數(shù)調(diào)用約定
編程語(yǔ)言在函數(shù)調(diào)用中傳遞參數(shù)的方式,稱(chēng)做函數(shù)調(diào)用約定(function calling convention)。x86-64架構(gòu)上的主流C/C++編譯器,都默認(rèn)使用基于寄存器的方式:調(diào)用者把參數(shù)放進(jìn)特定的寄存器傳給被調(diào)用函數(shù)。而Go的調(diào)用約定,簡(jiǎn)單地講,在最新的Go 1.18上,Go自己的runtime庫(kù)在amd64與arm64與ppc64架構(gòu)上使用基于寄存器的方式,其余地方(其他的CPU架構(gòu),以及非runtime庫(kù)和用戶(hù)寫(xiě)的庫(kù))使用基于棧的方式:調(diào)用者把參數(shù)依次壓棧,被調(diào)用者通過(guò)傳遞的偏移量去棧中訪問(wèn),執(zhí)行結(jié)束后再把返回值壓棧。
在上面代碼中,F(xiàn)P是一個(gè)虛擬寄存器,指向第一個(gè)參數(shù)在棧中的地址。多個(gè)參數(shù)和返回值會(huì)按順序?qū)R存放,因此x,y,返回值在棧中地址分別是FP加上偏移量0,8,16。
對(duì)寫(xiě)Go匯編代碼有幫助的工具
avo
熟悉匯編語(yǔ)言的讀者應(yīng)該知道,手寫(xiě)匯編語(yǔ)言,會(huì)有選擇寄存器、計(jì)算偏移量等繁瑣且易出錯(cuò)的步驟。avo庫(kù)就是為解決此類(lèi)問(wèn)題而生。如欲了解avo的具體用法,請(qǐng)參見(jiàn)其repo中給出的樣例。
text/template
這是Go語(yǔ)言自帶的一個(gè)庫(kù)。在寫(xiě)大量重復(fù)代碼時(shí)會(huì)有幫助,例如在向量化代碼中為不同類(lèi)型實(shí)現(xiàn)相同基本算子。具體用法參見(jiàn)官方文檔,這里不占用篇幅。
在Go匯編代碼中使用宏
Go匯編代碼支持跟C語(yǔ)言類(lèi)似的宏,也可以用在代碼大量重復(fù)的場(chǎng)景。內(nèi)部庫(kù)中就有很多例子,比如這里。
在MatrixOne數(shù)據(jù)庫(kù)中的Go語(yǔ)言匯編應(yīng)用
基本向量運(yùn)算加速
在OLAP數(shù)據(jù)庫(kù)計(jì)算引擎中,向量化是必不可少的加速手段。通過(guò)向量化,消除了大量簡(jiǎn)單函數(shù)調(diào)用帶來(lái)的不必要開(kāi)銷(xiāo)。而為了達(dá)到最大的向量化性能,使用SIMD指令是十分自然的選擇。
我們以8位整數(shù)向量化加法為例。將兩個(gè)數(shù)組的元素兩兩相加,把結(jié)果放入第三個(gè)數(shù)組。這樣的操作在某些C/C++編譯器中,可以自動(dòng)優(yōu)化成使用SIMD指令的版本。而以編譯速度見(jiàn)長(zhǎng)的Go編譯器,不會(huì)做這樣的優(yōu)化。這也是Go語(yǔ)言為了保證編譯速度所做的主動(dòng)選擇。在這個(gè)例子中,我們介紹如何使用Go匯編以AVX2指令集實(shí)現(xiàn)int8類(lèi)型向量加法(假設(shè)數(shù)組已經(jīng)按32字節(jié)填充)。
由于AVX2一共有16個(gè)256位寄存器,我們希望在循環(huán)展開(kāi)中把它們?nèi)渴褂蒙稀H绻耆謱?xiě)的話,重復(fù)羅列寄存器非常繁瑣且容易出錯(cuò)。因此我們使用avo來(lái)簡(jiǎn)化一些工作。avo的向量加法代碼如下:
package main
import (
. "github.com/mmcloughlin/avo/build"
. "github.com/mmcloughlin/avo/operand"
. "github.com/mmcloughlin/avo/reg"
)
var unroll = 16
var regWidth = 32
func 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])
}
VPADDB(y.Offset(regWidth*i), xs[i], xs[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()
}運(yùn)行命令
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, AVX2 TEXT ·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), BX int8AddBlockLoop: 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 int8AddBlockLoop int8AddTailLoop: CMPQ BX, $0x00000020 JL int8AddDone ADDQ $0x00000020, AX ADDQ $0x00000020, CX ADDQ $0x00000020, DX SUBQ $0x00000020, BX JMP int8AddTailLoop int8AddDone: RET
可以看到,在avo代碼中,我們只需要給變量指定寄存器類(lèi)型,生成匯編的時(shí)候會(huì)自動(dòng)幫我們綁定相應(yīng)類(lèi)型的可用寄存器。在很多場(chǎng)景下這確實(shí)能夠帶來(lái)方便。不過(guò)avo目前只支持x86架構(gòu),給arm CPU寫(xiě)匯編無(wú)法使用。
Go語(yǔ)言無(wú)法直接調(diào)用的指令
除了SIMD,還有很多Go語(yǔ)言本身無(wú)法使用到的CPU指令,比如密碼學(xué)相關(guān)指令。如果是用C/C++,可以使用編譯器內(nèi)置的intrinsics函數(shù)(gcc和clang皆提供)來(lái)調(diào)用,還算方便。遺憾的是Go語(yǔ)言并不提供intrinsics函數(shù)。遇到這樣的場(chǎng)景,匯編是唯一的解決辦法。Go語(yǔ)言自己的crypto官方庫(kù)里就有大量的匯編代碼。
這里我們以CRC32C指令作為例子。在MatrixOne的哈希表實(shí)現(xiàn)中,整數(shù)key的哈希函數(shù)只使用一條CRC32指令,達(dá)到了理論上的最高性能。代碼如下:
TEXT ·Crc32Int64Hash(SB), NOSPLIT, $0-16 MOVQ -1, SI CRC32Q data+0(FP), SI MOVQ SI, ret+8(FP) RET
實(shí)際代碼中,為了消除匯編函數(shù)調(diào)用帶來(lái)的指令跳轉(zhuǎn)開(kāi)銷(xiāo),以及參數(shù)進(jìn)出棧開(kāi)銷(xiāo),使用的是批量化的版本。這里為了節(jié)約篇幅,我們用簡(jiǎn)化版舉例。
編譯器無(wú)法達(dá)到的特殊優(yōu)化效果
下面是MatrixOne使用的兩個(gè)有序64位整數(shù)數(shù)組求交集的算法的一部分:
... 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 loop done: ...
CMPQ R10, (R11)這一行,是比較兩個(gè)數(shù)組當(dāng)前指針位置的元素。后面幾行根據(jù)這個(gè)比較的結(jié)果,來(lái)移動(dòng)對(duì)應(yīng)操作數(shù)數(shù)組及結(jié)果數(shù)組的指針。文字解釋不如對(duì)比下面等價(jià)的C語(yǔ)言代碼來(lái)得清楚:
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;
}匯編代碼中,循環(huán)體內(nèi)只做了一次比較運(yùn)算,并且沒(méi)有任何的分支跳轉(zhuǎn)。高級(jí)語(yǔ)言編譯器達(dá)不到這樣的優(yōu)化效果,原因是任何高級(jí)語(yǔ)言都不提供“根據(jù)一個(gè)比較運(yùn)算的3種不同結(jié)果,分別修改3個(gè)不同的數(shù)”這樣直接跟CPU指令集相關(guān)的語(yǔ)義。
這個(gè)例子算是對(duì)匯編語(yǔ)言威力的一個(gè)展示。編程語(yǔ)言不斷發(fā)展,抽象層次越來(lái)越高,但是在性能最大化的場(chǎng)景下,仍然需要直接與CPU指令打交道的匯編語(yǔ)言。
到此這篇關(guān)于淺析Go匯編語(yǔ)法和MatrixOne使用介紹的文章就介紹到這了,更多相關(guān)Go匯編MatrixOne使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)簡(jiǎn)易的命令行功能
這篇文章主要為大家詳細(xì)介紹了如何通過(guò)Golang實(shí)現(xiàn)一個(gè)簡(jiǎn)易的命令行功能,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-02-02
重學(xué)Go語(yǔ)言之如何開(kāi)發(fā)RPC應(yīng)用
這篇文章主要為大家詳細(xì)介紹了在Go語(yǔ)言中如何構(gòu)建RPC應(yīng)用,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09
Go語(yǔ)言展現(xiàn)快速排序算法全過(guò)程的思路及代碼示例
這篇文章主要介紹了Go語(yǔ)言展現(xiàn)快速排序算法全過(guò)程的思路及代碼示例,文章最后作者還提到了對(duì)Quick Sort算法優(yōu)化的一些想法,需要的朋友可以參考下2016-04-04
Go實(shí)現(xiàn)socks5服務(wù)器的方法
SOCKS5 是一個(gè)代理協(xié)議,它在使用TCP/IP協(xié)議通訊的前端機(jī)器和服務(wù)器機(jī)器之間扮演一個(gè)中介角色,使得內(nèi)部網(wǎng)中的前端機(jī)器變得能夠訪問(wèn)Internet網(wǎng)中的服務(wù)器,或者使通訊更加安全,這篇文章主要介紹了Go實(shí)現(xiàn)socks5服務(wù)器的方法,需要的朋友可以參考下2023-07-07
Golang使用channel實(shí)現(xiàn)一個(gè)優(yōu)雅退出功能
最近補(bǔ)?Golang?channel?方面八股的時(shí)候發(fā)現(xiàn)用?channel?實(shí)現(xiàn)一個(gè)優(yōu)雅退出功能好像不是很難,之前寫(xiě)的?HTTP?框架剛好也不支持優(yōu)雅退出功能,于是就參考了?Hertz?優(yōu)雅退出方面的代碼,為我的?PIANO?補(bǔ)足了這個(gè)?feature2023-03-03

