广告
返回顶部
首页 > 资讯 > 后端开发 > GO >浅析Go汇编语法和MatrixOne使用介绍
  • 740
分享到

浅析Go汇编语法和MatrixOne使用介绍

2024-04-02 19:04:59 740人浏览 薄情痞子
摘要

目录MatrixOne数据库是什么?Go汇编介绍为什么使用Go汇编?为什么不用CGO?Go汇编语法特点操作数顺序寄存器宽度标识函数调用约定对写Go汇编代码有帮助的工具avotext/

MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP、AP、流计算等多种负载的极简大数据引擎。MatrixOne由Go语言所开发,并已于2021年10月开源,目前已经release到0.3版本。在MatrixOne已发布的性能报告中,与业界领先的OLAP数据库Clickhouse相比也不落下风。作为一款Go语言实现的数据库,可以达到c++实现的数据库一样的性能,其中一个很重要的优化就是利用Go语言自带的汇编能力,来通过调用SIMD指令进行硬件加速。本文就将对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, $0-24
	MOVQ x+0(FP), AX
	MOVQ y+8(FP), CX
    ADDQ AX, CX
    MOVQ CX, ret+16(FP)
	RET

这四条汇编代码所做的依次是:

  • 第一个操作数x放入寄存器AX
  • 第二个操作数y放入寄存器
  • CXCX加上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])
    }
        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代码中,我们只需要给变量指定寄存器类型,生成汇编的时候会自动帮我们绑定相应类型的可用寄存器。在很多场景下这确实能够带来方便。不过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指令打交道的汇编语言。

到此这篇关于浅析Go汇编语法和MatrixOne使用介绍的文章就介绍到这了,更多相关Go汇编MatrixOne使用内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: 浅析Go汇编语法和MatrixOne使用介绍

本文链接: https://www.lsjlt.com/news/146522.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 浅析Go汇编语法和MatrixOne使用介绍
    目录MatrixOne数据库是什么Go汇编介绍为什么使用Go汇编?为什么不用CGO?Go汇编语法特点操作数顺序寄存器宽度标识函数调用约定对写Go汇编代码有帮助的工具avotext/t...
    99+
    2022-11-13
  • Go汇编语法和MatrixOne使用实例分析
    这篇文章主要介绍了Go汇编语法和MatrixOne使用实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Go汇编语法和MatrixOne使用实例分析文章都会有所收获,下面我们一起来看看吧。MatrixOne...
    99+
    2023-06-30
  • Go语言中的Base64编码原理介绍以及使用
    目录前言Go Base64编码什么是Base64编码为什么需要Base64编码Base64编码原理编码步骤位数不足情况Base64解码原理Base64标准编码变种总结前言 在网络中传...
    99+
    2022-11-13
  • 浅析go语言中gopath环境的设置和使用方法
    Go语言是一种高效、可靠的编程语言,它被广泛用于Web开发、系统编程等领域。在使用Go语言编程时,设置GOPATH是非常重要的一步。本文将介绍如何设置golang的GOPATH。一、什么是GOPATHGo语言的工作空间(workspace)...
    99+
    2023-05-14
    go语言 Golang GOPATH
  • 重点介绍Golang方法的语法和使用
    Golang作为一门比较年轻的语言,在方法中也有自己独特的实现方式。本文将重点介绍Golang方法的语法和使用。一、方法定义Golang中可以为任何类型定义方法,包括引用类型和非引用类型。方法定义格式如下:func (t Type) met...
    99+
    2023-05-14
  • Go语言中定时任务库Cron使用方法介绍
    目录快速入门Cron表达式格式预定义时间表设置时区常用的方法介绍快速入门 安装cron,注意这里安装的是v3版本。新版本和旧版时间使用有所区别 go get github.com/r...
    99+
    2022-11-13
  • 浅析Go语言容器之数组和切片的使用
    目录序列容器数组VectorDequeList单链表总结在 Java 的核心库中,集合框架可谓鼎鼎大名:Array 、List、Set、Queue、HashMap 等等,随便拎一个出...
    99+
    2022-11-11
  • 浅析PHP中SQL修改语法和使用方法
    PHP中的SQL修改语句是一种用于更新数据库中现有数据的命令,它可以修改表中某个或所有记录的数据值。本文将详细介绍PHP中SQL修改语句的语法和使用方法。一、语法在PHP中,SQL修改语句的语法如下:UPDATE 表名 SET 字段名 = ...
    99+
    2023-05-14
    SQL php
  • C语言rand和srand函数使用方法介绍
    目录前言随机数的本质重新播种生成一定范围内的随机数连续生成随机数前言 在实际编程中,我们经常需要生成随机数,例如,贪吃蛇游戏中在随机的位置出现食物,扑克牌游戏中随机发牌。 在C语言中...
    99+
    2023-02-11
    C语言rand和srand C语言rand方法 C语言srand方法
  • 如何在Linux上使用Go语言进行编程和算法实现?
    在Linux上使用Go语言进行编程和算法实现是一项有趣且有挑战性的任务。Go语言是由Google公司开发的一种编程语言,它的设计目标是提供一种高效、可靠和简单的编程语言。在本文中,我们将介绍如何在Linux上使用Go语言进行编程和算法实现,...
    99+
    2023-10-29
    linux 编程算法 numy
  • 并发编程必备:掌握Go语言API和NumPy的高效使用方法!
    随着计算机性能的不断提升,我们对高并发处理的需求也越来越高。而Go语言和NumPy是两个非常优秀的工具,它们可以帮助我们快速高效地实现并发编程。本文将介绍如何掌握Go语言API和NumPy的高效使用方法,帮助你更好地进行并发编程。 一、Go...
    99+
    2023-07-26
    api numpy 并发
  • 你是否了解如何使用Linux和Go编程语言实现高效的Spring算法?
    Linux和Go编程语言都是当今非常流行的技术,它们可以帮助我们实现高效的Spring算法。在本文中,我们将介绍如何使用Linux和Go编程语言来实现高效的Spring算法,并且会穿插一些演示代码来帮助读者更好的理解。 首先,我们需要了解什...
    99+
    2023-08-30
    spring 编程算法 linux
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作