Skip to content

Files

Latest commit

8d041bb · Oct 24, 2021

History

History
165 lines (94 loc) · 22 KB

File metadata and controls

165 lines (94 loc) · 22 KB

一、Go 性能简介

这本书是为中级到高级的 Go 开发人员编写的。这些开发人员将希望从他们的 Go 应用程序中挤出更多的性能。为此,本书将有助于推动现场可靠性工程手册中定义的四个黄金信号 https://landing.google.com/sre/sre-book/chapters/monitoring-distributed-systems/ )。如果我们能够减少延迟和错误,并在减少饱和的同时增加流量,我们的程序将继续保持更高的性能。遵循“四个黄金信号”的思想对任何开发考虑性能的 Go 应用程序的人都是有益的。

在本章中,您将了解计算机科学中性能的一些核心概念。您将了解 Go 计算机编程语言的一些历史,它的创造者如何决定将性能放在语言的前列是很重要的,以及为什么编写 PerformantGO 很重要。Go 是一种为性能而设计的编程语言,本书将带您了解如何利用 Go 的一些设计和工具。这将帮助您编写更高效的代码。

在本章中,我们将介绍以下主题:

  • 理解计算机科学中的性能
  • Go 简史
  • Go 性能背后的意识形态

提供这些主题是为了指导您开始理解使用 Go 语言编写高性能代码所需的方向。

技术要求

对于本书,您应该对 Go 语言有一定的理解。在探讨这些主题之前,需要了解的一些关键概念包括:

在本书中,将有许多代码示例和基准测试结果。这些都可以通过位于的 GitHub 存储库访问 https://github.com/bobstrecansky/HighPerformanceWithGo/

如果您有问题或希望请求更改存储库,请随时在的存储库中创建问题 https://github.com/bobstrecansky/HighPerformanceWithGo/issues/new

理解计算机科学中的性能

计算机科学中的性能是对计算机系统可以完成的工作的一种度量。性能代码对于许多不同的开发人员群体来说至关重要。无论您是一家需要快速向客户交付大量数据的大型软件公司的一员,还是一个计算资源有限的嵌入式计算设备程序员,还是一个希望从您用于您的宠物项目的 Raspberry Pi 中挤出更多请求的爱好者,绩效应该是你发展思维的最前沿。性能很重要,尤其是当您的规模继续增长时。

重要的是要记住,我们有时受到物理界限的限制。CPU、内存、磁盘 I/O 和网络连接都有基于您从云提供商购买或租用的硬件的性能上限。还有其他系统可能与我们的 Go 程序同时运行,也可能消耗资源,如操作系统包、日志实用程序、监视工具和其他二进制文件。请谨慎记住,我们的程序通常不是它们运行的物理机器上的唯一租户。

优化代码通常在许多方面都有帮助,包括:

  • 缩短响应时间:响应请求所需的总时间。
  • 延迟减少:系统内因果之间的时间延迟。
  • 增加的吞吐量:处理数据的速率。
  • 更高的可伸缩性:在一个包含的系统中可以处理更多的工作。

在计算机系统中,有许多方法可以处理更多的请求。添加更多单独的计算机(通常称为水平缩放)或升级到更强大的计算机(通常称为垂直缩放)是用于处理计算机系统内需求的常见做法。在不需要额外硬件的情况下为更多请求提供服务的最快方法之一是提高代码性能。性能工程作为一种帮助水平和垂直缩放的方法。代码的性能越高,在一台机器上可以处理的请求就越多。这种模式可能导致运行工作负载的物理主机更少或更便宜。这对于许多企业和爱好者来说都是一个巨大的价值主张,因为它有助于降低运营成本并改善最终用户体验。

关于大 O 表示法的一点注记

大 O 符号(https://en.wikipedia.org/wiki/Big_O_notation 通常用于描述基于输入大小的函数限制行为。在计算机科学中,大 O 符号用于解释算法之间的比较效率。我们将在第 2 章数据结构和算法中对此进行更详细的讨论。大 O 表示法在优化性能方面很重要,因为它被用作解释算法可扩展性的比较运算符。理解大 O 符号将有助于编写更高性能的代码,因为它将有助于在编写代码时推动代码的性能决策。了解不同算法在哪一点上具有相对的优势和劣势,有助于您确定当前实现的正确选择。我们无法改进我们无法测量的东西大 O 符号帮助我们对手头的问题陈述给出具体的测量。

衡量长期绩效的方法

在我们进行性能改进时,我们需要持续监控我们的更改以查看影响。许多方法可以用来监视计算机系统的长期性能。以下是这些方法的几个示例:

我们将在第 15 章中进一步讨论这些概念,比较版本之间的代码质量。这些范例帮助我们对代码中的性能优化做出明智的决策,并避免过早的优化。对于许多计算机程序员来说,过早优化是一个非常重要的方面。通常,我们必须确定什么是足够快的*。当许多其他代码路径有机会从性能角度进行改进时,我们可能会浪费时间来优化一小段代码。Go 的简单性允许在不增加认知负载开销或增加代码复杂性的情况下进行额外的优化。我们将在第 2 章数据结构和算法中讨论的算法将帮助我们避免过早优化。*

*# 优化策略概述

在本书中,我们还将试图了解我们优化的确切目的。优化 CPU 或内存利用率的技术可能与优化 I/O 或网络延迟的技术非常不同。了解您的问题空间以及硬件和上游 API 中的限制将有助于您确定如何针对手头的问题声明进行优化。优化也经常显示出收益递减。通常,基于外部因素,特定代码热点的开发投资回报是不值得的,或者添加优化将降低可读性并增加整个系统的风险。如果您能够在早期确定是否值得进行优化,您将能够有一个范围更窄的关注点,并且可能会继续开发一个性能更高的系统。

了解计算机系统内的基线操作可能会有所帮助。谷歌研究总监彼得·诺维格设计了一个表格(下图),帮助开发者理解典型计算机上的各种常见计时操作(https://norvig.com/21-days.html#answers

清楚地了解计算机的不同部分可以如何相互操作,有助于我们推断性能优化应该在哪里。从表中可以看出,从磁盘顺序读取 1 MB 的数据比通过 1 Gbps 网络链路发送 2 KBs 的数据需要更长的时间。能够为常见的计算机交互使用背面的数学比较运算符,这非常有助于推断下一步应该优化哪段代码。当您后退一步并查看整个系统的快照时,确定程序中的瓶颈会变得更容易。

将性能问题分解为小的、可管理的、可以同时改进的子问题是向优化的有益转变。试图一次解决所有性能问题通常会让开发人员陷入困境和沮丧,并常常导致许多性能工作失败。关注当前系统中的瓶颈通常可以产生结果。修复一个瓶颈通常会快速识别另一个瓶颈。例如,在解决 CPU 利用率问题后,您可能会发现系统磁盘无法写入计算速度足够快的值。以结构化的方式解决瓶颈是创建一个高性能、可靠软件的最佳方法之一。

优化级别

从下图中金字塔的底部开始,我们可以一直到顶部。此图显示了进行性能优化的建议优先级。这个金字塔的前两个层次——设计层次和算法与数据结构层次——通常会提供更多的实际性能优化目标。下图显示了一种通常有效的优化策略。改变程序设计以及算法和数据结构通常是提高代码库速度和质量的最有效方法:

设计级别的决策通常对性能有最可测量的影响。在设计阶段确定目标有助于确定最佳的优化方法。例如,如果我们正在为磁盘 I/O 速度较慢的系统进行优化,那么我们应该优先降低对磁盘的调用次数。相反,如果我们正在为计算资源有限的系统进行优化,我们只需要计算程序响应所需的最基本值。在一个新项目开始时创建一个详细的设计文档将有助于理解性能增益在哪里是重要的,以及如何在项目中优先安排时间。从在计算系统中传输有效负载的角度考虑,通常会发现可以进行优化的地方。我们将在第 3 章理解并发中进一步讨论设计模式。

算法和数据结构决策通常会对计算机程序的性能产生可测量的影响。在编写性能代码时,我们应该集中精力尝试使用常量 O(1)、对数 O(logn)、线性 O(n)和对数线性 O(n logn)函数。在规模上避免二次复杂性 O(n2)对于编写可伸缩程序也很重要。我们将在第 2 章数据结构和算法中进一步讨论 O 符号及其与 Go 的关系。

Go 简史

Robert Griesemer、Rob Pike 和 Ken Thompson 在 2007 年创建了 Go 编程语言。它最初设计为一种通用语言,非常关注系统编程。创造者在设计 Go 语言时考虑了两个核心原则:

  • 静态类型
  • 运行效率
  • 可读的
  • 实用的
  • 易学
  • 高性能网络和多处理

Go 于 2009 年公开发布,v1.0.3 于 2012 年 3 月 3 日发布。在撰写本书时,Go 版本 1.14 已经发布,Go 版本 2 即将发布。如前所述,Go 最初的核心架构考虑之一是具有高性能的网络和多处理。这本书将涵盖 Griesemer、Pike 和 Thompson 代表他们的语言实现和传播的许多设计考虑。设计师们创建了 GO,因为他们对 C++语言中的一些选择和方向感到不满。大型分布式编译集群上长期运行的复杂性是创建者的主要痛苦来源。在此期间,作者开始学习下一个 C++程序语言版本,称为 C++ X11。这个 C++版本有许多新的特点正在计划中,GO 团队决定他们要采用一个谚语 T0 的习惯用法,在他们使用的计算语言中,更少的是更为有趣的。

该语言的作者举行了他们的第一次会议,他们讨论了从 C 编程语言开始,构建特性和删除他们认为对该语言不重要的无关功能。该团队最终从零开始,只借用了 C 语言和其他他们熟悉的语言中最基本的部分。在他们的工作开始成形后,他们意识到他们正在剥夺其他语言的一些核心特性,特别是缺少标题、循环依赖项和类。作者认为,即使去除了其中的许多片段,Go 仍然可以比其前身更具性能力。

Go 标准图书馆

Go 中的标准库遵循相同的模式。它的设计考虑到了简单性和功能性。向标准库中添加切片、映射和复合文字有助于该语言早期变得固执己见。Go 的标准库位于$GOROOT范围内,可直接导入。将这些默认数据结构内置到语言中可以使开发人员有效地使用这些数据结构。标准库软件包与语言发行版捆绑在一起,并在安装 Go 后立即可用。人们经常提到,标准库是关于如何编写惯用 Go 的可靠参考。关于标准库惯用 Go 的理由是,这些核心库文章写得清晰、简洁,并且有相当多的上下文。它们还很好地添加了小而重要的实现细节,例如能够设置连接超时,并且能够显式地从底层函数收集数据。这些语言细节有助于语言的繁荣。

一些值得注意的 Go 运行时功能包括:

  • 用于安全内存管理的垃圾收集(并发、三色、标记清除收集器)
  • 同时支持多个任务的并发性(详见第 3 章理解并发性
  • 用于内存优化的堆栈管理(原始实现中使用了分段堆栈;堆栈复制是 Go 堆栈管理的当前咒语)

Go 工具集

Go 的二进制版本还包括一个用于创建优化代码的庞大工具集。在 Go 二进制文件中,go命令有很多功能,可以帮助构建、部署和验证代码。让我们讨论两个与性能相关的核心功能。

Godoc 是 Go 的文档工具,它将文档的关键保持在程序开发的最前沿。干净的实现、深入的文档和模块化都是构建可扩展、高性能系统的核心部分。Godoc 通过自动生成文档来帮助实现这些目标。Godoc 从$GOROOT$GOPATH中找到的包中提取并生成文档。生成此文档后,Godoc 运行 web 服务器并将生成的文档显示为网页。标准库的文档可以在 Go 网站上查看。例如,标准库pprof包的文档可在中找到 https://golang.org/pkg/net/http/pprof/

在语言中添加了gofmt(Go 的代码格式化工具)带来了不同的性能。gofmt一开始就允许 Go 在代码格式化方面非常固执己见。有了精确的强制格式规则,就有可能以开发人员可以理解的方式编写 Go,同时让工具在 Go 项目中按照一致的模式格式化代码。许多开发人员在保存他们正在编写的文件时,让他们的 IDE 或文本编辑器执行gofmt命令。一致的代码格式减少了认知负载,并允许开发人员关注代码的其他方面,而不是决定是否使用制表符或空格缩进代码。减少认知负载有助于提高开发人员的动力和项目速度。

Go 的构建系统也有助于提高性能。go build命令是编译包及其依赖项的强大工具。Go 的构建系统也有助于依赖关系管理。生成系统的结果输出是一个已编译的静态链接二进制文件,其中包含在编译平台上运行的所有必需元素。go module(Go 1.11 中引入并在 Go 1.13 中最终确定的具有初步支持的新功能)是 Go 的依赖关系管理系统。对一种语言进行明确的依赖关系管理有助于提供将版本化包分组为一个内聚单元的一致体验,从而实现更具可复制性的构建。具有可复制的构建有助于开发人员通过源代码的可验证路径创建二进制文件。在项目中创建供应商目录的可选步骤也有助于本地存储和满足项目的依赖关系。

编译后的二进制文件也是 Go 生态系统的重要组成部分。Go 还允许您为其他目标环境构建二进制文件,如果您需要为其他计算机体系结构交叉编译二进制文件,这将非常有用。拥有构建可在任何平台上运行的二进制文件的能力,可以帮助您快速迭代和测试代码,以在替代体系结构上发现瓶颈,避免它们变得更难修复。该语言的另一个关键特性是,您可以在一台具有 OS 和体系结构标志的机器上编译二进制文件,并且该二进制文件可以在另一个系统上执行。当构建系统具有大量系统资源且构建目标具有有限的计算资源时,这一点至关重要。为两种体系结构构建二进制文件与设置构建标志一样简单:

要在 x86_64 体系结构上为 macOS X 构建二进制文件,请使用以下执行模式:

GOOS=darwin GOARCH=amd64 go build -o myapp.osx

要在 ARM 体系结构上构建 Linux 二进制文件,请使用以下执行模式:

GOOS=linux GOARCH=arm go build -o myapp.linuxarm

您可以使用以下命令找到GOOSGOARCH的所有有效组合的列表:

go tool dist list -json

这有助于您查看 Go 语言可以编译二进制文件的所有 CPU 体系结构和操作系统。

基准测试概述

基准测试的概念也将成为本书的核心内容。Go 的测试功能具有一流的性能。能够在开发和发布过程中触发测试基准,可以继续交付性能良好的代码。随着新的副作用的引入、特性的添加以及代码复杂性的增加,有一种方法来验证跨代码库的性能回归是很重要的。许多开发人员将基准测试结果添加到他们的持续集成实践中,以确保他们的代码在所有新的请求添加到存储库时继续保持性能。您还可以使用golang.org/x/perf/cmd/benchstat包中提供的benchstat实用程序来比较有关基准的统计信息。下面的示例存储库在中有一个基准测试标准库排序功能的示例 https://github.com/bobstrecansky/HighPerformanceWithGo/tree/master/1-introduction

在标准库中紧密结合测试和基准测试,可以鼓励将性能测试作为代码发布过程的一部分。务必记住,基准并不总是指示现实世界的性能场景,因此对从基准中获得的结果持保留态度。记录、监视、分析和跟踪运行系统(将在第 12 章分析 Go 代码第 13 章跟踪 Go 代码第 15 章中讨论,比较版本间的代码质量)在您提交了正在处理的代码之后,可以帮助验证您在基准测试中所做的假设。

Go 性能背后的意识形态

Go 的大部分性能都是从并发性和并行性中获得的。goroutine 和 channels 通常用于并行执行许多请求。Go 可用的工具有助于实现接近 C 的性能,具有非常可读的语义。这是开发人员在大规模解决方案中常用 Go 的众多原因之一。

Goroutines–从一开始就性能出色

当 Go 被构思出来时,多核处理器开始在商用硬件中变得越来越普遍。Go 语言的作者认识到在他们的新语言中需要并发性。Go 通过 Goroutine 和 Channel 简化了并发编程(我们将在第 3 章理解并发中讨论)。Goroutines 是一种轻量级计算线程,与操作系统线程不同,通常被描述为该语言的最佳特性之一。goroutine 并行执行它们的代码,并在工作完成时完成。goroutine 的启动时间比线程的启动时间快,这允许在程序中进行更多的并发工作。与 Java 等依赖操作系统线程的语言相比,Go 的多处理模型效率更高。Go 在阻塞 goroutine 操作方面也很智能。这有助于提高内存利用率、垃圾收集和延迟。Go 的运行时使用GOMAXPROCS变量将 Goroutine 多路复用到真正的 OS 线程上。我们将在第 2 章数据结构和算法中了解更多关于 goroutine 的信息。

通道–类型化导管

通道提供了一个在 goroutine 之间发送和接收数据的模型,同时跳过了底层平台提供的过去的同步原语。通过正确考虑 goroutines 和 channels,我们可以实现高性能。通道既可以是缓冲的,也可以是非缓冲的,因此开发人员可以通过开放通道传递动态量的数据,直到接收器接收到该值,此时发送方解除了通道的阻塞。如果通道被缓冲,发送方将按照给定的缓冲区大小进行阻塞。一旦缓冲区被填满,发送方将解锁通道。最后,可以调用close()函数来指示通道将不再接收任何值。我们将在第 3 章理解并发中了解更多关于通道的信息。

C-可比性能

另一个初始目标是接近 C 语言在可比程序中的性能。Go 还有大量的评测和跟踪工具,我们将在第 12 章评测 Go 代码第 13 章跟踪 Go 代码中学习。Go 使开发人员能够查看 goroutine 使用情况、通道、内存和 CPU 利用率以及与单个调用相关的函数调用的明细。这是很有价值的,因为 Go 可以轻松地解决数据和可视化的性能问题。

大规模分布式系统

由于 Go 操作简单,并且在标准库中内置了网络原语,因此它通常用于大型分布式系统。在开发过程中能够快速迭代是构建健壮、可扩展系统的重要部分。高网络延迟通常是分布式系统中的一个问题,Go 团队已经在他们的平台上努力尝试并缓解这一问题。从标准图书馆网络实施到使 gRPC 成为在分布式平台上的客户端和服务器之间传递缓冲消息的一流公民,Go 语言开发人员已经将分布式系统问题放在了他们语言的问题空间的最前沿,并为这些复杂问题提出了一些优雅的解决方案。

总结

在本章中,我们学习了计算机科学中性能的核心概念。我们还学习了 Go 计算机编程语言的一些历史,以及它的诞生与性能工作的直接联系。最后,我们了解到,由于 Go 语言的实用性、灵活性和可扩展性,它被用于各种不同的情况。本章介绍了本书将持续构建的概念,允许您重新思考编写 Go 代码的方式。

第 2 章数据结构和算法中,我们将深入探讨数据结构和算法。我们将学习不同的算法,它们的大 O 符号,以及这些算法是如何在 Go 中构造的。我们还将学习这些理论算法如何与实际问题相关联,以及如何快速高效地为大量请求提供服务。学习更多关于这些算法的知识将有助于您在本章前面列出的优化三角的第二层中变得更加高效。*