目录

  • 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.gofunc Add(x, y int64) int64
//add_amd64.s#include "textflag.h"TEXT ·Add(SB), NOSPLIT, $0-24MOVQ x+0(FP), AXMOVQ y+8(FP), CXADDQ AX, CXMOVQ 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 = 16
var 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 * unrollblockitems := blocksize / 1regitems := regWidth / 1Label("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-72MOVQ x_base+0(FP), AXMOVQ y_base+24(FP), CXMOVQ r_base+48(FP), DXMOVQ x_len+8(FP), BXint8AddBlockLoop:CMPQ    BX, $0x00000200JL      int8AddTailLoopVMOVDQU (AX), Y0VMOVDQU 32(AX), Y1VMOVDQU 64(AX), Y2VMOVDQU 96(AX), Y3VMOVDQU 128(AX), Y4VMOVDQU 160(AX), Y5VMOVDQU 192(AX), Y6VMOVDQU 224(AX), Y7VMOVDQU 256(AX), Y8VMOVDQU 288(AX), Y9VMOVDQU 320(AX), Y10VMOVDQU 352(AX), Y11VMOVDQU 384(AX), Y12VMOVDQU 416(AX), Y13VMOVDQU 448(AX), Y14VMOVDQU 480(AX), Y15VPADDB  (CX), Y0, Y0VPADDB  32(CX), Y1, Y1VPADDB  64(CX), Y2, Y2VPADDB  96(CX), Y3, Y3VPADDB  128(CX), Y4, Y4VPADDB  160(CX), Y5, Y5VPADDB  192(CX), Y6, Y6VPADDB  224(CX), Y7, Y7VPADDB  256(CX), Y8, Y8VPADDB  288(CX), Y9, Y9VPADDB  320(CX), Y10, Y10VPADDB  352(CX), Y11, Y11VPADDB  384(CX), Y12, Y12VPADDB  416(CX), Y13, Y13VPADDB  448(CX), Y14, Y14VPADDB  480(CX), Y15, Y15VMOVDQU 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, AXADDQ    $0x00000200, CXADDQ    $0x00000200, DXSUBQ    $0x00000200, BXJMP     int8AddBlockLoopint8AddTailLoop:CMPQ    BX, $0x00000020JL      int8AddDoneVMOVDQU (AX), Y0VPADDB  (CX), Y0, Y0VMOVDQU Y0, (DX)ADDQ    $0x00000020, AXADDQ    $0x00000020, CXADDQ    $0x00000020, DXSUBQ    $0x00000020, BXJMP     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-16MOVQ   -1, SICRC32Q data+0(FP), SIMOVQ   SI, ret+8(FP)RET

实际代码中,为了消除汇编函数调用带来的指令跳转开销,以及参数进出栈开销,使用的是批量化的版本。这里为了节约篇幅,我们用简化版举例。

编译器无法达到的特殊优化效果

下面是MatrixOne使用的两个有序64位整数数组求交集的算法的一部分:

...
loop:CMPQ  DX, DIJE    doneCMPQ  R11, R8JE    doneMOVQ  (DX), R10MOVQ  R10, (SI)CMPQ  R10, (R11)SETLE ALSETGE BLSETEQ CLSHLB  $0x03, ALSHLB  $0x03, BLSHLB  $0x03, CLADDQ  AX, DXADDQ  BX, R11ADDQ  CX, SIJMP   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社区群 技术交流

Go汇编语法和MatrixOne使用介绍相关推荐

  1. 使用arm混合汇编计算两个64位的和_混合使用C、C++和汇编语之: C、C++ 和 ARM 汇编语言之间的调用...

    12.4C' target='_blank' style='cursor:pointer;color:#D05C38;text-decoration:underline;'>C.C++和ARM汇 ...

  2. 汇编语言中xor指令_汇编语言XOR指令:对两个操作数进行逻辑(按位)异或操作(推荐)...

    汇编语言 汇编语言(assembly language)是一种用于电子计算机.微处理器.微控制器或其他可编程器件的低级语言,亦称为符号语言.在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号 ...

  3. 一款基于Latex语法和MathJax渲染的零基础公式编辑器,数学公式插件

    零基础即可编辑公式 支持自定义编辑器配置和风格 支持二次编辑公式 支持作为插件和富文本编辑器一起使用 介绍 基于Latex语法和MathJax渲染的公式编辑器插件,易用.可二次编辑.内容可视化. 标题 ...

  4. 汇编中的la_汇编语言中,SP,BP ,SI,DI作用?

    这个很简单: sp:表示栈顶指针,指向栈顶地址.与SS相配合使用.ss为栈段. bp:是基址指针,段地址默认在SS中.可以定位物理地址,比如:"mov ax,[bp+si+6]/mov ax ...

  5. linux汇编语言cmp,汇编语言中cmp指令用法笔记与总结

    这篇文章主要介绍了汇编语言中cmp指令用法,结合实例形式总结分析了汇编语言cmp指令基本功能.使用方法及操作注意事项,需要的朋友可以参考下 本文实例讲述了汇编语言中cmp指令用法.分享给大家供大家参考 ...

  6. 计算机汇编指令mov和mova,汇编语言中mov和lea指令的区别详解

    指令(instruction)是一种语句,它在程序汇编编译时变得可执行.汇编器将指令翻译为机器语言字节,并且在运行时由 CPU 加载和执行. 一条指令有四个组成部分: 标号(可选) 指令助记符(必需) ...

  7. 【自学elasticsearch7】结合es语法和java的HighLevelClient:索引(映射)创建

    [自学elasticsearch7]结合es语法和java的HighLevelClient:索引(映射)创建 写在开头 开箱即用的elasticsearch 用curl语句在es中创建第一个索引(映射 ...

  8. R语言构建仿真数据库(sqlite)并使用dplyr语法和SQL语法查询数据库、将dplyr语法查询语句翻译为SQL查询语句

    R语言构建仿真数据库(sqlite)并使用dplyr语法和SQL语法查询数据库.将dplyr语法查询语句翻译为SQL查询语句 目录

  9. html和css之间有什么区别,html语法和css语法之间有什么区别

    区别:HTML由围绕内容的标签组成,例" 内容 ":而CSS由一条或多条声明和选择器组成,每条声明由一个属性和一个值组成,例"div{font-size:12px;}&q ...

最新文章

  1. 颜水成发了个「简单到令人尴尬」的视觉模型,证明Transformer威力源自其整体架构...
  2. 在 mac OS 中安装 xgboost python 包
  3. idea插件GsonFormat的使用
  4. [渝粤教育] 西南科技大学 西方经济学 在线考试复习资料(1)
  5. IIS7.5安全配置研究
  6. 干货:怎么提高科技成果转移转化成效?
  7. 笔记-Tukey Method发现outliers(离群点)
  8. c# 将dwg文件转化为pdf
  9. ​LeetCode刷题实战507:完美数
  10. 计算机无法验证签名,win7系统无法验证文件数字签名的解决方法
  11. 计算机键盘功能键介绍6,笔记本全部按键功能的详细说明笔记本电脑键盘上有什么区别...
  12. 制定科学学习计划的重要性
  13. [20190718]12c rman新特性 表恢复.txt
  14. ArcGIS按像元栅格值提取栅格
  15. 随机获取数组中的一个元素
  16. 数据结构——逻辑结构物理结构的区别用法
  17. html实现图片切割,利用CSS切割图片技术来动态显示图片
  18. 【srs】FLV的VideoTag转SrsFrame
  19. 模糊数学Fuzzy Set第0讲——Fuzzy Mathematics Word Computing
  20. 【C】逻辑与「」、逻辑或「||」

热门文章

  1. uni-app上传图片或者说照片(调用相册或者摄像头)
  2. Qt编写自定义控件66-光晕时钟
  3. 机械设计——曲柄滑块
  4. 电子电气设备的电路隔离技术
  5. JavaScript中获取当前日期
  6. Auto Machine Learning笔记 - Bayesian Optimization
  7. scrapy爬动态网址哔哩哔哩
  8. 西游记[非常的搞笑.是不容错过的精彩片子]
  9. php abbyy,ABBYY FineReader 14(OCR文字识别软件) v14.0.101.665 官方试用版
  10. 等保测评之安全管理机构