编程语言
首页 > 编程语言> > [golang]golang 汇编

[golang]golang 汇编

作者:互联网

https://lrita.github.io/2017/12/12/golang-asm/#why

在某些场景下,我们需要进行一些特殊优化,因此我们可能需要用到golang汇编,golang汇编源于plan9,此方面的 介绍很多,就不进行展开了。我们WHY和HOW开始讲起。

golang汇编相关的内容还是很少的,而且多数都语焉不详,而且缺乏细节。对于之前没有汇编经验的人来说,是很难 理解的。而且很多资料都过时了,包括官方文档的一些细节也未及时更新。因此需要掌握该知识的人需要仔细揣摩, 反复实验。

WHY

我们为什么需要用到golang的汇编,基本出于以下场景。

HOW

使用到golang会汇编时,golang的对象类型、buildin对象、语法糖还有一些特殊机制就都不见了,全部底层实现 暴露在我们面前,就像你拆开一台电脑,暴露在你面前的是一堆PCB、电阻、电容等元器件。因此我们必须掌握一些 go ABI的机制才能进行golang汇编编程。

go汇编简介

这部分内容可以参考:

寄存器

go 汇编中有4个核心的伪寄存器,这4个寄存器是编译器用来维护上下文、特殊标识等作用的:

所有用户空间的数据都可以通过FP(局部数据、输入参数、返回值)或SB(全局数据)访问。 通常情况下,不会对SB/FP寄存器进行运算操作,通常情况以会以SB/FP作为基准地址,进行偏移解引用 等操作。

SB

而且在某些情况下SB更像一些声明标识,其标识语句的作用。例如:

  1. TEXT runtime·_divu(SB), NOSPLIT, $16-0 在这种情况下,TEXT·SB共同作用声明了一个函数 runtime._divu,这种情况下,不能对SB进行解引用。
  2. GLOBL fast_udiv_tab<>(SB), RODATA, $64 在这种情况下,GLOBLfast_udiv_tabSB共同作用, 在RODATA段声明了一个私有全局变量fast_udiv_tab,大小为64byte,此时可以对SB进行偏移、解引用。
  3. CALL runtime·callbackasm1(SB) 在这种情况下,CALLruntime·callbackasm1SB共同标识, 标识调用了一个函数runtime·callbackasm1
  4. MOVW $fast_udiv_tab<>-64(SB), RM 在这种情况下,与2类似,但不是声明,是解引用全局变量 fast_udiv_tab
FB

FP伪寄存器用来标识函数参数、返回值。其通过symbol+offset(FP)的方式进行使用。例如arg0+0(FP)表示第函数第一个参数其实的位置(amd64平台),arg1+8(FP)表示函数参数偏移8byte的另一个参数。arg0/arg1用于助记,但是必须存在,否则 无法通过编译。至于这两个参数是输入参数还是返回值,得对应其函数声明的函数个数、位置才能知道。 如果操作命令是MOVQ arg+8(FP), AX的话,MOVQ表示对8byte长的内存进行移动,其实位置是函数参数偏移8byte 的位置,目的是寄存器AX,因此此命令为将一个参数赋值给寄存器AX,参数长度是8byte,可能是一个uint64,FP 前面的arg+是标记。至于FP的偏移怎么计算,会在后面的go函数调用中进行表述。同时我们 还可以在命令中对FP的解引用进行标记,例如first_arg+0(FP)FP的起始标记为参数first_arg,但是 first_arg只是一个标记,在汇编中first_arg是不存在的。

PC

实际上就是在体系结构的知识中常见的pc寄存器,在x86平台下对应ip寄存器,amd64上则是rip。除了个别跳转 之外,手写代码与PC寄存器打交道的情况较少。

images/posts/go/go-asm-pc-jump.png

SP

SP是栈指针寄存器,指向当前函数栈的栈顶,通过symbol+offset(SP)的方式使用。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是8字节,那么第一个局部变量就可以用localvar0-8(SP) 来表示。

但是硬件寄存器中也有一个SP。在用户手写的汇编代码中,如果操作SP寄存器时没有带symbol前缀,则操作的是 硬件寄存器SP。在实际情况中硬件寄存器SP与伪寄存器SP并不指向同一地址,具体硬件寄存器SP指向哪里与函 数

但是:

对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的SP都是硬件寄存器SP,无论 是否带 symbol。

我们这里对容易混淆的几点简单进行说明:

: 如何理解伪寄存器FPSP呢?其实伪寄存器FPSP相当于plan9伪汇编中的一个助记符,他们是根据当前函数栈空间计算出来的一个相对于物理寄存器SP的一个偏移量坐标。当在一个函数中,如果用户手动修改了物理寄存器SP的偏移,则伪寄存器FPSP也随之发生对应的偏移。例如

// func checking()(before uintptr, after uintptr)
TEXT ·checking(SB),$4112-16
        LEAQ x-0(SP), DI         //
        MOVQ DI, before+0(FP)    // 将原伪寄存器SP偏移量存入返回值before

        MOVQ    SP, BP           // 存储物理SP偏移量到BP寄存器
        ADDQ    $4096, SP        // 将物理SP偏移增加4K

        LEAQ x-0(SP), SI
        MOVQ    BP, SP           // 恢复物理SP,因为修改物理SP后,伪寄存器FP/SP随之改变,
                                 // 为了正确访问FP,先恢复物理SP
        MOVQ SI, cpu+8(FP)       // 将偏移后的伪寄存器SP偏移量存入返回值after
        RET
                                 // 从输出的after-before来看,正好相差4K
通用寄存器

在plan9汇编里还可以直接使用的amd64的通用寄存器,应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15这14个寄存器,虽然rbprsp也可以用,不过bpsp会被用来管 理栈顶和栈底,最好不要拿来进行运算。plan9中使用寄存器不需要带re的前缀,例如rax,只要写AX即可:

MOVQ $101, AX = mov rax, 101

下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:

X86_64raxrbxrcxrdxrdirsirbprspr8r9r10r11r12r13r14rip
Plan9 AX BX CX DX DI SI BP SP R8 R9 R10 R11 R12 R13 R14 PC

控制流

对于函数控制流的跳转,是用label来实现的,label只在函数内可见,类似goto语句:

next:
  MOVW $0, R1
  JMP  next

指令

使用汇编就意味着丧失了跨平台的特性。因此使用对应平台的汇编指令。这个需要自行去了解,也可以参考GoFunctionsInAssembly 其中有各个平台汇编指令速览和对照。

文件命名

使用到汇编时,即表明了所写的代码不能够跨平台使用,因此需要针对不同的平台使用不同的汇编 代码。go编译器采用文件名中加入平台名后缀进行区分。

比如sqrt_386.s sqrt_amd64p32.s sqrt_amd64.s sqrt_arm.s

或者使用+build tag也可以,详情可以参考go/build

函数声明

首先我们先需要对go汇编代码有一个抽象的认识,因此我们可以先看一段go汇编代码:

TEXT runtime·profileloop(SB),NOSPLIT,$8-16
  MOVQ    $runtime·profileloop1(SB), CX
  MOVQ    CX, 0(SP)
  CALL    runtime·externalthreadhandler(SB)
  RET

此处声明了一个函数profileloop,函数的声明以TEXT标识开头,以${package}·${function}为函数名。 如何函数属于本package时,通常可以不写${package},只留·${function}即可。·在mac上可以用shift+option+9 打出。$8表示该函数栈大小为8byte,计算栈大小时,需要考虑局部变量和本函数内调用其他函数时,需要传参的空间,不含函数返回地址和CALLER BP(这2个后面会讲到)。 $16表示该函数入参和返回值一共有16byte。当有NOSPLIT标识时,可以不写输入参数、返回值占用的大小。

/images/posts/go/go-asm-declare.png

那我们再看一个函数:

TEXT ·add(SB),$0-24
  MOVQ x+0(FP), BX
  MOVQ y+8(FP), BP
  ADDQ BP, BX
  MOVQ BX, ret+16(FP)
  RET

该函数等同于:

func add(x, y int64) int64 {
    return x + y
}

该函数没有局部变量,故$后第一个数为0,但其有2个输入参数,1个返回值,每个值占8byte,则第二个数为24(3*8byte)。

全局变量声明

以下就是一个私有全局变量的声明,<>表示该变量只在该文件内全局可见。 全局变量的数据部分采用DATA symbol+offset(SB)/width, value格式进行声明。

DATA divtab<>+0x00(SB)/4, $0xf4f8fcff  // divtab的前4个byte为0xf4f8fcff
DATA divtab<>+0x04(SB)/4, $0xe6eaedf0  // divtab的4-7个byte为0xe6eaedf0
...
DATA divtab<>+0x3c(SB)/4, $0x81828384  // divtab的最后4个byte为0x81828384
GLOBL divtab<>(SB), RODATA, $64        // 全局变量名声明,以及数据所在的段"RODATA",数据的长度64byte
GLOBL runtime·tlsoffset(SB), NOPTR, $4 // 声明一个全局变量tlsoffset,4byte,没有DATA部分,因其值为0。
                                       // NOPTR 表示这个变量数据中不存在指针,GC不需要扫描。

类似RODATA/NOPTR的特殊声明还有:

局部变量声明

局部变量存储在函数栈上,因此不需要额外进行声明,在函数栈上预留出空间,使用命令操作这些内存即可。因此这些 局部变量没有标识,操作时,牢记局部变量的分布、内存偏移即可。

在汇编文件中可以定义、引用宏。通过#define get_tls(r) MOVQ TLS, r类似语句来定义一个宏,语法结构与C语言类似;通过#include "textflag.h"类似语句来引用一个外部宏定义文件。

go编译器为了方便汇编中访问struct的指定字段,会在编译过程中自动生成一个go_asm.h文件,可以通过#include "go_asm.h"语言来引用,该文件中会生成该包内全部struct的每个字段的偏移量宏定义与结构体大小的宏定义,比如:

type vdsoVersionKey struct {
	version string
	verHash uint32
}

会生成宏定义:

#define vdsoVersionKey__size 24
#define vdsoVersionKey_version 0
#define vdsoVersionKey_verHash 16

在汇编代码中,我们就可以直接使用这些宏:

MOVQ vdsoVersionKey_version(DX) AX
MOVQ (vdsoVersionKey_version+vdsoVersionKey_verHash)(DX) AX

比如我们在runtime包中经常会看见一些代码就是如此:

    MOVQ    DX, m_vdsoPC(BX)
    LEAQ    ret+0(SP), DX
    MOVQ    DX, m_vdsoSP(BX)

我们可以通过命令go tool compile -S -asmhdr dump.h *.go来导出相关文件编译过程中会生成的宏定义。

地址运算

字段部分引用自《plan9-assembly-完全解析》

地址运算也是用 lea 指令,英文原意为Load Effective Address,amd64 平台地址都是8个字节,所以直接就用LEAQ就好:

LEAQ (BX)(AX*8), CX
// 上面代码中的 8 代表 scale
// scale 只能是 0、2、4、8
// 如果写成其它值:
// LEAQ (BX)(AX*3), CX
// ./a.s:6: bad scale: 3
// 整个表达式含义是 CX = BX + (AX * 8)
// 如果要表示3倍的乘法可以表示为:
LEAQ (AX)(AX*2), CX // => CX = AX + (AX * 2) = AX * 3

// 用 LEAQ 的话,即使是两个寄存器值直接相加,也必须提供 scale
// 下面这样是不行的
// LEAQ (BX)(AX), CX
// asm: asmidx: bad address 0/2064/2067
// 正确的写法是
LEAQ (BX)(AX*1), CX


// 在寄存器运算的基础上,可以加上额外的 offset
LEAQ 16(BX)(AX*1), CX
// 整个表达式含义是 CX = 16 + BX + (AX * 8)

// 三个寄存器做运算,还是别想了
// LEAQ DX(BX)(AX*8), CX
// ./a.s:13: expected end of operand, found (

其余MOVQ等表达式的区别是,在寄存器加偏移的情况下MOVQ会对地址进行解引用:

MOVQ (AX), BX   // => BX = *AX 将AX指向的内存区域8byte赋值给BX
MOVQ 16(AX), BX // => BX = *(AX + 16)


MOVQ AX, BX     // => BX = AX 将AX中存储的内容赋值给BX,注意区别

buildin类型

在golang汇编中,没有struct/slice/string/map/chan/interface{}等类型,有的只是寄存器、内存。因此我们需要了解这些 类型对象在汇编中是如何表达的。

(u)int??/float??

uint32就是32bit长的一段内存,float64就是64bit长的一段内存,其他相似类型可以以此类推。

int/unsafe.Pointer/unint

在32bit系统中int等同于int32uintptr等同于uint32unsafe.Pointer长度32bit。

在64bit系统中int等同于int64uintptr等同于uint64unsafe.Pointer长度64bit。

byte等同于uint8rune等同于int32

string底层是StringHeader 这样一个结构体,slice底层是SliceHeader 这样一个结构体。

map

map是指向hmap 的一个unsafe.Pointer

chan

chan是指向hchan 的一个unsafe.Pointer

interface{}

interface{}eface 这样一个结构体。详细可以参考深入解析GO

go函数调用

通常函数会有输入输出,我们要进行编程就需要掌握其ABI,了解其如何传递输入参数、返回值、调用函数。

go汇编使用的是caller-save模式,因此被调用函数的参数、返回值、栈位置都需要由调用者维护、准备。因此 当你需要调用一个函数时,需要先将这些工作准备好,方能调用下一个函数,另外这些都需要进行内存对其,对其 的大小是sizeof(uintptr)

我们将结合一些函数来进行说明:

无局部变量的函数

注意:其实go函数的栈布局在是否有局部变量时,是没有区别的。在没有局部变量时,只是少了局部变量那部分空间。在当时研究的时候,未能抽象其共同部分,导致拆成2部分写了。

对于手写汇编来说,所有参数通过栈来传递,通过伪寄存器FP偏移进行访问。函数的返回值跟随在输入参数 后面,并且对其到指针大小。amd64平台上指针大小为8byte。如果输入参数为20byte。则返回值会在从24byte其, 中间跳过4byte用以对其。

func xxx(a, b, c int) (e, f, g int) {
	e, f, g = a, b, c
	return
}

该函数有3个输入参数、3个返回值,假设我们使用x86_64平台,因此一个int占用8byte。则其函数栈空间为:

高地址位
                ┼───────────┼
                │  返回值g   │
                ┼───────────┼
                │  返回值f   │
                ┼───────────┼
                │  返回值e   │
                ┼───────────┼
                │  参数之c   │
                ┼───────────┼
                │  参数之b   │
                ┼───────────┼
                │  参数之a   │
                ┼───────────┼     <-- 伪FP
                │ 函数返回地址│
                ┼───────────┼     <-- 伪SP 和 硬件SP
低地址位

各个输入参数和返回值将以倒序的方式从高地址位分布于栈空间上,由于没有局部变量,则xxx的函数栈空间为 0,根据前面的描述,该函数应该为:

#include "textflag.h"

TEXT ·xxx(SB),NOSPLIT,$0-48
   MOVQ a+0(FP), AX           // FP+0  为参数a,将其值拷贝到寄存器AX中
   MOVQ AX, e+24(FP)          // FP+24 为返回值e,将寄存器AX赋值给返回值e
   MOVQ b+8(FP), AX           // FP+8  为参数b
   MOVQ AX, f+32(FP)          // FP+24 为返回值f
   MOVQ c+16(FP), AX          // FP+16 为参数c
   MOVQ AX, g+40(FP)          // FP+24 为返回值g
   RET                        // return

然后在一个go源文件(.go)中声明该函数即可

func xxx(a, b, c int) (e, f, g int)

有局部变量的函数

当函数中有局部变量时,函数的栈空间就应该留出足够的空间:

func zzz(a, b, c int) [3]int{
	var d [3]int
	d[0], d[1], d[2] = a, b, c
	return d
}

当函数中有局部变量时,我们就需要移动函数栈帧来进行栈内存分配,因此我们就需要了解相关平台计算机体系 的一些设计问题,在此我们只讲解x86平台的相关要求,我们先需要参考:

  1. Where the top of the stack is on x86
  2. Stack frame layout on x86-64
  3. x86 Assembly Guide

其中讲到x86平台上BP寄存器,通常用来指示函数栈的起始位置,仅仅其一个指示作用,现代编译器生成的代码 通常不会用到BP寄存器,但是可能某些debug工具会用到该寄存器来寻找函数参数、局部变量等。因此我们写汇编 代码时,也最好将栈起始位置存储在BP寄存器中。因此在amd64平台上,会在函数返回值之后插入8byte来放置CALLER BP寄存器。

此外需要注意的是,CALLER BP是在编译期由编译器插入的,用户手写代码时,计算framesize时是不包括这个 CALLER BP部分的,但是要计算函数返回值的8byte。是否插入CALLER BP的主要判断依据是:

此处需要注意,go编译器会将函数栈空间自动加8,用于存储BP寄存器,跳过这8字节后才是函数栈上局部变量的内存。 逻辑上的FP/SP位置就是我们在写汇编代码时,计算偏移量时,FP/SP的基准位置,因此局部变量的内存在逻辑SP的低地 址侧,因此我们访问时,需要向负方向偏移。

实际上,在该函数被调用后,编译器会添加SUBQ/LEAQ代码修改物理SP指向的位置。我们在反汇编的代码中能看到这部分操作,因此我们需要注意物理SP与伪SP指向位置的差别。

高地址位
          ┼───────────┼
          │  返回值g   │
          ┼───────────┼
          │  返回值f   │
          ┼───────────┼
          │  返回值e   │
          ┼───────────┼
          │  参数之c   │
          ┼───────────┼
          │  参数之b   │
          ┼───────────┼
          │  参数之a   │
          ┼───────────┼    <-- 伪FP
          │ 函数返回地址│
          ┼───────────┼
          │ CALLER BP │
          ┼───────────┼    <-- 伪SP
          │  变量之[2] │    <-- d0-8(SP)
          ┼───────────┼
          │  变量之[1] │    <-- d1-16(SP)
          ┼───────────┼
          │  变量之[0] │    <-- d2-24(SP)
          ┼───────────┼    <-- 硬件SP
低地址位

图中的函数返回地址使用的是调用者的栈空间,CALLER BP由编辑器“透明”插入,因此,不算在当前函数的栈空间内。我们实现该函数的汇编代码:

#include "textflag.h"

TEXT ·zzz(SB),NOSPLIT,$24-48    // $24值栈空间24byte,- 后面的48跟上面的含义一样,
                                // 在编译后,栈空间会被+8用于存储BP寄存器,这步骤由编译器自动添加
   MOVQ    $0, d-24(SP)         // 初始化d[0]
   MOVQ    $0, d-16(SP)         // 初始化d[1]
   MOVQ    $0, d-8(SP)          // 初始化d[2]
   MOVQ    a+0(FP), AX          // d[0] = a
   MOVQ    AX, d-24(SP)         //
   MOVQ    b+8(FP), AX          // d[1] = b
   MOVQ    AX, d-16(SP)         //
   MOVQ    c+16(FP), AX         // d[2] = c
   MOVQ    AX, d-8(SP)          //
   MOVQ    d-24(SP), AX         // d[0] = return [0]
   MOVQ    AX, r+24(FP)         //
   MOVQ    d-16(SP), AX         // d[1] = return [1]
   MOVQ    AX, r+32(FP)         //
   MOVQ    d-8(SP), AX          // d[2] = return [2]
   MOVQ    AX, r+40(FP)         //
   RET                          // return

然后我们go源码中声明该函数:

func zzz(a, b, c int) [3]int

汇编中调用其他函数

在汇编中调用其他函数通常可以使用2中方式:

下面演示一个CALL方法调用的例子:

func yyy(a, b, c int) [3]int {
    return zzz(a, b, c)
}

该函数使用汇编实现就是:

TEXT ·yyy0(SB), $48-48
   MOVQ a+0(FP), AX
   MOVQ AX, ia-48(SP)
   MOVQ b+8(FP), AX
   MOVQ AX, ib-40(SP)
   MOVQ c+16(FP), AX
   MOVQ AX, ic-32(SP)
   CALL ·zzz(SB)
   MOVQ z2-24(SP), AX
   MOVQ AX, r2+24(FP)
   MOVQ z1-16(SP), AX
   MOVQ AX, r1+32(FP)
   MOVQ z1-8(SP), AX
   MOVQ AX, r2+40(FP)
   RET

然后在go文件中声明yyy0,并且在main函数中调用:

func yyy0(a, b, c int) [3]int

//go:noinline
func yyy1(a, b, c int) [3]int {
    return zzz(a, b, c)
}

func main() {
    y0 := yyy0(1, 2, 3)
    y1 := yyy1(1, 2, 3)
    println("yyy0", y0[0], y0[1], y0[2])
    println("yyy1", y1[0], y1[1], y1[2])
}

在函数yyy0的栈空间分布为:

高地址位
          ┼───────────┼
          │ 返回值[2]  │    <-- r2+40(FP)
          ┼───────────┼
          │ 返回值[1]  │    <-- r1+32(FP)
          ┼───────────┼
          │ 返回值[0]  │    <-- r2+24(FP)
          ┼───────────┼
          │  参数之c   │    <-- c+16(FP)
          ┼───────────┼
          │  参数之b   │    <-- b+8(FP)
          ┼───────────┼
          │  参数之a   │    <-- a+0(FP)
          ┼───────────┼    <-- 伪FP
          │ 函数返回地址│    <-- yyy0函数返回值
          ┼───────────┼
          │ CALLER BP │
          ┼───────────┼    <-- 伪SP
          │ 返回值[2]  │    <-- z1-8(SP)
          ┼───────────┼
          │ 返回值[1]  │    <-- z1-16(SP)
          ┼───────────┼
          │ 返回值[0]  │    <-- z2-24(SP)
          ┼───────────┼
          │  参数之c   │    <-- ic-32(SP)
          ┼───────────┼
          │  参数之b   │    <-- ib-40(SP)
          ┼───────────┼
          │  参数之a   │    <-- ia-48(SP)
          ┼───────────┼    <-- 硬件SP
低地址位

其调用者和被调用者的栈关系为(该图来自plan9 assembly 完全解析):


                                       caller
                                 +------------------+
                                 |                  |
       +---------------------->  --------------------
       |                         |                  |
       |                         | caller parent BP |
       |           BP(pseudo SP) --------------------
       |                         |                  |
       |                         |   Local Var0     |
       |                         --------------------
       |                         |                  |
       |                         |   .......        |
       |                         --------------------
       |                         |                  |
       |                         |   Local VarN     |
                                 --------------------
 caller stack frame              |                  |
                                 |   callee arg2    |
       |                         |------------------|
       |                         |                  |
       |                         |   callee arg1    |
       |                         |------------------|
       |                         |                  |
       |                         |   callee arg0    |
       |      SP(Real Register)  ----------------------------------------------+   FP(virtual register)
       |                         |                  |                          |
       |                         |   return addr    |  parent return address   |
       +---------------------->  +------------------+---------------------------    <-------------------------------+
                                                    |  caller BP               |                                    |
                                                    |  (caller frame pointer)  |                                    |
                                     BP(pseudo SP)  ----------------------------                                    |
                                                    |                          |                                    |
                                                    |     Local Var0           |                                    |
                                                    ----------------------------                                    |
                                                    |                          |
                                                    |     Local Var1           |
                                                    ----------------------------                            callee stack frame
                                                    |                          |
                                                    |       .....              |
                                                    ----------------------------                                    |
                                                    |                          |                                    |
                                                    |     Local VarN           |                                    |
                                  SP(Real Register) ----------------------------                                    |
                                                    |                          |                                    |
                                                    |                          |                                    |
                                                    |                          |                                    |
                                                    |                          |                                    |
                                                    |                          |                                    |
                                                    +--------------------------+    <-------------------------------+

                                                              callee

此外我们还可以做一些优化,其中中间的临时变量,让zzz的输入参数、返回值复用yyy的输入参数、返回值 这部分空间,其代码为:

TEXT ·yyy(SB),NOSPLIT,$0-48
   MOVQ pc+0(SP),          AX            // 将PC寄存器中的值暂时保存在最后一个返回值的位置,因为在
                                         // 调用zzz时,该位置不会参与计算
   MOVQ AX,                ret_2+40(FP)  //
   MOVQ a+0(FP),           AX            // 将输入参数a,放置在栈顶
   MOVQ AX,                z_a+0(SP)     //
   MOVQ b+8(FP),           AX            // 将输入参数b,放置在栈顶+8
   MOVQ AX,                z_b+8(SP)     //
   MOVQ c+16(FP),          AX            // 将输入参数c,放置在栈顶+16
   MOVQ AX,                z_c+16(SP)    //
   CALL ·zzz(SB)                         // 调用函数zzz
   MOVQ ret_2+40(FP),      AX            // 将PC寄存器恢复
   MOVQ AX,                pc+0(SP)      //
   MOVQ z_ret_2+40(SP),    AX            // 将zzz的返回值[2]防止在yyy返回值[2]的位置
   MOVQ AX,                ret_2+40(FP)  //
   MOVQ z_ret_1+32(SP),    AX            // 将zzz的返回值[1]防止在yyy返回值[1]的位置
   MOVQ AX,                ret_1+32(FP)  //
   MOVQ z_ret_0+24(SP),    AX            // 将zzz的返回值[0]防止在yyy返回值[0]的位置
   MOVQ AX,                ret_0+24(FP)  //
   RET                                   // return

整个函数调用过程为:

高地址位
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │  返回值[2] │           │  函数返回值  │          │     PC     │
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │  返回值[1] │           │zzz返回值[2] │          │zzz返回值[2] │
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │  返回值[0] │           │zzz返回值[1] │          │zzz返回值[1] │
            ┼───────────┼  =调整后=> ┼────────────┼ =调用后=> ┼────────────┼
            │   参数之c  │           │zzz返回值[0] │          │zzz返回值[0] │
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │   参数之b  │           │   参数之c   │          │   参数之c   │
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │  参数之a   │  <-- FP   │   参数之b   │  <-- FP  │   参数之b   │
            ┼───────────┼           ┼────────────┼          ┼────────────┼
            │  函数返回值 │  <-- SP   │   参数之a   │  <-- SP  │   参数之a   │  <--FP
            ┼───────────┼           ┼────────────┼          ┼────────────┼
                                                            │  函数返回值  │  <--SP  zzz函数栈空间
                                                            ┼────────────┼
                                                            │ CALLER BP  │
                                                            ┼────────────┼
                                                            │  zzz变量之2 │
                                                            ┼────────────┼
                                                            │  zzz变量之1 │
                                                            ┼────────────┼
                                                            │  zzz变量之0 │
                                                            ┼────────────┼
低地址位

然后我们可以使用反汇编来对比我们自己实现的汇编代码版本和go源码版本生成的汇编代码的区别:

我们自己汇编的版本:

TEXT main.yyy(SB) go/asm/xx.s
  xx.s:31               0x104f6b0               488b0424                MOVQ 0(SP), AX
  xx.s:32               0x104f6b4               4889442430              MOVQ AX, 0x30(SP)
  xx.s:33               0x104f6b9               488b442408              MOVQ 0x8(SP), AX
  xx.s:34               0x104f6be               48890424                MOVQ AX, 0(SP)
  xx.s:35               0x104f6c2               488b442410              MOVQ 0x10(SP), AX
  xx.s:36               0x104f6c7               4889442408              MOVQ AX, 0x8(SP)
  xx.s:37               0x104f6cc               488b442418              MOVQ 0x18(SP), AX
  xx.s:38               0x104f6d1               4889442410              MOVQ AX, 0x10(SP)
  xx.s:39               0x104f6d6               e865ffffff              CALL main.zzz(SB)
  xx.s:40               0x104f6db               488b442430              MOVQ 0x30(SP), AX
  xx.s:41               0x104f6e0               48890424                MOVQ AX, 0(SP)
  xx.s:42               0x104f6e4               488b442428              MOVQ 0x28(SP), AX
  xx.s:43               0x104f6e9               4889442430              MOVQ AX, 0x30(SP)
  xx.s:44               0x104f6ee               488b442420              MOVQ 0x20(SP), AX
  xx.s:45               0x104f6f3               4889442428              MOVQ AX, 0x28(SP)
  xx.s:46               0x104f6f8               488b442418              MOVQ 0x18(SP), AX
  xx.s:47               0x104f6fd               4889442420              MOVQ AX, 0x20(SP)
  xx.s:48               0x104f702               c3                      RET

go源码版本生成的汇编:

TEXT main.yyy(SB) go/asm/main.go
  main.go:20            0x104f360               4883ec50                        SUBQ $0x50, SP
  main.go:20            0x104f364               48896c2448                      MOVQ BP, 0x48(SP)
  main.go:20            0x104f369               488d6c2448                      LEAQ 0x48(SP), BP
  main.go:20            0x104f36e               48c744247000000000              MOVQ $0x0, 0x70(SP)
  main.go:20            0x104f377               48c744247800000000              MOVQ $0x0, 0x78(SP)
  main.go:20            0x104f380               48c784248000000000000000        MOVQ $0x0, 0x80(SP)
  main.go:20            0x104f38c               488b442458                      MOVQ 0x58(SP), AX
  main.go:21            0x104f391               48890424                        MOVQ AX, 0(SP)
  main.go:20            0x104f395               488b442460                      MOVQ 0x60(SP), AX
  main.go:21            0x104f39a               4889442408                      MOVQ AX, 0x8(SP)
  main.go:20            0x104f39f               488b442468                      MOVQ 0x68(SP), AX
  main.go:21            0x104f3a4               4889442410                      MOVQ AX, 0x10(SP)
  main.go:21            0x104f3a9               e892020000                      CALL main.zzz(SB)
  main.go:21            0x104f3ae               488b442418                      MOVQ 0x18(SP), AX
  main.go:21            0x104f3b3               4889442430                      MOVQ AX, 0x30(SP)
  main.go:21            0x104f3b8               0f10442420                      MOVUPS 0x20(SP), X0
  main.go:21            0x104f3bd               0f11442438                      MOVUPS X0, 0x38(SP)
  main.go:22            0x104f3c2               488b442430                      MOVQ 0x30(SP), AX
  main.go:22            0x104f3c7               4889442470                      MOVQ AX, 0x70(SP)
  main.go:22            0x104f3cc               0f10442438                      MOVUPS 0x38(SP), X0
  main.go:22            0x104f3d1               0f11442478                      MOVUPS X0, 0x78(SP)
  main.go:22            0x104f3d6               488b6c2448                      MOVQ 0x48(SP), BP
  main.go:22            0x104f3db               4883c450                        ADDQ $0x50, SP
  main.go:22            0x104f3df               c3                              RET

经过对比可以看出我们的优点:

go源码版本的优点:

我们可以运行一下go benchmark来比较一下两个版本,可以看出自己的汇编版本速度上明显快于go源码版本。

go test -bench=. -v -count=3
goos: darwin
goarch: amd64
BenchmarkYyyGoVersion-4        100000000            16.9 ns/op
BenchmarkYyyGoVersion-4        100000000            17.0 ns/op
BenchmarkYyyGoVersion-4        100000000            17.1 ns/op
BenchmarkYyyAsmVersion-4       200000000            10.1 ns/op
BenchmarkYyyAsmVersion-4       200000000             7.90 ns/op
BenchmarkYyyAsmVersion-4       200000000             8.01 ns/op
PASS
ok      go/asm    13.005s

回调函数/闭包

var num int
func call(fn func(), n int) {
	fn()
	num += n
}

func basecall() {
	call(func() {
		num += 5
	}, 1)
}

如上面所示,当函数(call)参数中包含回调函数(fn)时,回调函数的指针通过一种简介方式传入,之所以采用这种设计也是为了照顾闭包调用的实现。接下来简单介绍一下这种传参。当一个函数的参数为一个函数时,其调用者与被调用者之间的关系如下图所示:

                                       caller
                                 +------------------+
                                 |                  |
       +---------------------->  --------------------
       |                         |                  |
       |                         | caller parent BP |
       |           BP(pseudo SP) --------------------
       |                         |                  |
       |                         |   Local Var0     |
       |                         --------------------
       |                         |                  |
       |                         |   .......        |
       |                         --------------------
 caller stack frame              |                  |
       |                         |   Local VarN     |      ┼────────────┼
       |                         |------------------|      │    ....    │  如果是闭包时,可
       |                         |                  |      ┼────────────┼  以扩展该区域存储
       |                         |   callee arg1(n) |      │    ....    │  闭包中的变量。
       |                         |------------------|      ┼────────────┼
       |                         |                  | ---->│ fn pointer │  间接临时区域
       |                         |   callee arg0    |      ┼────────────┼
       |      SP(Real Register)  ----------------------------------------------+   FP(virtual register)
       |                         |                  |                          |
       |                         |   return addr    |  parent return address   |
       +---------------------->  +------------------+---------------------------    <-------------------------------+
                                                    |  caller BP               |                                    |
                                                    |  (caller frame pointer)  |                                    |
                                     BP(pseudo SP)  ----------------------------                                    |
                                                    |                          |                                    |
                                                    |     Local Var0           |                                    |
                                                    ----------------------------                                    |
                                                    |                          |
                                                    |     Local Var1           |
                                                    ----------------------------                            callee stack frame
                                                    |                          |
                                                    |       .....              |
                                                    ----------------------------                                    |
                                                    |                          |                                    |
                                                    |     Local VarN           |                                    |
                                  SP(Real Register) ----------------------------                                    |
                                                    |                          |                                    |
                                                    |                          |                                    |
                                                    +--------------------------+    <-------------------------------+

                                                              callee

在golang的ABI中,关于回调函数、闭包的上下文由调用者(caller-basecall)来维护,被调用者(callee-call)直接按照规定的格式来使用即可。

  1. 调用者需要申请一段临时内存区域来存储函数(func() { num+=5 })的指针,当传递参数是闭包时,该临时内存区域开可以进行扩充,用于存储闭包中捕获的变量,通常编译器将该内存区域定义成型为struct { F uintptr; a *int }的结构。该临时内存区域可以分配在栈上,也可以分配在堆上,还可以分配在寄存器上。到底分配在哪里,需要编译器根据逃逸分析的结果来决定;
  2. 将临时内存区域的地址存储于对应被调用函数入参的对应位置上;其他参数按照上面的常规方法放置;
  3. 使用CALL执行调用被调用函数(callee-call);
  4. 在被调用函数(callee-call)中从对应参数位置中取出临时内存区域的指针存储于指定寄存器DX(仅针对amd64平台)
  5. 然后从DX指向的临时内存区域的首部取出函数(func() { num+=5 })指针,存储于AX(此处寄存器可以任意指定)
  6. 然后在执行CALL AX指令来调用传入的回调函数。
  7. 当回调函数是闭包时,需要使用捕获的变量时,直接通过集群器DX加对应偏移量来获取。

下面结合几个例子来理解:

例一

func callback() {
	println("xxx")
}

func call(fn func()) {
	fn()
}

func call1() {
	call(callback)
}

func call0()

其中call0函数可以用汇编实现为:

TEXT ·call0(SB), $16-0           # 分配栈空间16字节,8字节为call函数的入参,8字节为间接传参的'临时内存区域'
	LEAQ	·callback(SB), AX    # 取·callback函数地址存储于'临时内存区域'
	MOVQ	AX, fn-8(SP)         #
	LEAQ	fn-8(SP), AX         # 取'临时内存区域'地址存储于call入参位置
	MOVQ	AX, fn-16(SP)        #
	CALL	·call(SB)            # 调用call函数
	RET

注意:如果我们使用go tool compile -l -N -S来获取call1的实现,可以的得到:

TEXT    "".call1(SB), ABIInternal, $16-0
    MOVQ    (TLS), CX
    CMPQ    SP, 16(CX)
    JLS     55
    SUBQ    $16, SP
    MOVQ    BP, 8(SP)
    LEAQ    8(SP), BP
    FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    FUNCDATA        $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
    PCDATA  $2, $1
    PCDATA  $0, $0                 # 以上是函数编译器生成的栈管理,不用理会
    LEAQ    "".callback·f(SB), AX  # 这部分,貌似没有分配'临时内存区域'进行中转,
    PCDATA  $2, $0                 # 而是直接将函数地址赋值给call的参数。然后按
    MOVQ    AX, (SP)               # 照这样写,会出现SIGBUS错误。对比之下,其猫
    CALL    "".call(SB)            # 腻可能出现在`callback·f`上,此处可能包含
    MOVQ    8(SP), BP              # 一些隐藏信息,因为手写汇编采用这种格式是会
    ADDQ    $16, SP                # 编译错误的。
    RET

例二

func call(fn func(), n int) {
	fn()
}

func testing() {
	var n int
	call(func() {
		n++
	}, 1)
	_ = n
}

其生成的汇编为:

TEXT	testing.func1(SB), NOSPLIT|NEEDCTXT, $16-0  # NEEDCTXT标识闭包
	MOVQ	8(DX), AX                               # 从DX+8偏移出取出捕获参数n的指针
	INCQ	(AX)                                    # 对参数n指针指向的内存执行++操作,n++
	RET

TEXT	testing(SB), NOSPLIT, $56-0
	MOVQ	$0, n+16(SP)             # 初始化栈上临时变量n
	XORPS	X0, X0                   # 清空寄存器X0
	MOVUPS	X0, autotmp_2+32(SP)     # 用X0寄存器初始化栈上临时空间,该空间是分配给闭包的临时内存区域
	LEAQ	autotmp_2+32(SP), AX     # 取临时内存区域指针到AX
	MOVQ	AX, autotmp_3+24(SP)     # 不知道此步有何用意,liveness?
	TESTB	AL, (AX)
	LEAQ	testing.func1(SB), CX    # 将闭包函数指针存储于临时内存区域首部
	MOVQ	CX, autotmp_2+32(SP)
	TESTB	AL, (AX)
	LEAQ	n+16(SP), CX             # 将临时变量n的地址存储于临时内存区域尾部
	MOVQ	CX, autotmp_2+40(SP)
	MOVQ	AX, (SP)                 # 将临时内存区域地址赋值给call函数入参1
	MOVQ	$1, 8(SP)                # 将立即数1赋值给call函数入参2
	CALL	call(SB)                 # 调用call函数
	RET

# func call(fn func(), n int)
TEXT	call(SB), NOSPLIT, $8-16
	MOVQ	"".fn+16(SP), DX     # 取出临时区域的地址到DX
	MOVQ	(DX), AX             # 对首部解引用获取函数指针,存储到AX
	CALL	AX                   # 调用闭包函数
	RET

直接调用C函数(FFI)

我们都知道CGO is not Go,在go中调用C函数存在着巨大额外开销,而一些短小精悍的C函数,我们可以考虑绕过CGO机制,直接调用,比如runtime包中vDSO的调用、fastcgorustgo等。要直接调用C函数,就要遵循C的ABI。

amd64 C ABI

在调用C函数时,主流有2种ABI:

在ABI规定中,涉及内容繁多,下面简单介绍一下System V ABI中参数传递的协议:

issue#20427

由于该issue的存在,通常goroutine的栈空间很小,很可能产生栈溢出的错误。解决的方法有:

编译/反编译

因为go汇编的资料很少,所以我们需要通过编译、反汇编来学习。

// 编译
go build -gcflags="-S"
go tool compile -S hello.go
go tool compile -l -N -S hello.go // 禁止内联 禁止优化
// 反编译
go tool objdump <binary>

总结

了解go汇编并不是一定要去手写它,因为汇编总是不可移植和难懂的。但是它能够帮助我们了解go的一些底层机制, 了解计算机结构体系,同时我们需要做一些hack的事时可以用得到。

比如,我们可以使用go:noescape来减少内存的分配:

很多时候,我们可以使函数内计算过程使用栈上的空间做缓存,这样可以减少对内存的使用,并且是计算速度更快:

func xxx() int{
	var buf [1024]byte
	data := buf[:]
	// do something in data
}

但是,很多时候,go编译器的逃逸分析并不让人满意,经常会使buf移动到堆内存上,造成不必要的内存分配。 这是我们可以使用sync.Pool,但是总让人不爽。因此我们使用汇编完成一个noescape函数,绕过go编译器的 逃逸检测,使buf不会移动到堆内存上。

// asm_amd64.s
#include "textflag.h"

TEXT ·noescape(SB),NOSPLIT,$0-48
        MOVQ    d_base+0(FP),   AX
        MOVQ    AX,     b_base+24(FP)
        MOVQ    d_len+8(FP),    AX
        MOVQ    AX,     b_len+32(FP)
        MOVQ    d_cap+16(FP),AX
        MOVQ    AX,     b_cap+40(FP)
        RET
//此处使用go编译器的指示
//go:noescape
func noescape(d []byte) (b []byte)

func xxx() int {
	var buf [1024]byte
	data := noescape(buf[:])
	// do something in data
	// 这样可以确保buf一定分配在xxx的函数栈上
}

c2goasm

当我们需要做一些密集的数列运算或实现其他算法时,我们可以使用先进CPU的向量扩展指令集进行加速,如sse4_2/avx/avx2/avx-512等。有些人觉得通常可以遇不见这样的场景,其实能够用到这些的场景还是很多的。比如,我们常用的监控采集go-metrics库,其中就有很多可以优化的地方,如SampleSumSampleMaxSampleMin这些函数都可以进行加速。

但是,虽然这些方法很简单,但是对于汇编基础很弱的人来说,手写这些sse4_2/avx/avx2/avx-512指令代码,仍然是很困难的。但是,我们可以利用clang/gcc这些深度优化过的C语言编译器来帮我们生成对于的汇编代码。

所幸,这项工作已经有人帮我们很好的完成了,那就是c2goasmc2goasm可以将C/C++编译器生成的汇编代码转换为golang汇编代码。在这里,我们可以学习该工具如何使用。它可以帮助我们在代码利用上sse4_2/avx/avx2/avx-512等这些先进指令。但是这些执行需要得到CPU的支持。因此我们先要判断使用的CPU代码是否支持。

注意c2goasm中其中有很多默认规则需要我们去遵守:

在linux上,我们可以使用命令cat /proc/cpuinfo |grep flags来查看CPU支持的指令集。但是在工作环境中,我们的代码需要在多个环境中运行,比如开发环境、和生产环境,这些环境之间可能会有很大差别,因此我们希望我们的代码可以动态支持不同的CPU环境。这里,我们可以用到intel-go/cpuid,我们可以实现多个指令版本的代码,然后根据运行环境中CPU的支持情况,选择实际实行哪一段逻辑:

package main

import (
	"fmt"

	"github.com/intel-go/cpuid"
)

func main() {
	fmt.Println("EnabledAVX", cpuid.EnabledAVX)
	fmt.Println("EnabledAVX512", cpuid.EnabledAVX512)
	fmt.Println("SSE4_1", cpuid.HasFeature(cpuid.SSE4_1))
	fmt.Println("SSE4_2", cpuid.HasFeature(cpuid.SSE4_2))
	fmt.Println("AVX", cpuid.HasFeature(cpuid.AVX))
	fmt.Println("AVX2", cpuid.HasExtendedFeature(cpuid.AVX2))
}

然后,我们可以先使用C来实现这3个函数:

#include <stdint.h>
/* 我们要实现3中指令的汇编实现,因此我们需要生成3个版本的C代码,此处使用宏来辅助添加后缀,避免生成的函数名冲突 */
#if defined ENABLE_AVX2
#define NAME(x) x##_avx2
#elif defined ENABLE_AVX
#define NAME(x) x##_avx
#elif defined ENABLE_SSE4_2
#define NAME(x) x##_sse4_2
#endif

int64_t NAME(sample_sum)(int64_t *beg, int64_t len) {
    int64_t sum = 0;
    int64_t *end = beg + len;
    while (beg < end) {
        sum += *beg++;
    }
    return sum;
}

int64_t NAME(sample_max)(int64_t *beg, int64_t len) {
    int64_t max = 0x8000000000000000;
    int64_t *end = beg + len;
	if (len == 0) {
        return 0;
    }
    while (beg < end) {
        if (*beg > max) {
            max = *beg;
        }
        beg++;
    }
    return max;
}

int64_t NAME(sample_min)(int64_t *beg, int64_t len) {
    if (len == 0) {
        return 0;
    }
    int64_t min = 0x7FFFFFFFFFFFFFFF;
    int64_t *end = beg + len;
    while (beg < end) {
        if (*beg < min) {
            min = *beg;
        }
        beg++;
    }
    return min;
}

然后使用clang生成三中指令的汇编代码:

clang -S -DENABLE_SSE4_2 -target x86_64-unknown-none -masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -O3 -fno-builtin -ffast-math -msse4 lib/sample.c -o lib/sample_sse4.s
clang -S -DENABLE_AVX -target x86_64-unknown-none -masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -O3 -fno-builtin -ffast-math -mavx lib/sample.c -o lib/sample_avx.s
clang -S -DENABLE_AVX2 -target x86_64-unknown-none -masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -O3 -fno-builtin -ffast-math -mavx2 lib/sample.c -o lib/sample_avx2.s

注意:此处目前有一个待解决的问题issues8,如果谁指定如何解决,请帮助我一下。使用clang生成的AVX2汇编代码,其中局部变量0x8000000000000000/0x7FFFFFFFFFFFFFFF会被分片到RODATA段,并且使用32byte对其。使用c2goasm转换时,会生成一个很大的全局变量(几个G…,此处会运行很久)。目前的解决方式是,将生成

.LCPI1_0:
	.quad	-9223372036854775808    # 0x8000000000000000
	.section	.rodata,"a",@progbits
	.align	32
.LCPI1_1:
	.long	0                       # 0x0
	.long	2                       # 0x2
	.long	4                       # 0x4
	.long	6                       # 0x6
	.zero	4
	.zero	4
	.zero	4
	.zero	4
	.text
	.globl	sample_max_avx2

改为:

.LCPI1_0:
        .quad   -9223372036854775808    # 0x8000000000000000
        .quad   -9223372036854775808    # 0x8000000000000000
        .quad   -9223372036854775808    # 0x8000000000000000
        .quad   -9223372036854775808    # 0x8000000000000000
        .section        .rodata,"a",@progbits
.LCPI1_1:
        .long   0                       # 0x0
        .long   2                       # 0x2
        .long   4                       # 0x4
        .long   6                       # 0x6
        .zero   4
        .zero   4
        .zero   4
        .zero   4
        .text
        .globl  sample_max_avx2
        .align  16, 0x90
        .type   sample_max_avx2,@function

另一处同理,具体修改后的结果为:sample_avx2.s

回归正题,添加对应的go函数声明,我们要生成的三个go汇编文件为:sample_sse4_amd64.ssample_avx_amd64.ssample_avx2_amd64.s,因此对应的三个go文件为:sample_sse4_amd64.gosample_avx_amd64.gosample_avx2_amd64.go。 其中声明的go函数为下面,我们挑其中一个文件说,其他两个类似:

package sample

import "unsafe"

// 声明的go汇编函数,不支持go buildin 数据类型,参数个数要与c实现的参数个数相等,最多支持14个。
//go:noescape
func _sample_sum_sse4_2(addr unsafe.Pointer, len int64) (x int64)

//go:noescape
func _sample_max_sse4_2(addr unsafe.Pointer, len int64) (x int64)

//go:noescape
func _sample_min_sse4_2(addr unsafe.Pointer, len int64) (x int64)

// 因为我们希望输入参数为一个slice,则我们在下面进行3个封装。

func sample_sum_sse4_2(v []int64) int64 {
    x := (*slice)(unsafe.Pointer(&v))
    return _sample_sum_sse4_2(x.addr, x.len)
}

func sample_max_sse4_2(v []int64) int64 {
    x := (*slice)(unsafe.Pointer(&v))
    return _sample_max_sse4_2(x.addr, x.len)
}

func sample_min_sse4_2(v []int64) int64 {
    x := (*slice)(unsafe.Pointer(&v))
    return _sample_min_sse4_2(x.addr, x.len)
}

有了这些函数声明,我们就可以使用c2goasm进行转换了:

c2goasm -a -f lib/sample_sse4.s sample_sse4_amd64.s
c2goasm -a -f lib/sample_avx.s sample_avx_amd64.s
c2goasm -a -f lib/sample_avx2.s sample_avx2_amd64.s

然后我们添加一段初始化逻辑,根据CPU支持的指令集来选择使用对应的实现:

import (
	"math"
	"unsafe"

	"github.com/intel-go/cpuid"
)

var (
	// SampleSum returns the sum of the slice of int64.
	SampleSum func(values []int64) int64
	// SampleMax returns the maximum value of the slice of int64.
	SampleMax func(values []int64) int64
	// SampleMin returns the minimum value of the slice of int64.
	SampleMin func(values []int64) int64
)

func init() {
	switch {
	case cpuid.EnabledAVX && cpuid.HasExtendedFeature(cpuid.AVX2):
		SampleSum = sample_sum_avx2
		SampleMax = sample_max_avx2
		SampleMin = sample_min_avx2
	case cpuid.EnabledAVX && cpuid.HasFeature(cpuid.AVX):
		SampleSum = sample_sum_avx
		SampleMax = sample_max_avx
		SampleMin = sample_min_avx
	case cpuid.HasFeature(cpuid.SSE4_2):
		SampleSum = sample_sum_sse4_2
		SampleMax = sample_max_sse4_2
		SampleMin = sample_min_sse4_2
  default:
    // 纯go实现
		SampleSum = sampleSum
		SampleMax = sampleMax
		SampleMin = sampleMin
	}
}

此时我们的工作就完成了,我们可以使用go test的benchmark来进行比较,看看跟之前的纯go实现,性能提升了多少:

name         old time/op  new time/op  delta
SampleSum-4   519ns ± 1%    53ns ± 2%  -89.72%  (p=0.000 n=10+9)
SampleMax-4   676ns ± 2%   183ns ± 2%  -73.00%  (p=0.000 n=10+10)
SampleMin-4   627ns ± 1%   180ns ± 1%  -71.27%  (p=0.000 n=10+9)

我们可以看出,sum方法得到10倍的提升,max/min得到了3倍多的提升,可能是因为max/min方法中每次循环中都有一次分支判断的原因,导致其提升效果不如sum方法那么多。

完整的实现在lrita/c2goasm-example

RDTSC精确计时

在x86架构的CPU上,每个CPU上有一个单调递增的时间戳寄存器,可以帮助我们精确计算每一段逻辑的精确耗时,其调用代价和计时精度远远优于time.Now(),在runtime中有着广泛应用,可以参考runtime·cputicks的实现。在但是对于指令比较复杂的函数逻辑并不适用于此方法,因为该寄存器时与CPU核心绑定,每个CPU核心上的寄存器可能并不一致,如果被测量的函数比较长,在运行过程中很可能发生CPU核心/线程的调度,使该函数在执行的过程中被调度到不同的CPU核心上,这样测量前后取到的时间戳不是来自于同一个寄存器,从而造成比较大的误差。

还要注意的是RDTSC并不与其他指令串行,为了保证计时的准确性,需要在调用RDTSC前增加对应的内存屏障,保证其准确性

参考

标签:FP,汇编,函数,SP,golang,go,AX,MOVQ
来源: https://www.cnblogs.com/landv/p/11589074.html