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

优化Go程序的简单技巧-stephen.sh

根据我的经验,性能不佳表现为以下两种方式之一:我的职业生涯大部分时间都是用Python做数据科学,或者用Go构建服务; 我有更多优化后者的经验。Go通常不是我编写的服务的瓶颈-程序通常在与数据库通信时受到IO限制。但是,在批处理机器学习管道中-就像我在之前的角色中构建的那样-您的程序通常受CPU限制。当您的Go程序使用过多的CPU,并且过度使用会产生负面影响时,您可以使用各种策略来缓解这种情况。这

根据我的经验,性能不佳表现为以下两种方式之一:

  • 在小规模上表现良好的运营,但随着用户数量的增长而变得不可行。这些通常是O(N)或O(N²)操作。当您的用户群很小时,这些表现很好,通常是为了将产品推向市场。随着您的使用基础的增长,您会看到更多您不期望的 病态示例 ,并且您的服务将停止运行。
  • 许多个别小优化的来源 - AKA'千人死亡'。

我的职业生涯大部分时间都是用 Python 做数据科学,或者用 Go 构建服务; 我有更多优化后者的经验。Go通常不是我编写的服务的瓶颈 - 程序通常在与数据库通信时受到IO限制。但是,在批处理机器学习管道中 - 就像我在之前的角色中构建的那样 - 您的程序通常受CPU限制。当您的Go程序使用过多的CPU,并且过度使用会产生负面影响时,您可以使用各种策略来缓解这种情况。

这篇文章解释了一些可以用来显着提高程序性能的技巧。我故意忽略需要付出巨大努力的技术,或者对程序结构进行大量更改。

在你开始之前

在对程序进行任何更改之前,请花时间创建适当的基线进行比较。如果你不这样做,你会在黑暗中四处搜寻,想知道你的改变是否有任何改善。首先编写基准测试,并获取在pprof中使用的 配置文件 。在最好的情况下,这将是一个 Go基准 :这允许轻松使用pprof和内存分配分析。您还应该使用 benchcmp :一个有用的工具,用于比较两个基准测试之间的性能差异。

如果您的代码不容易进行基准测试,那么请从您可以计算的时间开始。您可以使用手动配置代码 runtime/pprof 。

让我们开始吧!

使用sync.Pool重新使用以前分配的对象

sync.Pool 实现一个  free-list .。这允许您重新使用先前分配的struct 。这会在多个用法中分配对象的分配,从而减少垃圾收集器必须完成的工作。API非常简单:实现一个分配对象新实例的函数。它应该返回一个指针类型。

var bufpool = sync.Pool{
    New: func() interface{} {
        buf := make([]byte, 512)
        return &buf
    }}

在此之后,您可以Get()从池中获取对象,在Put()完成后将它们返回。

// sync.Pool returns a interface{}: you must cast it to the underlying type
// before you use it.
b := *bufpool.Get().(*[]byte)
defer bufpool.Put(&b)


// Now, go do interesting things with your byte buffer.
buf := bytes.NewBuffer(b)

在Go 1.13之前,每次发生垃圾收集时,池都被清除。这可能会对分配很多的程序的性能产生不利影响。在1.13中, 似乎更多的对象将在GC中存活下来 。

在将对象放回池中之前,必须将 struct 的字段清零。

如果不这样做,则可以从池中获取包含先前使用数据的“脏”对象。这可能是一个严重的安全风险!

type AuthenticationResponse {
    Token string
    UserID string
}

rsp := authPool.Get().(*AuthenticationResponse)
defer authPool.Put(rsp)

// If we don't hit this if statement, we might return data from other users!  
if blah {
    rsp.UserID = "user-1"
    rsp.Token = "super-secret
}

return rsp

确保始终保持零内存的安全方法是明确地这样做:

// reset resets all fields of the AuthenticationResponse before pooling it.

func (a* AuthenticationResponse) reset() {

a.Token = ""

a.UserID = ""

}

rsp := authPool.Get().(*AuthenticationResponse)

defer func() {

rsp.reset()

authPool.Put(rsp)

}()

其中,这不是一个问题的唯一情况是当您使用正是你写的内存。例如:

var (
    r io.Reader
    w io.Writer
)

// Obtain a buffer from the pool.
buf := *bufPool.Get().(*[]byte)
defer bufPool.Put(&buf)

// We only write to w exactly what we read from r, and no more.  
nr, er := r.Read(buf)
if nr > 0 {
    nw, ew := w.Write(buf[0:nr])
}

避免使用包含指针的struct作为大型Map的Key

在垃圾收集期间,运行时扫描包含指针的对象,并追踪它们。如果你有一个非常大的map[string]int,GC必须检查地图中的每个字符串,每个GC,因为字符串包含指针。

在这个例子中,我们向a写入1000万个元素map[string]int,并为垃圾收集计时。我们在包范围内分配映射以确保它是堆分配的。

package main

import (
    "fmt"
    "runtime"
    "strconv"
    "time"
)

const (
    numElements = 10000000
)

var foo = map[string]int{}

func timeGC() {
    t := time.Now()
    runtime.GC()
    fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
    for i := 0; i for {
        timeGC()
        time.Sleep(1 * time.Second)
    }
}

运行此程序,我们看到以下内容:

inthash → go install && inthash
gc took: 98.726321ms
gc took: 105.524633ms
gc took: 102.829451ms
gc took: 102.71908ms
gc took: 103.084104ms
gc took: 104.821989ms

我们可以做些什么来改善它?尽可能删除指针似乎是一个好主意 - 我们将减少垃圾收集器必须追逐的指针数量。 字符串包含指针 ; 所以让我们实现这个map[int]int。

package main

import (
    "fmt"
    "runtime"
    "time"
)

const (
    numElements = 10000000
)

var foo = map[int]int{}

func timeGC() {
    t := time.Now()
    runtime.GC()
    fmt.Printf("gc took: %s\n", time.Since(t))
}

func main() {
    for i := 0; i for {
        timeGC()
        time.Sleep(1 * time.Second)
    }
}

再次运行程序,我们得到以下内容:

go install && inthash
gc took: 3.608993ms
gc took: 3.926913ms
gc took: 3.955706ms
gc took: 4.063795ms
gc took: 3.91519ms
gc took: 3.75226ms

好多了。我们已经将垃圾收集时间缩短了97%。在生产用例中,在插入Map之前,您需要将字符串哈希为整数。

你可以做更多的事情来逃避GC。如果您分配无指针结构,整数或字节的巨型数组, GC将不会扫描它 :这意味着您不需要支付GC开销。这些技术通常需要对程序进行大量的重新设计,因此我们今天不会深入研究它们。

与所有优化一样,您的里程可能会有所不同。查看 来自Damian Gryski 的 Twitter帖子,这 是一个有趣的例子,从大型Map中删除字符串以支持更智能的数据结构实际上增加了内存。一般来说,你应该阅读他所提出的一切。

代码生成编组代码以避免运行时反射

将struct编组和解组为各种序列化格式(如JSON)是一种常见操作; 特别是在构建微服务时。实际上,您经常会发现大多数微服务实际上做的唯一事情就是序列化。函数类似于json.Marshal并json.Unmarshal依赖于 运行时反射 来将结构字段序列化为字节,反之亦然。这可能很慢:反射并不像显式代码那样高效。

但是,它不一定是这种方式。编组JSON的机制有点像这样:

package json

// Marshal take an object and returns its representation in JSON.
func Marshal(obj interface{}) ([]byte, error) {
    // Check if this object knows how to marshal itself to JSON
    // by satisfying the Marshaller interface.
    if m, is := obj.(json.Marshaller); is {
        return m.MarshalJSON()
    }

    // It doesn't know how to marshal itself. Do default reflection based marshallling.
    return marshal(obj)
}

如果我们知道如何编组JSON,我们有一个避免运行时反射的钩子。但是我们不想手写所有的编组代码,那么我们该怎么办?让计算机为我们编写代码!像 easyjson 这样的代码生成器查看struct,并生成高度优化的代码,该代码与现有的编组接口json.Marshaller完全兼容。

下载该包,并在$file.go包含要为其生成代码的结构上运行以下命令。

easyjson -all $file.go

您应该找到$file_easyjson.go已生成的文件。由于easyjson已经为您实现了json.Marshaller接口,因此将调用这些函数而不是基于反射的默认值。恭喜: 您刚刚将JSON编组代码加速了3倍 。你可以通过很多东西来提高性能。

更改struct时,您需要确保重新生成编组代码。如果您忘记了,您添加的新字段将不会被序列化和反序列化,这可能会令人困惑!您可以使用它go generate来为您处理此代码生成。为了使这些与结构保持同步,我喜欢generate.go在包的根目录中调用包中go generate的所有文件:当有许多需要生成的文件时,这可以帮助维护。热门提示:go generate在CI中调用并检查它没有带有签入代码的差异,以确保结构是最新的。

使用strings.Builder建立字符串

在Go中,字符串是不可变的:将它们视为只读字节片。这意味着每次创建字符串时,都会分配新内存,并可能为垃圾收集器创建更多工作。

在Go 1.10中, strings.Builder 作为构建字符串的有效方式被引入。在内部,它写入一个字节缓冲区。只有在调用String()构建器时,才会实际创建字符串。它依赖于一些unsafe技巧来将基础字节作为具有零分配的字符串返回:请参阅 此博客 以进一步了解其工作原理。

让我们进行性能比较以验证两种方法:

// main.go
package main

import "strings"

var strs = []string{
    "here's",
    "a",
    "some",
    "long",
    "list",
    "of",
    "strings",
    "for",
    "you",
}

func buildStrNaive() string {
    var s string

    for _, v := range strs {
        s += v
    }

    return s
}

func buildStrBuilder() string {
    b := strings.Builder{}

    // Grow the buffer to a decent length, so we don't have to continually
    // re-allocate.
    b.Grow(60)

    for _, v := range strs {
        b.WriteString(v)
    }

    return b.String()
}
// main_test.go
package main

import (
    "testing"
)

var str string

func BenchmarkStringBuildNaive(b *testing.B) {
    for i := 0; i for i := 0; i 

在Macbook Pro上得到以下结果:

go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/sjwhitworth/perfblog/strbuild
BenchmarkStringBuildNaive-8          5000000           255 ns/op         216 B/op          8 allocs/op
BenchmarkStringBuildBuilder-8       20000000            54.9 ns/op        64 B/op          1 allocs/op

我们可以看到, strings.Builder速度提高了4.7倍 ,导致分配数量的1/8,以及分配的内存的1/4。

如果性能很重要,请使用strings.Builder。一般来说,我建议除了最简单的构建字符串之外的所有情况都使用它。

使用strconv而不是fmt

fmt 是Go中最知名的软件包之一。您可能已经在第一个Go程序中使用它来向屏幕打印“hello,world”。然而,当涉及将整数和浮点数转换为字符串时,它的性能不如它的低级表兄: strconv 。对于API中的一些非常小的变化,这个软件包可以为您提供更好的性能。

fmt主要是interface{}作为函数的参数。这有两个缺点:

  • 你失去了类型安全。对我来说这是一个很大的问题。
  • 它可以增加所需的分配数量。传递非指针类型interface{}通常会导致堆分配。阅读 此博客 ,找出原因。

以下程序显示了性能差异:

// main.go
package main

import (
    "fmt"
    "strconv"
)

func strconvFmt(a string, b int) string {
    return a + ":" + strconv.Itoa(b)
}

func fmtFmt(a string, b int) string {
    return fmt.Sprintf("%s:%d", a, b)
}

func main() {}
// main_test.go
package main

import (
    "testing"
)

var (
    a    = "boo"
    blah = 42
    box  = ""
)

func BenchmarkStrconv(b *testing.B) {
    for i := 0; i for i := 0; i 

Macbook Pro上的基准测试结果:

go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/sjwhitworth/perfblog/strfmt
BenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/op
BenchmarkFmt-8          10000000           143 ns/op          72 B/op     

我们可以看到 strconv版本快3.5倍 ,分配数量的1/3,分配的内存的一半。

分配make中的容量以避免重新分配

在我们进行性能改进之前,让我们快速回顾一下slice 。slice 是Go中非常有用的构造。它提供了一个可调整大小的数组,能够在不重新分配的情况下在相同的底层内存上获取不同的视图。如果你偷看引擎盖下,slice 由三个元素组成:

type slice struct {
    // pointer to underlying data in the slice.
    data uintptr
    // the number of elements in the slice.
    len int
    // the number of elements that the slice can 
    // grow to before a new underlying array
    // is allocated.
    cap int     
}

说明:

  • data:指向slice 中基础数据的指针
  • len:slice 中当前的元素数。
  • cap:slice 在重新分配之前可以增长的元素数。

在引擎盖下,slice 是固定长度的阵列数组。当你到达cap一个slice 时,会分配一个前一个slice 上限加倍的新数组,将内存从旧切片复制到新slice ,旧数组被丢弃

我经常看到类似下面的代码,当预先知道slice 的容量时,会分配零容量的slice 。

var userIDs []string
for _, bar := range rsp.Users {
    userIDs = append(userIDs, bar.ID)
}

在这种情况下,切片以零长度和零容量开始。收到响应后,我们将用户附加到slice 。当我们这样做时,我们达到了slice 的容量:需要分配了一个新的底层数组,它是前一个slice 容量的两倍,并且slice 中的数据被复制到其中。如果响应中有8个用户,则会产生5个分配。

一种更有效的方法是将其更改为以下内容:

userIDs := make([]string, 0, len(rsp.Users)

for _, bar := range rsp.Users {
    userIDs = append(userIDs, bar.ID)
}

我们已经使用make明确地将容量分配给slice 。现在,我们可以附加到slice ,知道我们不会触发额外的分配和复制。

如果您不知道应分配多少因为容量是动态的或稍后在程序中计算的,请测量在程序运行时最终得到的切片大小的分布。我通常采用第90或第99百分位数,并对程序中的值进行硬编码。如果您有RAM来换取CPU,请将此值设置为高于您认为需要的值。

此建议也适用于map:使用make(map[string]string, len(foo))将在引擎盖下分配足够的容量以避免重新分配。

使用允许您传递字节slice 的方法

使用包时,请查看使用允许传递字节slice 的方法:这些方法通常可以让您更好地控制分配。

time.Format vs.  time.AppendFormat 是一个很好的例子。time.Format返回一个字符串。在引擎盖下,这会分配一个新的字节slice 并对其进行调用time.AppendFormat。time.AppendFormat采用字节缓冲区,写入时间的格式化表示,并返回扩展字节slice 。这在标准库的其他包中很常见:请参阅strconv.AppendFloat(链接)或bytes.NewBuffer。

为什么这会增加性能呢?那么,您现在可以传递从您获得的字节slice sync.Pool,而不是每次都分配一个新的缓冲区。或者,您可以将初始缓冲区大小增加到您认为更适合您的程序的值,以减少切片重新复制。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 我们


推荐阅读
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 在Oracle11g以前版本中的的DataGuard物理备用数据库,可以以只读的方式打开数据库,但此时MediaRecovery利用日志进行数据同步的过 ... [详细]
author-avatar
全球时_尚热门焦点吧
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有