Go 1.5 内存清除汇编源码注释

Mon 11 February 2019

最近在阅读 Go1.5 的源码,发现源码中多处调用了了 memclr ,进一步深入了解发现原来 Go 为了实现内存复用会将已经回收到 cache 的内存清除,清除逻辑使用汇编实现,本文是基于自己的理解进行的注释。

注意:

  1. 最新版的 Go 中(1.11)该函数已经改名为 memclrNoHeapPointers
  2. 我自己添加的注释以 //# 开始
  3. Go 针对不同的平台实现了不同的内存清除,这里仅对 amd64 平台下的实现进行注释:

源文件: https://github.com/golang/go/blob/release-branch.go1.5/src/runtime/memclr_amd64.s

// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !plan9

#include "textflag.h"

// NOTE: Windows externalthreadhandler expects memclr to preserve DX.

// void runtime·memclr(void* ptr, uintptr n)

//# - TEXT 在 Plan9 中用于声明函数
//# - runtime·memclr 为函数名,中间的·不是普通的点在 Mac 下通过 Option+Shift+9 打出,· 前是包名后面是函数名
//# - SB(伪寄存器)全局静态基指针,用来声明函数或全局变量(此处用来声明函数)
//# - NOSPLIT 定义在 https://github.com/golang/go/blob/master/src/runtime/textflag.h
//# - $0-16:
//#   + $0 栈帧大小为0(局部变量+可能需要的额外调用函数的参数空间总大小,不包括调用其他函数时的 ret address 的大小)
//#   + 16 参数基返回值的大小(16 表示两个双四字的参数)
TEXT runtime·memclr(SB), NOSPLIT, $0-16
     //# FP(伪寄存器):通过 `symbol+offset(FP)` 的方式引用输入参数
     //# symbol 没有任何用,只是增加可读性,但不能省略
     //# FP 指向整个栈帧的底部的 BP 寄存器
     MOVQ    ptr+0(FP), DI   //# 第一个参数移动到 DI 寄存器(DI 目标索引寄存器)
     MOVQ    n+8(FP), BX     //# 第二个参数移动到 BX 寄存器(BX 为基址寄存器,用于内存寻址)
     XORQ    AX, AX          //# 清零 AX 寄存器(AX 为累加寄存器)

// MOVOU seems always faster than REP STOSQ.
tail:
     TESTQ   BX, BX          // set ZF to 1 if n is 0
     JEQ     _0              // jump to _0 if ZF == 1(returns)
     CMPQ    BX, $2
     JBE     _1or2           // jump to _1or2 if n <= 2
     CMPQ    BX, $4
     JBE     _3or4           // jump to _3or4 if n > 2 and n <= 4
     CMPQ    BX, $8
     // ...
     JBE     _5through8
     CMPQ    BX, $16
     JBE     _9through16

     //# 大于 16 开始使用 128 位寄存器 X0,
     //# PXOR 将 X0 寄存器置为 0
     PXOR    X0, X0
     CMPQ    BX, $32
     JBE     _17through32
     CMPQ    BX, $64
     JBE     _33through64
     CMPQ    BX, $128
     JBE     _65through128
     CMPQ    BX, $256
     JBE     _129through256
     // TODO: use branch table and BSR to make this just a single dispatch
     // TODO: for really big clears, use MOVNTDQ.

//# 大于 256 则通过循环
loop:
     //# MOVOU 相当于 AT&T/Intel 的 MOVDQU -- 移动非对齐的双四字
     //# X0 相当与 AT&T/Intel 的 SSE 新增的 %xmm0(128位元暂存器)
     //# 参见 https://zh.wikipedia.org/wiki/SSE
     MOVOU   X0, 0(DI)
     MOVOU   X0, 16(DI)
     MOVOU   X0, 32(DI)
     MOVOU   X0, 48(DI)
     MOVOU   X0, 64(DI)
     MOVOU   X0, 80(DI)
     MOVOU   X0, 96(DI)
     MOVOU   X0, 112(DI)
     MOVOU   X0, 128(DI)
     MOVOU   X0, 144(DI)
     MOVOU   X0, 160(DI)
     MOVOU   X0, 176(DI)
     MOVOU   X0, 192(DI)
     MOVOU   X0, 208(DI)
     MOVOU   X0, 224(DI)
     MOVOU   X0, 240(DI)
     SUBQ    $256, BX  //# 递减 BX
     ADDQ    $256, DI  //# 递增 DI
     CMPQ    BX, $256
     JAE     loop      //# 如果 BX 依然大于 256 则继续循环
     JMP     tail      //# 否则进入 tail

_1or2:
     MOVB    AX, (DI)
     MOVB    AX, -1(DI)(BX*1)
     RET
_0:
     RET
_3or4:
     MOVW    AX, (DI)
     MOVW    AX, -2(DI)(BX*1)
     RET
_5through8:
     MOVL    AX, (DI)
     MOVL    AX, -4(DI)(BX*1)
     RET
_9through16:
     MOVQ    AX, (DI)
     MOVQ    AX, -8(DI)(BX*1)
     RET
_17through32:
     MOVOU   X0, (DI)
     MOVOU   X0, -16(DI)(BX*1)
     RET
_33through64:
     MOVOU   X0, (DI)
     MOVOU   X0, 16(DI)
     MOVOU   X0, -32(DI)(BX*1)
     MOVOU   X0, -16(DI)(BX*1)
     RET
_65through128:
     MOVOU   X0, (DI)
     MOVOU   X0, 16(DI)
     MOVOU   X0, 32(DI)
     MOVOU   X0, 48(DI)
     MOVOU   X0, -64(DI)(BX*1)
     MOVOU   X0, -48(DI)(BX*1)
     MOVOU   X0, -32(DI)(BX*1)
     MOVOU   X0, -16(DI)(BX*1)
     RET
_129through256:
     MOVOU   X0, (DI)
     MOVOU   X0, 16(DI)
     MOVOU   X0, 32(DI)
     MOVOU   X0, 48(DI)
     MOVOU   X0, 64(DI)
     MOVOU   X0, 80(DI)
     MOVOU   X0, 96(DI)
     MOVOU   X0, 112(DI)
     MOVOU   X0, -128(DI)(BX*1)
     MOVOU   X0, -112(DI)(BX*1)
     MOVOU   X0, -96(DI)(BX*1)
     MOVOU   X0, -80(DI)(BX*1)
     MOVOU   X0, -64(DI)(BX*1)
     MOVOU   X0, -48(DI)(BX*1)
     MOVOU   X0, -32(DI)(BX*1)
     MOVOU   X0, -16(DI)(BX*1)
     RET

在此期间参阅了大量的资料,最大的坑就是 Go 使用的汇编是 Plan9而非常见的 x86 汇编,参考资料如下:

Category: Go Tagged: golang go 源码 plan9 汇编 内存清除 memclr

Comments

Fork me on GitHub