首页 > 系统相关 >【深度解析】'go build'缓存机制:揭秘Windows下缓慢的原因

【深度解析】'go build'缓存机制:揭秘Windows下缓慢的原因

时间:2024-03-08 21:13:01浏览次数:33  
标签:缓存 hash Windows fyne buildID build go

引言

本文主要围绕 go build 的缓存hash计算与获取缓存文件来编写。

  笔者是Windows系统用户,在 go buildgo list -export一些需要编译(但已存在编译缓存)场景下执行的很慢。网上有很多说法大多都是说关闭杀毒软件、关闭磁盘扫描等,并未清楚的描述为什么。
  接下来我将围绕go build这个命令阅读了go build的相关代码与文档,从中得知缓存是如何做到的,又是如何命中缓存的。

缓存机制的基本原理

构建缓存,是指对go build编译的内容进行缓存,它会缓存到GOCACHE env中,它的缓存目录如下:

|go-build
|--[hash[:2]]  (文件哈希值前2位,减少太碎片化)
|----[hash]-d  (compile file: package or bin)
|----[hash]-a  (action)
  1. 以上文件,在编译包后会将内容从 builder workDir 复制到go-build(GOCACHE)目录。
  2. 这些个缓存文件中也写入了在构建时生成的buildID,而buildID,它是根据actionID和contentID组合得来的。<actionId>/<contentId>、<actionID>/<dep[0].buildID>/<contentID>

如何生成hash的?

我们要构建go项目前,是需要得到所有的依赖的包(ImportPath),最终针对每个包进行编译。

在生成hash前,我们先了解下面两点

生成操作图:操作图是指对每个包的操作,如何操作、该操作依赖了什么等,是对操作的描述。在最后执行这些操作图时,会以去找到该这些操作图中没有依赖项的先执行。

编译包:然后根据操作图执行。我们都知道包之间是有依赖关系的,我们需要提前知道谁依赖谁,最终对它们生成一个排序号的编译顺序的项,让构建时保证了包编译时所依赖的包是已编译的。

而缓存则就是在编译前通过操作图计算出hash去寻找缓存,如果没有缓存则才去编译,编译完后会根据当前操作图的hash去写出缓存到go-build(其实他在编译时会存放到当前builder.workDir,最后移动到GOCACHE目录。

缓存包: go/internal/cache

该包内是针对缓存文件的读取、hash的生成、以及对缓存文件的修改时间等操作。

  1. 通过写入byte内容,然后计算该内容得到哈希。
  2. 通过指定文件,读取文件内容来计算得到哈希。
  3. 读取指定hash的缓存内容,如读缓存的包、读缓存的gofiles等。

获取操作hash: (Builder).builderActionID(action)

获得当前操作的哈希(此哈希可去找到缓存的包)

操作id: buildAction

阅读该源码可:view codes

使用cache.NewHas 来创建一个hash,例如当前是计算buildAction则:cache.NewHash("build " + p.ImportPath) ,然后再根据当前操作图的内容和go版本等信息向当前的hash流里写入内容,如:fmt.Fprintf(h, "compile\n")

根据编译动作的内容生成hash: 它是通过不同的操作、go版本、系统版本和命令的自身需要的数据而组合计算得出的哈希则是它的操作ID。

for example: 构建包: build fyne.io/fyne/v2/container

HASH NAME: build fyne.io/fyne/v2/container
HASH Body:
"go1.21.5"
"compile\n"
"dir /home/liuscraft/go/pkg/mod/fyne.io/fyne/[email protected]/container\n"
"go 1.17\n"
"goos linux goarch amd64\n"
"import \"fyne.io/fyne/v2/container\"\n"
"omitdebug false standard false local false prefix \"\"\n"
"compile compile version go1.21.5 [] []\n"
"GOAMD64=v1\n"
"file apptabs.go 0-0RazvPT20JpC1aFUKV\n"
"file container.go Utx1rP_vPvYe_OUB19Lf\n"
"file doctabs.go cYVyCdCSNxjkDqVHsU6U\n"
"file layouts.go wCNQmhu3VqxjUX7yb4Cx\n"
"file scroll.go j_xTEDYDq-fgmgyj78Y-\n"
"file split.go a6dGVhpLhUcb70jDbV57\n"
"file tabs.go Cipr6syoIG1ar0a9Ov2m\n"
"import fyne.io/fyne/v2 eASK30olqGqtSeoxyrQ1\n"
"import fyne.io/fyne/v2/canvas G4FlPwPnYXXw7dxFB0hj\n"
"import fyne.io/fyne/v2/driver/desktop Hw0IFMuFUgmW1JYrCVxz\n"
"import fyne.io/fyne/v2/internal xUhHtYQsqkgm0m6rMPSd\n"
"import fyne.io/fyne/v2/internal/widget MSnJkoFbIzwRUaqJ6OHT\n"
"import fyne.io/fyne/v2/layout LkN4E60WNeLZkNtASg0j\n"
"import fyne.io/fyne/v2/theme iY_BtWGjYnHtGt7hxWkM\n"
"import fyne.io/fyne/v2/widget oXRt7xbBrqP4DYzCAZ_r\n"
"import image/color 5KY_2W7V9mJ4XIGT_c6u\n"
"import sync oHEoZ3U7NHMHs26WzTVq\n"
SUM:41cde5b3a2504ac5d3bd960869a40a0c96fde013ee77e88eb882276b9501b3c4

上文中,这些import开头Utx1rP_vPvYe_OUB19Lf类似的文本,他是当前编译包的所依赖的操作id,也就是说,上文中的这些id都是通过相同方式生成的。
file部分则是通过使用cache.FileHash计算得来的,它计算的是文件内容。

从这里我们就可以看出,这个生成id的操作相当的复杂,大量的IO最终导致在磁盘性能底、文件系统不行的状况下,会导致一小段的耗时。

最终拿着上文中的内容,再利用cache.Sum()来计算这个内容的hash值,来代表当前buildAction的hash(也就是操作id)

获取操作: linkActionID

link操作的id,也是差不多的操作,细节可自行阅读源码:
func (b *Builder) linkActionID(a *Action) cache.ActionID: view code

contentId如何得到

这个目前只看到直接等值为 actionId,说是零时占位。

buildID如何获得

buildid:可阅读源码 buildid.go

builid组成: <actionID>/<contentId><actionId>/<deps[0].buildId>/<contentID>
注意: <deps[0].buildId>

  • 其实就是依赖的操作图id,例如我link操作依赖一个包的编译操作,那么这里就是这个包的buildid,他展开其实最终结果是: <actionID>/<actionID>/<contentId>/<contentId>

如果我们知道buildId,那我们肯定知道ActionID和contentID,他其实就是buildid的前半段或后半段。

// actionID returns the action ID half of a build ID.
func actionID(buildID string) string {
	i := strings.Index(buildID, buildIDSeparator)
	if i < 0 {
		return buildID
	}
	return buildID[:i]
}

// contentID returns the content ID half of a build ID.
func contentID(buildID string) string {
	return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:]
}

buildIDSeparator: 他就是buildId中的 / 符号

使用action ID寻找缓存

存在的target(已编译的包、或二进制)

这个的前提也是需要计算hash的
阅读: 读文件的buildID的实现: view codes
这个target,是pkg、bin这些里的标准库等内容(在加载importpath生成操作图的时候就已经得到了)

这里是指编译的包和go的二进制文件中其实是存储了当前文件的buildid的.

// compile
buildID, _ := buildid.ReadFile(target)
if strings.HasPrefix(buildID, actionID+buildIDSeparator) {
	a.buildID = buildID
	if a.json != nil {
		a.json.BuildID = a.buildID
	}
	a.built = target
	// Poison a.Target to catch uses later in the build.
	a.Target = "DO NOT USE - " + a.Mode
	return true
}

// link(当前编译的包是被父级link的,那可以看看这个是不是被父级link的,是的话也不需要编译)
if !b.NeedExport && a.Mode == "build" && len(a.triggers) == 1 && a.triggers[0].Mode == "link" {
	if id := strings.Split(buildID, buildIDSeparator); len(id) == 4 && id[1] == actionID {
		oldBuildID := a.buildID
		a.buildID = id[1] + buildIDSeparator + id[2]
		linkID := buildid.HashToString(b.linkActionID(a.triggers[0]))
		if id[0] == linkID {
			// Best effort attempt to display output from the compile and link steps.
			// If it doesn't work, it doesn't work: reusing the cached binary is more
			// important than reprinting diagnostic information.
			if printOutput {
				showStdout(b, c, a, "stdout")      // compile output
				showStdout(b, c, a, "link-stdout") // link output
			}

			// Poison a.Target to catch uses later in the build.
			a.Target = "DO NOT USE - main build pseudo-cache Target"
			a.built = "DO NOT USE - main build pseudo-cache built"
			if a.json != nil {
				a.json.BuildID = a.buildID
			}
			return true
		}
		// Otherwise restore old build ID for main build.
		a.buildID = oldBuildID
	}
}

存在的操作输出缓存(读GOCACHE中的-d, output文件)

这个的前提也是需要计算hash的
阅读: 查看缓存文件是否存在,存在则需要读该输出文件的buildID(与上面同样)

这个其实也是编译后的包或者二进制文件,没啥区别,只不过这个是第三方包的缓存在GOCACHE

// Check to see if the action output is cached.
if file, _, err := cache.GetFile(c, actionHash); err == nil {
	if buildID, err := buildid.ReadFile(file); err == nil {
		if printOutput {
			showStdout(b, c, a, "stdout")
		}
		a.built = file
		a.Target = "DO NOT USE - using cache"
		a.buildID = buildID
		if a.json != nil {
			a.json.BuildID = a.buildID
		}
		if p := a.Package; p != nil && target != "" {
			p.Stale = true
			// Clearer than explaining that something else is stale.
			p.StaleReason = "not installed but available in build cache"
		}
		return true
	}
}

补充 tool buildId工具

通过go tool buildid [-w] <file> 来得到一个文件的buildid,然后跟当前操作哈希进行判断是否一直,如果一直则直接使用中国编译的包或者二进制文件。

Windows下的性能问题

通过hash的生成的过程,其实就可以得知Windows慢的原因了:大量的IO操作,文件读写。

  1. 磁盘速度
  2. Windows文件系统,它会比macos和Linux慢
  3. 杀毒软件干扰
  4. 磁盘扫描软件干扰

这就是为什么,网上只要有人提起 go build慢,就会有人回答关闭杀软、关闭磁盘扫描等。

测试windows/linux文件递归速度

测试代码:

package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "time"
)

func recursiveFiles(directory string) error {
    err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.Mode().IsRegular() {
            // fmt.Println(path)
        }
        return nil
    })
    return err
}

func main() {
    start := time.Now()
    directory := `wokdir`
    err := recursiveFiles(directory)
    if err != nil {
        fmt.Println(err)
    }
    log.Printf("%.2f", time.Since(start).Seconds())
}

结果(使用了大量的文件、文件夹):
linux: 0.06s
windows: 2.51s

结论

  1. 构建缓存的文件是存储在GOCACHE ENV的目录下
  2. 缓存哈希是通过actionID来进行的,缓存单位是包(编译单位也是包)
  3. 缓存在GOCACHE中时,它其实是被2个字符名称的文件夹所包裹的,其实就是这些缓存文件名的前两位值(这里是防止大量缓存文件都在一个文件夹下,而是让他们分散到不同的文件夹)
  4. 计算缓存hash它存在大量的IO操作,依赖越多,那么IO操作就越多。
  5. 无论是否标准库还是第三方库,都会计算hash。

参考资料

[1]. pkgsite: https://pkg.go.dev/cmd/go/internal/work
[2]. go sources(1.22.1): https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/cmd/go/internal/work

标签:缓存,hash,Windows,fyne,buildID,build,go
From: https://www.cnblogs.com/liuscraft/p/18061863

相关文章

  • windows安装RocketMQ
    一、RocketMQ介绍1.开发指南:Gitee中文学习地址(https://www.processon.com/view/link/620c69d95653bb4ec5bb75cd#map)二、RocketMQ下载官方下载地址::https://rocketmq.apache.org/zh/download三、安装部署过程(带!为非必要操作)1.Java环境classpath.;%JAVA_HOME%\lib\dt.ja......
  • 如何在c#中禁用Windows键
    usingSystem;usingSystem.Runtime.InteropServices;usingSystem.Windows.Forms;publicclassKeyboardHook:IDisposable{privateconstintWM_KEYDOWN=0x0100;privateconstintWM_KEYUP=0x0101;privateconstintWM_SYSKEYDOWN=0x0104;......
  • 完全颠覆Windows使用体验!微软将在今年发布“AI Explorer”
    据WindowsCentral报道,微软将在今年晚些时候在Windows11上推出一系列AI功能,其中就包括被内部称为“AIExplorer”的新功能。据消息人士透露,“AIExplorer” 被微软描述为“高级Copilot”,是将AIPC与非AIPC区分开来的重磅AI体验。其内置的历史记录/时间线功能可以在所有应用中......
  • golang将时间转为时间戳碰到的问题
    golang将字符串"2024-03-0716:00:00"转为时间戳代码如下:packagemainimport("fmt""time")funcmain(){//定义时间格式,与字符串中的时间格式匹配constlayout="2006-01-0215:04:05"//要转换的时间字符......
  • gorm 中left join的使用
    使用mysql语句执行时可以执行成功,但是使用go语言编程保存到struct中时出现问题。代码如下:sflog.Debug("QueryByTaskId",id)  typeDatastruct{    TaskId     int64 `json:"taskId"`    VehicleName  string `json:"vehicleNa......
  • windows系统关闭指定端口
    前言有时候启动程序时会提示Addressalreadyinuse,意思就是启动项目的端口已经被别的程序占用。此时可以查询是哪个程序占了该端口,并且把占用端口的进程kill掉。一、windows系统关闭指定端口1、打开cmd输入netstat-ano|findstrxxx查看占用该端口的进程PID,其中xxx为具体的端......
  • golang进阶之反射
    目录一、go中变量的内在机制二、反射1.反射是把双刃剑2.反射的简介三、reflect库1.reflect.TypeOf(1)reflect.Type的name和kind(2)kind的能返回的类型如下2.reflect.ValueOf(1)反射取值(2)反射改值3.isNil()和isValid()四、结构体的反射1.StructField类型2.结构体反射示例(1......
  • windows 10/11 下安装 ssh 服务
    https://zhuanlan.zhihu.com/p/634969945 windows一般自带ssh服务,只是需要去把服务开启下:检查OpenSSH的可用性以管理员身份打开PowerShell并运行:Get-WindowsCapability-Online|Where-ObjectName-like'OpenSSH*'命令返回Name:OpenSSH.Client~~~~0.0.1......
  • Vulnhub内网渗透Jangow01靶场通关
    详细请见个人博客靶场下载地址。下载下来后是.vmdk格式,vm直接导入。M1请使用UTM进行搭建,教程见此。该靶场可能出现网络问题,解决方案见此信息搜集arp-scan-l #主机发现ip为192.168.168.15nmap-sV-A-p-192.168.168.15 #端口扫描发现开放了21ftp和80http端口,对......
  • JavaScript 打包器esbuild的基础使用
    esbuild是一种类似于webpack的极速JavaScript打包器。esbuild项目主要目标是:开辟一个构建工具性能的新时代,创建一个易用的现代打包器。先安装esbuildnpmiesbuild-g-g代表全局范围检查esbuild的版本esbuild--version命令行构建esbuildsrc\app.jsx--bundle--outfi......