溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go匯編語法和MatrixOne使用實例分析

發(fā)布時間:2022-04-19 17:05:47 來源:億速云 閱讀:139 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹了Go匯編語法和MatrixOne使用實例分析的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Go匯編語法和MatrixOne使用實例分析文章都會有所收獲,下面我們一起來看看吧。

MatrixOne數(shù)據(jù)庫是什么?

MatrixOne是一個新一代超融合異構(gòu)數(shù)據(jù)庫,致力于打造單一架構(gòu)處理TP、AP、流計算等多種負載的極簡大數(shù)據(jù)引擎。MatrixOne由Go語言所開發(fā),并已于2021年10月開源,目前已經(jīng)release到0.3版本。在MatrixOne已發(fā)布的性能報告中,與業(yè)界領(lǐng)先的OLAP數(shù)據(jù)庫Clickhouse相比也不落下風。作為一款Go語言實現(xiàn)的數(shù)據(jù)庫,可以達到C++實現(xiàn)的數(shù)據(jù)庫一樣的性能,其中一個很重要的優(yōu)化就是利用Go語言自帶的匯編能力,來通過調(diào)用SIMD指令進行硬件加速。

Go匯編介紹

Go是一種較新的高級語言,提供諸如協(xié)程、快速編譯等激動人心的特性。但是在數(shù)據(jù)庫引擎中,使用純粹的Go語言會有力所未逮的時候。例如,向量化是數(shù)據(jù)庫計算引擎常用的加速手段,而Go語言無法通過調(diào)用SIMD指令來使向量化代碼的性能最大化。又例如,在安全相關(guān)代碼中,Go語言無法調(diào)用CPU提供的密碼學相關(guān)指令。在C/C++/Rust的世界中,解決這類問題可通過調(diào)用CPU架構(gòu)相關(guān)的intrinsics函數(shù)。而Go語言提供的解決方案是Go匯編。本文將介紹Go匯編的語法特點,并通過幾個具體場景展示其使用方法。

本文假定讀者已經(jīng)對計算機體系架構(gòu)和匯編語言有基本的了解,因此常用的名詞(比如“寄存器”)不做解釋。如缺乏相關(guān)預備知識,可以尋求網(wǎng)絡(luò)資源進行學習,例如這里。

如無特殊說明,本文所指的匯編語言皆針對x86(amd64)架構(gòu)。關(guān)于x86指令集,Intel和AMD官方都提供了完整的指令集參考文檔。想快速查閱,也可以使用這個列表。Intel的intrinsics文檔也可以作為一個參考。

為什么使用Go匯編?

維基百科把使用匯編語言的理由概括成3類:

  • 直接操作硬件

  • 使用特殊的CPU指令

  • 解決性能問題

Go程序員使用匯編的理由,也不外乎這3類。如果你面對的問題在這3個類別里面,并且沒有現(xiàn)成的庫可用,就可以考慮使用Go匯編。

為什么不用CGO?

  • 巨大的函數(shù)調(diào)用開銷

  • 內(nèi)存管理問題

  • 打破goroutine語義 若協(xié)程里運行CGO函數(shù),會占據(jù)單獨線程,無法被Go運行時正常調(diào)度。

  • 可移植性差 交叉編譯需要目的平臺的全套工具鏈。在不同平臺部署需要安裝更多依賴庫。

倘若在你的場景中以上幾點無法接受,不妨嘗試一下Go匯編。

Go匯編語法特點

根據(jù)Rob Pike的The Design of the Go Assembler,Go使用的匯編語言并不嚴格與CPU指令一一對應(yī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與機器指令的對應(yīng)關(guān)系,只需要了解Plan 9 assembly的語法特點。網(wǎng)絡(luò)上有一些可獲得的文檔,如這里和這里。

一例勝千言,下面我們以最簡單的64位整數(shù)加法為例,從不同方面來看Go匯編語法的特點。

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

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

  • 第一個操作數(shù)x放入寄存器AX

  • 第二個操作數(shù)y放入寄存器

  • CXCX加上AX,結(jié)果放回CX

  • CX放入返回值所在棧地址

操作數(shù)順序

x86匯編最常用的語法有兩種,AT&T語法和Intel語法。AT&T語法結(jié)果數(shù)放在最后,其他操作數(shù)放在前面。Intel語法結(jié)果數(shù)放最前面,其他操作數(shù)在后面。

Go的匯編在這方面接近AT&T語法,結(jié)果數(shù)放最后。

一個容易寫錯的例子是CMP指令。從效果上來看,CMP類似于SUB指令只修改EFLAGS標志位,不修改操作數(shù)。而在Go匯編中,CMP是以第一個操作數(shù)減去第二個操作數(shù)(與SUB相反)的結(jié)果來設(shè)置標志位。

寄存器寬度標識

部分指令支持不同的寄存器寬度。以64位操作數(shù)的ADD為例,按AT&T語法,指令名要加上寬度后綴變成ADDQ,寄存器也要加上寬度前綴變成RAX和RCX。按Intel語法,指令名不變,只給寄存器加上前綴。

上面例子可以看出,Go匯編跟兩者都不同:指令名需要加寬度后綴,寄存器不變。

函數(shù)調(diào)用約定

編程語言在函數(shù)調(diào)用中傳遞參數(shù)的方式,稱做函數(shù)調(diào)用約定(function calling convention)。x86-64架構(gòu)上的主流C/C++編譯器,都默認使用基于寄存器的方式:調(diào)用者把參數(shù)放進特定的寄存器傳給被調(diào)用函數(shù)。而Go的調(diào)用約定,簡單地講,在最新的Go 1.18上,Go自己的runtime庫在amd64與arm64與ppc64架構(gòu)上使用基于寄存器的方式,其余地方(其他的CPU架構(gòu),以及非runtime庫和用戶寫的庫)使用基于棧的方式:調(diào)用者把參數(shù)依次壓棧,被調(diào)用者通過傳遞的偏移量去棧中訪問,執(zhí)行結(jié)束后再把返回值壓棧。

在上面代碼中,F(xiàn)P是一個虛擬寄存器,指向第一個參數(shù)在棧中的地址。多個參數(shù)和返回值會按順序?qū)R存放,因此x,y,返回值在棧中地址分別是FP加上偏移量0,8,16。

對寫Go匯編代碼有幫助的工具

avo

熟悉匯編語言的讀者應(yīng)該知道,手寫匯編語言,會有選擇寄存器、計算偏移量等繁瑣且易出錯的步驟。avo庫就是為解決此類問題而生。如欲了解avo的具體用法,請參見其repo中給出的樣例。

text/template

這是Go語言自帶的一個庫。在寫大量重復代碼時會有幫助,例如在向量化代碼中為不同類型實現(xiàn)相同基本算子。具體用法參見官方文檔,這里不占用篇幅。

在Go匯編代碼中使用宏

Go匯編代碼支持跟C語言類似的宏,也可以用在代碼大量重復的場景。內(nèi)部庫中就有很多例子,比如這里。

在MatrixOne數(shù)據(jù)庫中的Go語言匯編應(yīng)用

基本向量運算加速

在OLAP數(shù)據(jù)庫計算引擎中,向量化是必不可少的加速手段。通過向量化,消除了大量簡單函數(shù)調(diào)用帶來的不必要開銷。而為了達到最大的向量化性能,使用SIMD指令是十分自然的選擇。

我們以8位整數(shù)向量化加法為例。將兩個數(shù)組的元素兩兩相加,把結(jié)果放入第三個數(shù)組。這樣的操作在某些C/C++編譯器中,可以自動優(yōu)化成使用SIMD指令的版本。而以編譯速度見長的Go編譯器,不會做這樣的優(yōu)化。這也是Go語言為了保證編譯速度所做的主動選擇。在這個例子中,我們介紹如何使用Go匯編以AVX2指令集實現(xiàn)int8類型向量加法(假設(shè)數(shù)組已經(jīng)按32字節(jié)填充)。

由于AVX2一共有16個256位寄存器,我們希望在循環(huán)展開中把它們?nèi)渴褂蒙?。如果完全手寫的話,重復羅列寄存器非常繁瑣且容易出錯。因此我們使用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])
    }
        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()
}

運行命令

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代碼中,我們只需要給變量指定寄存器類型,生成匯編的時候會自動幫我們綁定相應(yīng)類型的可用寄存器。在很多場景下這確實能夠帶來方便。不過avo目前只支持x86架構(gòu),給arm CPU寫匯編無法使用。

Go語言無法直接調(diào)用的指令

除了SIMD,還有很多Go語言本身無法使用到的CPU指令,比如密碼學相關(guān)指令。如果是用C/C++,可以使用編譯器內(nèi)置的intrinsics函數(shù)(gcc和clang皆提供)來調(diào)用,還算方便。遺憾的是Go語言并不提供intrinsics函數(shù)。遇到這樣的場景,匯編是唯一的解決辦法。Go語言自己的crypto官方庫里就有大量的匯編代碼。

這里我們以CRC32C指令作為例子。在MatrixOne的哈希表實現(xiàn)中,整數(shù)key的哈希函數(shù)只使用一條CRC32指令,達到了理論上的最高性能。代碼如下:

TEXT ·Crc32Int64Hash(SB), NOSPLIT, $0-16
	MOVQ   -1, SI
	CRC32Q data+0(FP), SI
	MOVQ   SI, ret+8(FP)
	RET

實際代碼中,為了消除匯編函數(shù)調(diào)用帶來的指令跳轉(zhuǎn)開銷,以及參數(shù)進出棧開銷,使用的是批量化的版本。這里為了節(jié)約篇幅,我們用簡化版舉例。

編譯器無法達到的特殊優(yōu)化效果

下面是MatrixOne使用的兩個有序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)這一行,是比較兩個數(shù)組當前指針位置的元素。后面幾行根據(jù)這個比較的結(jié)果,來移動對應(yīng)操作數(shù)數(shù)組及結(jié)果數(shù)組的指針。文字解釋不如對比下面等價的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;
}

匯編代碼中,循環(huán)體內(nèi)只做了一次比較運算,并且沒有任何的分支跳轉(zhuǎn)。高級語言編譯器達不到這樣的優(yōu)化效果,原因是任何高級語言都不提供“根據(jù)一個比較運算的3種不同結(jié)果,分別修改3個不同的數(shù)”這樣直接跟CPU指令集相關(guān)的語義。

這個例子算是對匯編語言威力的一個展示。編程語言不斷發(fā)展,抽象層次越來越高,但是在性能最大化的場景下,仍然需要直接與CPU指令打交道的匯編語言。

關(guān)于“Go匯編語法和MatrixOne使用實例分析”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“Go匯編語法和MatrixOne使用實例分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI