Go 逃逸分析的缺陷

首发于:https://studygolang.com/articles/12396

先阅读这个由四部分组成的系列文章,对理解逃逸分析和数据语义会有帮助。下面详细介绍了阅读逃逸分析报告和 输出的方法。(GCTT 已经在翻译中)

参考处:https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html

介绍

即使使用了 4 年,我仍然会被这门语言震惊到。多亏了编译器执行的静态代码分析,编译器可以对其生成的代码进行一些有趣的优化。编码器执行的其中一种分析称为逃逸分析。这会对内存管理进行优化和简化。

在过去的两年中,(Go)语言团队一直致力于优化编译器生成的代码,以获得更好的性能,并且做了极其出色的工作。我相信,如果逃逸分析中的一些现有的缺陷得以解决,那么, 程序会得到更大的改进。早在 2015 年 2 月, 就撰写了这篇文章,概述了编译器已知的逃逸分析缺陷。

参考处:https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/edit

我很好奇,自这篇文章以来,当中有多少提及的缺陷被修复了,然后,我发现,迄今为止,只有少数一些缺陷得到了解决。也就是说,有五个特定的缺陷尚未被修复,而我很乐意看到在 不久的将来发布的版本中能有所改善。我将这些缺陷标记为:

间接赋值(Indirect Assignment)

间接调用(Indirect Call)

切片和 Map 赋值(Slice and Map Assignments)

接口(Interfaces)

未知缺陷(Unknown)

我认为,探索这些缺陷是很有趣的,所以,你可以看到它们被修复后对现有 Go 程序的积极影响。下面你所看到的所有东西都基于 1.9 编译器。

间接赋值

“间接赋值(Indirect Assignment)”缺陷与通过间接分配值时发生的分配有关。这是一个代码示例:

代码清单 1

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example1/example1_test.go

在代码清单 1 中,类型 拥有单个字段,这个字段的名字是 ,它是一个指向整型的指针。然后在第 11 行到第 13 行中,构造了一个类型为 的值,使用紧凑形式,用 变量的地址来初始化 字段。 变量是作为一个指针创建的,因此,这个变量与在第 17 行创建的变量是一样的。

在第 16 行中,声明了名为 的变量,然后在第 17 行中,构造了一个使用指针语义的类型为 的值,然后将其赋值给指针变量 。接着在第 18 行中, 变量的地址被赋给变量 执行的值中的 字段。在这个语句中,存在通过使用指针变量的赋值,这是一种间接赋值。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

在逃逸分析报告中, 逃逸给出的理由是,()。我想这是指编译器需要执行诸如以下的操作来完成此赋值。

输出清晰地显示, 是在堆上分配的,而 不是。我在 语言小萌新写的 代码中,大量看到 16 行到 18 行这样的代码。这个缺陷可以帮助更萌新的开发者从堆中移除一些垃圾。

间接调用

“间接调用()”缺陷与和通过间接调用的函数共享一个值时发生的分配有关。下面是一个代码示例:

代码清单 2.1

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example2/example2_test.go

在代码清单 2.1 中,在第 21 行声明了一个名为 的命名函数。这个函数接受一个整型的地址和一个整型值作为参数。然后,这个函数将传递的整型值赋值给 p 指针指向的位置。

在第 07 行,声明了一个类型为 ,名字为 的变量,这个变量在第 08 行对 的函数调用过程中发生了共享。从第 10 行到第 13 行,存在类似的情况。声明了一个类型为 的变量 ,然后这个变量作为第一个参数共享给一个在第 13 行声明和执行的字面函数。这个字面函数与 函数相同。

最后,在第 15 行到第 17 行之间,函数被赋给一个名为 的变量。通过变量 , 函数被执行,其中,变量 被共享。第 17 行的这个函数调用是通过 p 变量间接完成的。这与第 13 行的字面函数没有显式函数变量所执行的函数调用方式情况相同。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

在逃逸分析报告中,为变量 和 y3 变量的分配给出的原因是 (parameter to indirect call)。 输出很清楚的显示出, 和 被分配在堆上,而 不是。

虽然,我会认为在第 13 行调用的函数字面量的使用是代码异味,但是,第 16 行变量 的使用并不是。在 中,人们总是会传递函数作为参数。特别是在构建 服务的时候。修复这个间接调用缺陷会帮助减少 服务应用中的许多分配。

这里是一个你会在许多 服务应用中找到的例子。

代码清单 2.2

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example2/example2_http_test.go

在代码清单 2.2 中,第 26 行声明了一个通用的处理器封装函数,该函数在另一个字面函数的范围内封装了一个处理器函数,以提供样板代码。然后在第 11 行,声明了一个用于特定路由的处理函数,然后在第 15 行,它被传给 函数,以便可以与样板代码处理函数链接在一起。在第 19 行,创建了一个 值,然后与第 20 行的 调用共享。调用 在功能上同时执行了样板代码和特定的请求处理器。

第 20 行的 调用属于间接调用,因为 变量是一个函数变量。这会导致 http.Request 变量分配在堆上,这是没有必要的。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

在逃逸分析报告中,你可以看到这种分配的原因是 (parameter to indirect call)。pprof 报告显示, 变量正在分配。如前所述,这是人们在用 构建 服务时编写的常见代码。修复这个缺陷会减少程序中大量的分配。

切片和 Map 赋值

“切片和 赋值(Slice and Map Assignments)”缺陷与值在切片或者 中共享时发生的分配有关。这里是一个代码示例:

代码清单 3

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example3/example3_test.go

在代码清单 3 中,第 07 行创建了一个 ,它保存类型 的值的地址。然后在第 08 行,创建了一个类型 的值,接着在第 09 行,在 中共享了这个值,map 的键为 0。在第 11 行保存 地址的切片上也发生了同样的事情。在创建切片后,索引 0 内共享了类型为 的值。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

逃逸分析报告中给出的原因是 () 和 ()。更有趣的是,逃逸分析报告显示, 和切片数据结构不分配(不逃逸)。

不分配 Map 和切片

这进一步证明,代码示例中的 和 无需在堆上分配。

我一直认为,在合理和实际的情况下, 和切片中的数据应该作为值存储。特别是当这些数据结构正存储着一个请求或任务的核心数据的时候。这个缺陷为尝试避免通过使用指针来存储数据提供了另一个理由。修复这个缺陷可能几乎没有什么回报,因为静态大小的 和切片很少见。

接口

“接口()”缺陷与之前看到的“间接调用”缺陷有关。这是一个使用接口产生实际成本的缺陷。下面是一个代码示例:

代码清单 4

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example4/example4_test.go

在代码清单 4 中,在第 05 行声明了一个名为 的接口,并且为了示例目的,这个接口保持得非常简单。然后,在第 09 行声明了一个名为 X 的具体类型,并且,使用值接收器来实现 接口。

在第 17 行中,构建了一个类型为 的值,然后将其赋给 变量。在第 18 行, 变量的一个拷贝存储在 i1 接口变量中,接着,在第 19 行,相同的 变量与 接口变量共享。在第 21 和 22 行,同时对 和 接口变量调用 。

为了创建一个更实际的例子,在第 30 行声明了一个名为 的函数,它接受任何实现 接口的具体数据。然后,在第 31 行,对本地接口变量同样调用 。 函数代表了大家在 中写的大量函数。

在第 24 行,构造了一个类型为 名为 的变量,然后将其作为拷贝传递给 ,并分别在第 25 和 26 行中共享。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

注意,在基准报告中有四个分配。这是因为代码会复制 和 变量,这也会产生分配。在第 18 行中使用 变量进行赋值时,以及在第 25 行中对 进行函数调用使用 的值时,创建了这些副本。

在逃逸分析报告中,为 以及 的副本逃逸提供的原因是 ()。这很有趣,因为第 21 和 22 行对 的调用才是这个缺陷真正的罪魁祸首。请记住,针对接口的方法调用需要通过 进行间接调用。正如你之前看到的,间接调用是逃逸分析中的一个缺陷。

逃逸分析报告为 变量逃逸给出的原因是 (passed to call[argument escapes])。但是在这两种情况下,(interface-converted) 是另一个原因,它描述了数据存储在接口里的事实。

有趣的是,如果你移除第 31 行中 函数里的方法调用,那么,分配就会消失。实际上,第 21,22 和 中的 31 行中,通过接口变量对 的间接调用才是问题所在。

我总是在说,从 1.9 甚至更早的版本开始,使用接口会产生间接和分配的开销。这是逃逸分析的缺陷,如果修正这一缺陷,会给 程序带来最大的影响。这可以减少单独日志包的大量分配。不要使用接口,除非它们(指接口)提供的价值是显著的。

未知

这种类型的分配是某些我完全不明白的东东。即使在看了工具的输出,还是没搞明白。这里,我把它们供出,期望能得到一些答案。

下面是代码示例。

代码清单 5

https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example5/example5_test.go

在代码清单 5 中,第 10 行创建了一个类型为 的值,并将其设置为零值。然后,在第 11 行构造了一个切片值,并将其传递给 变量上的 方法调用。最后,为了防止潜在的编译器优化抛出所有的代码,调用 方法。该调用不是创造 变量逃逸的必要条件。

以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 命令的输出。

基准测试输出

逃逸分析报告

输出

在这个代码中,我没有看到第 11 行对 的方法调用引起逃逸的任何原因。我得到了一个看起来很有意思的指引,但我会留给你去进一步探索。

这可能与 类型的引导数组有关。它意味着一种优化,但是从逃逸分析的角度来说,它让 指向自身,这是一种循环依赖,通常难以分析。或者也许是因为 ,又或者也许只是几个因素和 中非常复杂的代码的组合。

有这个问题,它与导致这种分配的引导数组有关:

在 上发布了此回复:

情况下的逃逸是因为 认为给 的参数逃逸。如果你在 包的源代码上运行逃逸分析,那么它会输出(对于 ):

考虑到 是语言内置函数,似乎编译器应该知道这里,源参数不逃逸。或者有可能编译器在对 的实际实现做一些十分有趣的事情,以至于源在某些情况下会逃逸。

总结

我试图指出 1.9 版本至今存在的一些更有趣的逃逸分析缺陷。接口缺陷可能是一旦修复就会对当今的 程序产生最大影响的缺陷。我觉得最有意思的是,我们所有人都能从这些缺陷的修复中获益,而不需要有这方面的个人专长。编译器执行的静态代码分析提供了诸多好处,例如优化你随时写入的代码的能力。也许最大的好处是,消除或减少你不得不维持的认知负担。

via: https://www.ardanlabs.com/blog/2018/01/escape-analysis-flaws.html

作者:William Kennedy

译者:ictar

校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

喜欢本文的朋友们,欢迎长按下图关注订阅哦!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180723G0Y8YZ00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券