Go匯編語法MatrixOne使用介紹

      網友投稿 808 2022-05-30

      目錄

      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/matrixorigin/matrixone 有興趣的讀者歡迎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.go func Add(x, y int64) int64

      //add_amd64.s #include "textflag.h" TEXT ·Add(SB), NOSPLIT,

      //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

      -24 MOVQ x+0(FP), AX MOVQ y+8(FP), CX ADDQ AX, CX MOVQ CX, ret+16(FP) RET

      這四條匯編代碼所做的依次是:

      第一個操作數x放入寄存器AX

      第二個操作數y放入寄存器CX

      Go匯編語法和MatrixOne使用介紹

      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 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]) } 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, 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 VMOVDQU (AX), Y0 VPADDB (CX), Y0, Y0 VMOVDQU Y0, (DX) ADDQ $0x00000020, AX ADDQ $0x00000020, CX ADDQ $0x00000020, DX SUBQ $0x00000020, BX JMP int8AddTailLoop int8AddDone: 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 loop done: ...

      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社區群 技術交流

      Go 分布式 數據庫 匯編語言

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:一文縱覽向量檢索
      下一篇:兩個月面試6家(美團、搜狐)已拿滴滴18k*16薪offer
      相關文章
      亚洲无线观看国产精品| 亚洲国产精品久久久久网站| 国产亚洲美女精品久久久久| 亚洲一区二区三区偷拍女厕| 亚洲va中文字幕无码| 久久精品亚洲中文字幕无码网站| 亚洲s码欧洲m码吹潮| 亚洲国产人成在线观看69网站| 亚洲精品无码mv在线观看网站| 亚洲AV无码一区二区三区性色 | 亚洲AV无码乱码精品国产| 亚洲AV无码一区二区一二区| jizzjizz亚洲日本少妇| 亚洲国产成人精品无码区在线网站| 亚洲av无码成人精品区在线播放| 久久亚洲精品11p| 国产成人亚洲毛片| 亚洲AV无码成人精品区大在线| 亚洲精品456播放| 亚洲gay片在线gv网站| 亚洲AV无码精品国产成人| 日本亚洲中午字幕乱码| 亚洲精品偷拍视频免费观看 | 亚洲福利中文字幕在线网址| 亚洲国产一区明星换脸| 伊人久久综在合线亚洲91| 亚洲AV无码AV日韩AV网站| 亚洲国产成人AV网站| 亚洲国产成人精品女人久久久 | 亚洲国产一区二区a毛片| 亚洲欧洲免费视频| 亚洲国产精品综合久久2007| 亚洲综合色丁香婷婷六月图片| 亚洲自偷自拍另类12p| 国产亚洲3p无码一区二区| 无码久久精品国产亚洲Av影片| 91亚洲一区二区在线观看不卡| 亚洲国产精品无码av| 国内精品99亚洲免费高清| 亚洲国产成人高清在线观看| 亚洲高清美女一区二区三区|