热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

golist指针_「GCTT出品」Go语言机制之内存剖析

前序(Prelude)本系列文章总共四篇,主要帮助大家理解Go语言中一些语法结构和其背后的设计原则,包括指针、栈、堆、逃逸分析和值指针传递。这是第三篇&

前序(Prelude)

本系列文章总共四篇,主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则,包括指针、栈、堆、逃逸分析和值/指针传递。这是第三篇,主要介绍堆和逃逸分析。(译者注:这一篇可看成第二篇的进阶版)

以下是本系列文章的索引:

  1. 「GCTT 出品」Go 语言机制之栈和指针
  2. 「GCTT 出品」Go 语言机制之逃逸分析
  3. Go 语言机制之内存剖析
  4. Go 语言机制之数据和语法的设计哲学

观看这段示例代码的视频演示:GopherCon Singapore (2017) - Escape Analysis

介绍(Introduction)

在前面的博文中,通过一个共享在 goroutine 的栈上的值的例子讲解了逃逸分析的基础。还有其他没有介绍的造成值逃逸的场景。为了帮助大家理解,我将调试一个分配内存的程序,并使用非常有趣的方法。

程序(The Program)

我想了解 io 包,所以我创建了一个简单的项目。给定一个字符序列,写一个函数,可以找到字符串 elvis 并用大写开头的 Elvis 替换它。我们正在讨论国王(Elvis 即猫王,摇滚明星),他的名字总是大写的。

这是一个解决方案的链接:https://play.golang.org/p/n_SzF4Cer4

这是一个压力测试的链接:https://play.golang.org/p/TnXrxJVfLV

代码列表里面有两个不同的函数可以解决这个问题。这篇博文将会关注(其中的)algOne 函数,因为它使用到了 io 库。你可以自己用下 algTwo,体验一下内存,CPU 消耗的差异。

清单 1

bee77b2299c31db6f43c41967b63dd0d.png

这是完整的 algOne 函数。

清单 2

95c63e8dafa0fa04cfc02f7e310547b0.png
df57ba19ca9ff9f05627b84796a82429.png

我想知道的是这个函数的性能表现得怎么样,以及它在堆上分配带来什么样的压力。为了这个目的,我们将进行压力测试。

压力测试(Benchmarking)

这个是我写的压力测试函数,它在内部调用 algOne 函数去处理数据流。

清单 3

ae0837bfe95306ff453cdf0bef870e2c.png

有这个压力测试函数,我们就可以运行 go test 并使用 -bench,-benchtime 和 -benchmem 选项。

清单 4

255eecd601cec14cbf9f8742e4efdde2.png

运行完压力测试后,我们可以看到 algOne 函数分配了两次值,每次分配了 117 个字节。这真的很棒,但我们还需要知道哪行代码造成了分配。为了这个目的,我们需要生成压力测试的分析数据。

性能分析(Profiling)

为了生成分析数据,我们将再次运行压力测试,但这次为了生成内存检测数据,我们打开 -memprofile 开关。

清单 5

0cb3a08dc171fa44fb95c38f8245a59f.png

一旦压力测试完成,测试工具就会生成两个新的文件。

清单 6

48e52f6d929bc8cb6b0cc37d5d72dcca.png

源码在 memcpu 目录中,algOne 函数在 stream.go 文件中,压力测试函数在 stream_test.go 文件中。新生成的文件为 mem.out 和 memcpu.test。mem.out 包含分析数据和 memcpu.test 文件,以及包含我们查看分析数据时需要访问符号的二进制文件。

有了分析数据和二进制测试文件,我们就可以运行 pprof 工具学习数据分析。

清单 7

608a32d4a1ac76176be074d55dc8143f.png

当分析内存数据时,为了轻而易举地得到我们要的信息,你会想用 -alloc_space 选项替代默认的 -inuse_space 选项。这将会向你展示每一次分配发生在哪里,不管你分析数据时它是不是还在内存中。

在 (pprof) 提示下,我们使用 list 命令检查 algOne 函数。这个命令可以使用正则表达式作为参数找到你要的函数。

清单 8

fc7ccd522934934df2adada55409811f.png

基于这次的数据分析,我们现在知道了 input,buf 数组在堆中分配。因为 input 是指针变量,分析数据表明 input 指针变量指定的 bytes.Buffer 值分配了。我们先关注 input 内存分配以及弄清楚为啥会被分配。

我们可以假定它被分配是因为调用 bytes.NewBuffer 函数时在栈上共享了 bytes.Buffer 值。然而,存在于 flat 列(pprof 输出的第一列)的值告诉我们值被分配是因为 algOne 函数共享造成了它的逃逸。

我知道 flat 列代表在函数中的分配是因为 list 命令显示 Benchmark 函数中调用了 aglOne。

清单 9

18b1087c70ea7a6a874f3bd44066a0fc.png

因为在 cum 列(第二列)只有一个值,这告诉我 Benchmark 没有直接分配。所有的内存分配都发生在函数调用的循环里。你可以看到这两个 list 调用的分配次数是匹配的。

我们还是不知道为什么 bytes.Buffer 值被分配。这时在 go build 的时候打开 -gcflags "-m -m" 就派上用场了。分析数据只能告诉你哪些值逃逸,但编译命令可以告诉你为啥。

编译器报告(Compiler Reporting)

让我们看一下编译器关于代码中逃逸分析的判决。

清单 10

c1949170d8867d6677d48846ce562f97.png

这个命令产生了一大堆的输出。我们只需要搜索输出中包含 stream.go:83,因为 stream.go 是包含这段代码的文件名并且第 83 行包含 bytes.Buffer 的值。搜索后我们找到 6 行。

清单 11

e20f572d1af1f0aa7e681ac01399f51f.png

我们搜索 stream.go:83 找到的第一行很有趣。

清单 12

e8af41e578dbc5d3f5e983411865b702.png

可以肯定 bytes.Buffer 值没有逃逸,因为它传递给了调用栈。这是因为没有调用 bytes.NewBuffer,函数内联处理了。

所以这是我写的代码片段:

清单 13

16c07699d9876799a068c435c2d1d131.png

因为编译器选择内联 bytes.NewBuffer 函数调用,我写的代码被转成:

清单 14

1d657c18835b389babf8d7882500013d.png

这意味着 algOne 函数直接构造 bytes.Buffer 值。那么,现在的问题是什么造成了值从 algOne 栈帧中逃逸?答案在我们搜索结果中的另外 5 行。

清单 15

e958d0e5ccec25550d7399b0e2f85695.png

这几行告诉我们代码中的第 93 行造成了逃逸。input 变量被赋值给一个接口变量。

接口(Interfaces)

我完全不记得在代码中将值赋给了接口变量。然而,如果你看到 93 行,就可以非常清楚地看到发生了什么。

清单 16

b01c47199fe49a617236f56cf070a632.png

io.ReadFull 调用造成了接口赋值。如果你看了 io.ReadFull 函数的定义,你可以看到一个接口类型是如何接收 input 值。

清单 17

6d86a9cf37ff91afabbd0c001bc2c995.png

传递 bytes.Buffer 地址到调用栈,在 Reader 接口变量中存储会造成一次逃逸。现在我们知道使用接口变量是需要开销的:分配和重定向。所以,如果没有很明显的使用接口的原因,你可能不想使用接口。下面是我选择在我的代码中是否使用接口的原则。

使用接口的情况:

  • 用户 API 需要提供实现细节的时候。
  • API 的内部需要维护多种实现。
  • 可以改变的 API 部分已经被识别并需要解耦。

不使用接口的情况:

  • 为了使用接口而使用接口。
  • 推广算法。
  • 当用户可以定义自己的接口时。

现在我们可以问自己,这个算法真的需要 io.ReadFull 函数吗?答案是否定的,因为bytes.Buffer` 类型有一个方法可以供我们使用。使用方法而不是调用一个函数可以防止重新分配内存。

让我们修改代码,删除 io 包,并直接使用 Read 函数而不是 input 变量。

修改后的代码删除了 io 包的调用,为了保留相同的行号,我使用空标志符替代 io 包的引用。这会允许(没有使用的)库导入的行待在列表中。

清单 18

051471f61dec3006f606857ed782ae04.png

修改后我们执行压力测试,可以看到 bytes.Buffer 的分配消失了。

清单 19

e03c419d25b1088b86aac8db47963c83.png

我们可以看到大约 29% 的性能提升。代码从 2570 ns/op 降到 1814 ns/op。解决了这个问题,我们现在可以关注 buf 切片数组。如果再次使用测试代码生成分析数据,我们应该能够识别到造成剩下的分配的原因。

清单 20

5fcdc54ba6fe1a5bf1c8d3bdb79fe08f.png

只剩下 89 行所示,对数组切片的分配。

栈帧

想知道造成 buf 数组切片的分配的原因?让我们再次运行 go build,并使用 -gcflags "-m -m" 选项并搜索 stream.go:89。

清单 21

f385f7d78de4cd5ac2b72a2ea8a425e7.png

报告显示,对于栈来说,数组太大了。这个信息误导了我们。并不是说底层的数组太大,而是编译器在编译时并不知道数组的大小。

值只有在编译器编译时知道其大小才会将它分配到栈中。这是因为每个函数的栈帧大小是在编译时计算的。如果编译器不知道其大小,就只会在堆中分配。

为了验证(我们的想法),我们将值硬编码为 5,然后再次运行压力测试。

清单 22

1da13296167633435d516002aea9832b.png

这一次我们运行压力测试,分配消失了。

清单 23

84f301ef61726dc1fe794f182320aeba.png

如果你再看一下编译器报告,你会发现没有需要逃逸处理的。

清单 24

fa013c3c55fef4d68884e068fd5ba18a.png

很明显我们无法确定切片的大小,所以我们在算法中需要一次分配。

分配和性能(Allocation and Performance)

比较一下我们在重构过程中,每次提升的性能。

清单 25

a5eef7eba2536e896406d11482416d65.png

删除掉 bytes.Buffer 里面的(重新)内存分配,我们获得了大约 29% 的性能提升,删除掉所有的分配,我们能获得大约 33% 的性能提升。内存分配是应用程序性能影响因素之一。

结论(Conclusion)

Go 拥有一些神奇的工具使你能了解编译器作出的跟逃逸分析相关的一些决定。基于这些信息,你可以通过重构代码使得值存在于栈中而不需要在(被重新分配到)堆中。你不是想去掉所有软件中所有的内存(再)分配,而是想最小化这些分配。

这就是说,写程序时永远不要把性能作为第一优先级,因为你并不想(在写程序时)一直猜测性能。写正确的代码才是你第一优先级。这意味着,我们首先要关注的是完整性、可读性和简单性。一旦有了可以运行的程序,才需要确定程序是否足够快。假如程序不够快,那么使用语言提供的工具来查找和解决性能问题。




推荐阅读
  • 使用ArcGIS for Java和Flex浏览自定义ArcGIS Server 9.3地图
    本文介绍了如何在Flex应用程序中实现浏览自定义ArcGIS Server 9.3发布的地图。这是一个基本的入门示例,适用于初学者。 ... [详细]
  • 解决Only fullscreen opaque activities can request orientation错误的方法
    本文介绍了在使用PictureSelectorLight第三方框架时遇到的Only fullscreen opaque activities can request orientation错误,并提供了一种有效的解决方案。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 多线程基础概览
    本文探讨了多线程的起源及其在现代编程中的重要性。线程的引入是为了增强进程的稳定性,确保一个进程的崩溃不会影响其他进程。而进程的存在则是为了保障操作系统的稳定运行,防止单一应用程序的错误导致整个系统的崩溃。线程作为进程的逻辑单元,多个线程共享同一CPU,需要合理调度以避免资源竞争。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 探讨如何在Go语言中高效地处理大规模切片的去重操作,特别是针对百万级数据量的场景。 ... [详细]
  • MySQL的查询执行流程涉及多个关键组件,包括连接器、查询缓存、分析器和优化器。在服务层,连接器负责建立与客户端的连接,查询缓存用于存储和检索常用查询结果,以提高性能。分析器则解析SQL语句,生成语法树,而优化器负责选择最优的查询执行计划。这一流程确保了MySQL能够高效地处理各种复杂的查询请求。 ... [详细]
  • 在机器学习领域,深入探讨了概率论与数理统计的基础知识,特别是这些理论在数据挖掘中的应用。文章重点分析了偏差(Bias)与方差(Variance)之间的平衡问题,强调了方差反映了不同训练模型之间的差异,例如在K折交叉验证中,不同模型之间的性能差异显著。此外,还讨论了如何通过优化模型选择和参数调整来有效控制这一平衡,以提高模型的泛化能力。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • Java并发机制详解及其在数据安全性保障中的应用方案 ... [详细]
  • 在 CentOS 7 系统中安装 Scrapy 时遇到了一些挑战。尽管 Scrapy 在 Ubuntu 上安装简便,但在 CentOS 7 上需要额外的配置和步骤。本文总结了常见问题及其解决方案,帮助用户顺利安装并使用 Scrapy 进行网络爬虫开发。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • Netty框架中运用Protobuf实现高效通信协议
    在Netty框架中,通过引入Protobuf来实现高效的通信协议。为了使用Protobuf,需要先准备好环境,包括下载并安装Protobuf的代码生成器`protoc`以及相应的源码包。具体资源可从官方下载页面获取,确保版本兼容性以充分发挥其性能优势。此外,配置好开发环境后,可以通过定义`.proto`文件来自动生成Java类,从而简化数据序列化和反序列化的操作,提高通信效率。 ... [详细]
  • HBase Java API 进阶:过滤器详解与应用实例
    本文详细探讨了HBase 1.2.6版本中Java API的高级应用,重点介绍了过滤器的使用方法和实际案例。首先,文章对几种常见的HBase过滤器进行了概述,包括列前缀过滤器(ColumnPrefixFilter)和时间戳过滤器(TimestampsFilter)。此外,还详细讲解了分页过滤器(PageFilter)的实现原理及其在大数据查询中的应用场景。通过具体的代码示例,读者可以更好地理解和掌握这些过滤器的使用技巧,从而提高数据处理的效率和灵活性。 ... [详细]
author-avatar
唯美爱人2014
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有