【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

改变一个字符后,我的 Go 程序快了 42%

  • 2023-02-14
    北京
  • 本文字数:2825 字

    阅读完需:约 9 分钟

改变一个字符后,我的 Go 程序快了 42%

我要先声明通常的基准测试警告:42% 的加速是在我的计算机上运行程序的数据时测量的,所以大家对这个数字请持保留态度。

这个 Go 程序是做什么的?


codeowners 是一个 Go 程序,它根据 GitHub CODEOWNERS 文件中定义的一系列规则,打印出存储库中每个文件的所有者。可能有这样的规则:所有以 .go 结尾的文件都归 @gophers 团队所有,或者 docs/ 目录下的所有文件都归 @docs 团队所有。


当考虑一个给定的路径时,最后匹配的规则获胜。一个简单但天真的匹配算法会向后迭代每条路径的规则,当找到匹配时就会停止。智能算法确实存在,但那是以后的事了。Ruleset.Match 函数如下所示:


type Ruleset []Rule

func (r Ruleset) Match(path string) (*Rule, error) { for i := len(r) - 1; i >= 0; i-- { rule := r[i] match, err := rule.Match(path) if match || err != nil { return &rule, err } } return nil, nil}
复制代码


用 pprof 和 flamegraph 查找“slow bits”


当在一个中等规模的存储库中运行该工具时,它的运行速度有点慢:

$ hyperfine codeownersBenchmark 1: codeowners  Time (mean ± σ):      4.221 s ±  0.089 s    [User: 5.862 s, System: 0.248 s]  Range (min … max):    4.141 s …  4.358 s    10 runs
复制代码


为了了解程序将时间花在哪里,我用 pprof 记录了一个 CPU 配置文件。你可以通过将以下代码片段添加到 main 函数的顶部来获得生成的 CPU 配置文件:

 

pprofFile, pprofErr := os.Create("cpu.pprof")if pprofErr != nil {  log.Fatal(pprofErr)}pprof.StartCPUProfile(pprofFile)defer pprof.StopCPUProfile()
复制代码


题外话:我经常使用 pprof,所以我将这段代码保存为 vscode 代码片段。我只要输入 pprof,点击 tab,就会出现这个代码片段。


Go 附带了一个方便的交互式配置文件可视化工具。通过运行以下命令,然后导航到页面顶部菜单中的火焰图视图,将配置文件可视化为火焰图。


$ go tool pprof -http=":8000" ./codeowners ./cpu.pprof
复制代码


正如我所料,大部分时间都花在了 Match 函数上。CODEOWNERS 模式被编译成正则表达式,Match 函数的大部分时间都花在 Go 的正则表达式引擎中。但是我也注意到,很多时间都花在了分配和回收内存上。下面的火焰图中的紫色块与模式 gc|malloc 相匹配,你可以看到它们在总体上表示程序执行时间的一个有意义的部分。



使用逃逸分析跟踪搜寻堆分配


因此,让我们看看是否有任何分配可以消除,以减少 GC 的压力和在 malloc 花费的时间。


Go 编译器使用一种叫做逃逸分析(escape analysis)的技术来计算出何时需要在堆上驻留一些内存。假设一个函数初始化了一个结构,然后返回一个指向它的指针。如果该结构是在堆栈中分配的,那么一旦函数返回,并且相应的堆栈框架失效,返回的指针就会失效。在这种情况下,Go 编译器将确定该指针已经“逃离”了函数,并将该结构移至堆中。


你可以通过向 go build 传递 -gcflags=-m 来看到这些决定:


$ go build -gcflags=-m *.go 2>&1 | grep codeowners.go./codeowners.go:82:18: inlining call to os.IsNotExist./codeowners.go:71:28: inlining call to filepath.Join./codeowners.go:52:19: inlining call to os.Open./codeowners.go:131:6: can inline Rule.Match./codeowners.go:108:27: inlining call to Rule.Match./codeowners.go:126:6: can inline Rule.RawPattern./codeowners.go:155:6: can inline Owner.String./codeowners.go:92:29: ... argument does not escape./codeowners.go:96:33: string(output) escapes to heap./codeowners.go:80:17: leaking param: path./codeowners.go:70:31: []string{...} does not escape./codeowners.go:71:28: ... argument does not escape./codeowners.go:51:15: leaking param: path./codeowners.go:105:7: leaking param content: r./codeowners.go:105:24: leaking param: path./codeowners.go:107:3: moved to heap: rule./codeowners.go:126:7: leaking param: r to result ~r0 level=0./codeowners.go:131:7: leaking param: r./codeowners.go:131:21: leaking param: path./codeowners.go:155:7: leaking param: o to result ~r0 level=0./codeowners.go:159:13: "@" + o.Value escapes to heap
复制代码


输出有点嘈杂,但你可以忽略其中的大部分。由于我们正在寻找分配,“move to heap”是我们应该关注的短语。回顾上面的 Match 代码,Rule 结构被存储在 Ruleset 片中,我们可以确信它已经在堆中了。由于返回的是一个指向规则的指针,所以不需要额外的分配。


然后我看到了--通过分配 rule := r[i],我们将堆中分配的 Rule 从片中复制到堆栈中,然后通过返回 &rule,我们创建了一个指向结构副本的(逃逸)指针。幸运的是,解决这个问题很容易。我们只需要将 & 号往上移一点,这样我们就引用了片中的结构,而不是复制它:


 func (r Ruleset) Match(path string) (*Rule, error) { 	for i := len(r) - 1; i >= 0; i-- {-		rule := r[i]+		rule := &r[i] 		match, err := rule.Match(path) 		if match || err != nil {-			return &rule, err+			return rule, err 		} 	} 	return nil, nil }
复制代码


我确实考虑过另外两种方法:


  1. 将 Ruleset 从 []Rule 更改为 []*Rule,这意味着我们不再需要显式引用该规则。

  2. 返回 Rule 而不是 *Rule。这仍然会复制 Rule,但它应该保留在堆栈上,而不是移动到堆上。


然而,由于此方法是公共 API 的一部分,这两种方法都会导致一个重大的变化。


无论如何,在做了这个修改之后,我们可以通过从编译器获得一个新的跟踪并将其与旧的跟踪进行比较来看看它是否达到了预期的效果:


$ diff trace-a trace-b14a15> ./codeowners.go:105:7: leaking param: r to result ~r0 level=016d16< ./codeowners.go:107:3: moved to heap: rule
复制代码


成功了!分配消失了。现在让我们看看删除一个堆分配会如何影响性能:


$ hyperfine ./codeowners-a ./codeowners-bBenchmark 1: ./codeowners-a  Time (mean ± σ):      4.146 s ±  0.003 s    [User: 5.809 s, System: 0.249 s]  Range (min … max):    4.139 s …  4.149 s    10 runs

Benchmark 2: ./codeowners-b Time (mean ± σ): 2.435 s ± 0.029 s [User: 2.424 s, System: 0.026 s] Range (min … max): 2.413 s … 2.516 s 10 runs

Summary ./codeowners-b ran 1.70 ± 0.02 times faster than ./codeowners-a
复制代码


由于该分配是针对匹配的每一条路径进行的,因此在这种情况下,删除它会获得 1.7 倍的速度提升(这意味着它的运行速度要快 42%)。对一个字符的变化来说还不错。


作者简介:

Harry,在 GitHub 工作,负责领导团队为开发人员开发安全产品。Dependabot 共同创始人之一。

 

原文链接:

https://hmarr.com/blog/go-allocation-hunting/#circle=on


推荐阅读:

一个架构师在 2023 年需要掌握哪些“必杀技”?

2023-02-14 11:329962

评论

发布
暂无评论
发现更多内容

等保二级需要测评吗?多久测评一次?

行云管家

网络安全 堡垒机 等保测评 过等保 二级等保

走进吉利|EMQ 映云科技助力车企“新四化”

EMQ映云科技

车联网 物联网 IoT emq iov

使用 grpcurl 通过命令行访问 gRPC 服务

AlwaysBeta

golang gRPC

艾瑞发布《2021年中国企业级 SaaS 行业研究报告》,ONES 入选典型厂商案例

万事ONES

项目管理 SaaS 协同办公

图谱相关技术在风控反作弊中的应用和探索

百度Geek说

后端 软件架构

掘金热榜第一!阿里P8用近十个月整理出来999页Java岗核心笔记限时开源!

Java 架构 面试 程序人生 编程语言

关于Promise你需要知道的一切

devpoint

大前端 Promise 引航计划

从产业链、架构和技术三个层面,看元宇宙与RPA的发展关系

王吉伟频道

RPA 自动化 区块链+ 机器人流程自动化 元宇宙

恒源云(GpuShare)_快进来!这里有超高性价比的GPU资源

恒源云

深度学习

一周信创舆情观察(9.13~9.26)

统小信uos

东软熙康、百度、京东方,互联网医院的三个典型样本

海比研究院

四面字节跳动(高级开发岗):分布式+中间件+TCP+JVM+Hashmap

Java 编程 架构 面试 计算机

华为云开源的Karmada正式成为CNCF首个多云容器编排项目

华为云开发者联盟

容器 集群 cncf Karmada

国家级|深信服再获CNCERT应急服务国家级支撑单位称号!

科技热闻

快准狠!百万年薪的阿里架构师分享深入剖析Java虚拟机文档

Java 架构 面试 程序人生 编程语言

华为云带你探秘Xtrabackup备份原理和常见问题分析

华为云数据库小助手

GaussDB 华为云数据库 MySQL 数据库 GaussDB(for MySQL)

大厂敲门砖!Alibaba技术官甩出的Springboot笔记

Java 架构 面试 程序人生 编程语言

博睿数据亮相北京IMC2021第三届中国智造CIO峰会

博睿数据

如何构建混合云管理平台?——资源管理篇

金蝶天燕云

云计算 云平台

国庆出游神器:魔幻黑科技换天造物,让vlog秒变科幻大片!

华为云开发者联盟

视频 modelarts 科幻 国庆 天空

金秋10月,日更挑战你准备好了么!?

InfoQ写作社区官方

热门活动 10月月更

第 8 章 -《Linux 一学就会》-Centos8 软件包的管理与安装

学神来啦

Linux 运维

阿里巴巴架构师十年整理出的JavaSpringBoot核心文档,真是太全了

Java 架构 面试 程序人生 编程语言

从工具、工具箱到数字化软件工厂——DevOps 设计理念与工程实践专场 | CIF 精彩看点

CODING DevOps

DevOps 数字化 研发工具 腾讯云 CIF 峰会

堡垒机和虚拟机的两大区别分析-行云管家

行云管家

网络安全 虚拟机 服务器 堡垒机

不得不聊一聊英语爱好者的单词杂货铺了

小匚

随笔杂谈 生活记录 英语 大学英语四六级 单词

智能大数据专场,百度智能云带来智能大数据产品架构全景图

百度大脑

人工智能 大数据

搞不懂云原生

二哥不再迷茫

恒拓高科亮相中国-东北亚博览会,赋能海内外企业构建数字化新生态

WorkPlus

阅读

Python代码阅读(第31篇):将一个列表分割成指定大小的小列表

Felix

Python 编程 Code Programing 阅读代码

目睹阿里技术官写的Tomcat架构笔记后,瞬间觉得自己是渣渣

Java 架构 面试 程序人生 编程语言

改变一个字符后,我的 Go 程序快了 42%_文化 & 方法_Harry_InfoQ精选文章