当前位置:首页 > 数码 > 内存优化与渣滓搜集-深化探求优化程序性能的最佳通常-Go (内存优化与渣子有关吗)

内存优化与渣滓搜集-深化探求优化程序性能的最佳通常-Go (内存优化与渣子有关吗)

admin1个月前 (04-21)数码15

Go提供了智能化的内存治理机制,但在某些状况下须要更精细的微调从而防止出现OOM失误。本文将讨论Go的渣滓搜集器、运行程序内存优化以及如何防止OOM(Out-Of-Memory)失误。

Go中的堆(Heap)栈(Stack)

我不会具体引见渣滓搜集器如何上班,曾经有很多关于这个主题的文章和官网文档(比如AGuidetotheGoGarbageCollector[2]和源码[3])。但是,我会提到一些有助于了解本文主题的基本概念。

你或许曾经知道,Go的数据可以存储在两个关键的内存存储中:栈(stack)和堆(heap)。

通常,栈存储的数据的大小和经常使用时期可以由Go编译器预测,包含函数部分变量、函数参数、前往值等。

栈是智能治理的,遵照后进先出(LIFO)准则。当调用函数时,所无关系数据都放在栈的顶部,函数完结时,这些数据将从栈中删除。栈不须要复杂的渣滓搜集机制,其内存治理开支最小,在栈中检索和存储数据的环节十分快。

但是,并不是一切数据都可以存储在栈中。在口头环节中灵活更改的数据或须要在函数范围之外访问的数据不能放在栈上,由于编译器无法预测其经常使用状况,这种数据应该存储在堆中。

与栈不同,从堆中检索数据并对其启动治理的老本更高。

栈里放什么,堆里放什么?

正如前面提到的,栈用于具备可预测大小和寿命的值,例如:

Go编译器在选择将数据放在栈中还是堆中时会思考各种纤细差异。

例如,预调配大小为64KB的数据将存储在栈中,而大于64KB的数据将存储在堆中。这雷同适用于数组,假设数组超越10MB,将存储在堆中。

可以经常使用逃逸剖析(escapeanalysis)来确定特定变量的存储位置。

例如,可以经过命令行编译参数-gcflags=-m来剖析运行程序:

gobuild-gcflags=-mmn.go

假设经常使用-gcflags=-m参数编译上方的main.go:

packagemainfuncmain(){vararrayBefore10Mb[1310720]intarrayBefore10Mb[0]=1vararrayAfter10Mb[1310721]intarrayAfter10Mb[0]=1sliceBefore64:=make([]int,8192)sliceOver64:=make([]int,8193)sliceOver64[0]=sliceBefore64[0]}

结果是:

#command-line-arguments./main.go:3:6:caninlinemain./main.go:7:6:movedtoheap:arrayAfter10Mb./main.go:10:23:make([]int,8192)doesnotescape./main.go:11:21:make([]int,8193)escapestoheap

可以看到arrayAfter10Mb数组被移动到堆中,由于大小超越了10MB,而arrayBefore10Mb依然留在栈中(关于int变量,10MB等于10*1024*1024/8=1310720个元素)。

此外,sliceBefore64没有存储在堆中,由于它的大小小于64KB,而sliceOver64被存储在堆中(关于int变量,64KB等于64*1024/8=8192个元素)。

要了解更多关于在堆中调配的位置和内容,可以参考malloc.go源码[4]。

因此,经常使用堆的一种方法是尽量防止用它!但是,假设数据曾经落在堆中了呢?

与栈不同,堆的大小是有限的,并且不时增长。堆存储灵活创立的对象,如结构体、分片和映射,以及由于其限度而无法放入栈中的大内存块。

在堆中重用内存并防止其齐全阻塞的惟一工具是渣滓搜集器。

浅谈渣滓搜集器的上班原理

渣滓搜集器(GC)是一种专门用于识别和监禁灵活调配内存的系统。

Go经常使用基于跟踪和标志和扫描算法的渣滓搜集算法。在标志阶段,渣滓搜集器将运行程序正在经常使用的数据标志为生动堆。而后,在清算阶段,GC遍历一切未标志为生动的内存并复用。

渣滓搜集器不是收费上班的,须要消耗两个关键的系统资源:CPU时期和物理内存。

渣滓搜集器中的内存由以下部分组成:

渣滓搜集器所消耗的CPU时期与其上班细节无关。有一种称为"stop-the-world"的渣滓搜集器成功,它在渣滓搜集时期齐全中止程序口头,造成CPU时期被花在非消费性上班上。

在Go里,渣滓搜集器并不是齐全"stop-the-world",而是与运行程序并行口头其大部分上班(例如标志堆)。

但是,渣滓搜集器的操作依然有一些限度,并且会在一个周期内屡次齐全中止上班代码的口头,想要了解更多可以阅读源码[5]。

如何治理渣滓搜集器

在Go中可以经过某些参数治理渣滓搜集器:GOGC环境变量或runtime/debug包中的等效函数SetGCPercent。

GOGC参数确定将触发渣滓搜集的新未调配堆内存相关于生动内存的百分比。

GOGC的自动值是100,象征着当新内存到达生动堆内存的100%时将触发渣滓搜集。

当新堆占用生动堆的100%时,将运转渣滓搜集器

咱们以示例程序为例,经过gotooltrace跟踪堆大小的变动,咱们用Go1.20.1版原本运转程序。

在本例中,performMemoryIntensiveTask函数经常使用了在堆中调配的少量内存。这个函数启动一个队列大小为NumWorker的上班池,义务数量等于NumTasks。

packagemainimport("fmt""os""runtime/debug""runtime/trace""sync")const(NumWorkers=4//Numberofworkers.NumTasks=500//Numberoftasks.MemoryIntense=10000//Sizeofmemory-intensivetask(numberofelements).)funcmain(){//Writetothetracefile.f,_:=os.Create("trace.out")trace.Start(f)defertrace.Stop()//Setthetargetpercentageforthegarbagecollector.Defaultis100%.debug.SetGCPercent(100)//Taskqueueandresultqueue.taskQueue:=make(chanint,NumTasks)resultQueue:=make(chanint,NumTasks)//Startworkers.varwgsync.WaitGroupwg.Add(NumWorkers)fori:=0;i<NumWorkers;i++{goworker(taskQueue,resultQueue,&wg)}//Sendtaskstothequeue.fori:=0;i<NumTasks;i++{taskQueue<-i}close(taskQueue)//Retrieveresultsfromthequeue.gofunc(){wg.Wait()close(resultQueue)}()//Processtheresults.forresult:=rangeresultQueue{fmt.Println("Result:",result)}fmt.Println("Done!")}//Workerfunction.funcworker(tasks<-chanint,resultschan<-int,wg*sync.WaitGroup){deferwg.Done()fortask:=rangetasks{result:=performMemoryIntensiveTask(task)results<-result}}//performMemoryIntensiveTaskisamemory-intensivefunction.funcperformMemoryIntensiveTask(taskint)int{//Createalarge-sizedslice.>//Writingtothetracefile.f,_:=os.Create("trace.out")trace.Start(f)defertrace.Stop()

经过gotooltrace,可以观察堆大小的变动,并剖析程序中渣滓搜集器的行为。

请留意,gotooltrace的准确细节和性能或许因go版本不同而有所差异,因此倡导参考官网文档,以失掉无关其在特定go版本中经常使用的具体信息。

GOGC的自动值

GOGC参数可以经常使用runtime/debug包中的debug.SetGCPercent启动设置,GOGC自动设置为100%。

用上方命令运转程序:

gorunmain.go

程序口头后,将会创立trace.out文件,可以经常使用gotool工具对其启动剖析。要做到这一点,口头命令:

gotooltracetrace.out

而后可以经过关上web阅读器并访问来检查基于web的跟踪检查器。

在"STATS"选项卡中,可以看到"Heap"字段,显示了在运行程序口头时期堆大小的变动状况,图中白色区域示意堆占用的内存。

在"PROCS"选项卡中,"GC"(渣滓搜集器)字段显示的蓝色列示意触发渣滓搜集器的时辰。

一旦新堆的大小到达优惠堆大小的100%,就会触发渣滓搜集。例如,假设生动堆大小为10MB,则当以后堆大小到达10MB时将触发渣滓搜集。

跟踪一切渣滓搜集调用使咱们能够确定渣滓搜集器处于优惠形态的总时期。

GOGC=100时的GC调用次数

示例中,当GOGC值为100时,将调用渣滓搜集器16次,总口头时期为14ms。

更频繁的调用GC

假设咱们将debug.SetGCPercent(10)设置为10%后运转代码,将观察到渣滓搜集器调用的频率更高。如今,假设以后堆大小到达生动堆大小的10%时,将触发渣滓搜集。

换句话说,假设生动堆大小为10MB,则以后堆大小到达1MB时就将触发渣滓搜集。

在本例中,渣滓搜集器被调用了38次,总渣滓搜集时期为28ms。

GOGC=10时的GC调用次数

可以观察到,将GOGC设置为低于100%的值可以参与渣滓搜集的频率,或许造成CPU经常使用率参与并降低程序性能。

更少的调用GC

假设运转相反程序,但将debug.SetGCPercent(1000)设置为1000%,咱们将失掉以下结果:

可以看到,以后堆的大小不时在增长,直抵到达生动堆大小的1000%。换句话说,假设生动堆大小为10MB,则以后堆大小到达100MB时将触发渣滓搜集。

GOGC=1000时的GC调用次数

在以后状况下,渣滓搜集器被调用一次性并口头2毫秒。

封锁GC

还可以经过设置GOGC=off或调用debug.SetGCPercent(-1)来禁用渣滓搜集。

上方是禁用渣滓搜集器而不设置GOMEMLIMIT时堆的行为:

当GC=off时,堆大小不时增长。

可以看到,在封锁GC后,运行程序的堆大小不时在增长,直到程序口头为止。

堆占用多少内存?

在生动堆的实践内存调配中,通常不像咱们在trace中看到的那样活期和可预测的上班。

生动堆随着每个渣滓搜集周期灵活变动,并且在某些条件下,其相对值或许出现峰值。

例如,假设由于多个并行义务的堆叠,生动堆的大小可以增长到800MB,那么只要在以后堆大小到达1.6GB时才会触发渣滓搜集。

现代开发通常在具备内存经常使用限度的容器中运转运行。因此,假设容器将内存限度设置为1GB,并且总堆大小参与到1.6GB,则容器将失效,并出现OOM(outofmemory)失误。

让咱们模拟一下这种状况。例如,咱们在内存限度为10MB的容器中运转程序(仅用于测试目的)。file:

FROMgolang:latestasbuilderWORKDIR/srcCOPY..RUNgoenv-wGO111MODULE=onRUNgomodvendorRUNCGO_ENABLED=0GOOS=gobuild-mod=vendor-a-installsuffixcgo-o./cmd/FROMgolang:latestWORKDIR/root/COPY--from=builder/src/app.EXPOSE8080CMD["./app"]

Docker-compose形容:

version:'3'services:my-app:build:context:.dockerfile:Dockerfileports:-8080:8080deploy:resources:limits:memory:10M

让咱们经常使用前面设置GOGC=1000%的代码启动容器。

可以经常使用以下命令运转容器:

docker-composebuilddocker-composeup

几秒钟后,容器将解体,并发生与OOM相对应的失误。

exitedwithcode137

这种状况十分令人不快:GOGC只控制新堆的相对值,而容器有相对限度。

如何防止OOM?

从1.19版本开局,在GOMEMLIMIT选项的协助下,Golang引入了一个名为"软内存治理"的个性,runtime/debug包中名为SetMemoryLimit的相似函数(可以阅读48409-soft-memory-limit.md[6]了解无关此选项的一些幽默的设计细节)提供了相反的性能。

GOMEMLIMIT环境变量设置Go运转时可以经常使用的总体内存限度,例如:GOMEMLIMIT=8MiB。要设置内存值,须要经常使用大小后缀,在本例中为8MB。

让咱们启动将GOMEMLIMIT境变量设置为8MiB的容器。为此,咱们将环境变量参与到docker-compose文件中:

version:'3'services:my-app:environment:GOMEMLIMIT:"8MiB"build:context:.dockerfile:Dockerfileports:-8080:8080deploy:resources:limits:memory:10M

如今,当启动容器时,程序运转没有任何失误。该机制是专门为处置OOM疑问而设计的。

这是由于启用GOMEMLIMIT=8MiB后,会活期调用渣滓搜集器,并将堆大小坚持在必定限度内,结果就是会频繁调用渣滓搜集器以防止内存过载。

运转渣滓搜集器以使堆大小坚持在必定的限度内。

老本是什么?

GOMEMLIMIT是强有力的工具,但也或许大失所望。

在上方的堆跟踪图中可以看到这种场景的一个示例。

当总内存大小由于生动堆或耐久程序走漏的增长而凑近GOMEMLIMIT时,将开局依据该限度不时调用渣滓搜集器。

由于频繁调用渣滓搜集器,运行程序的运转时或许会有限参与,从而消耗运行程序的CPU时期。

这种行为被称为死亡螺旋[7],或许造成运行程序性能降低,与OOM失误不同,这种疑问很难检测和修复。

这正是GOMEMLIMIT机制造为软限度起作用的要素。

Go不能100%保障GOMEMLIMIT指定的内存限度会被严厉口头,而是会准许经常使用超出限度的内存,并防止频繁调用渣滓搜集器的状况。

为了成功这一点,须要对CPU经常使用设置限度。目前,这个限度被设置为一切处置器时期的50%,CPU窗口为2*GOMAXPROCS秒。

这就是为什么咱们不能齐全防止OOM失误,而是会将其推早退很久以后出现。

在哪里运行GOMEMLIMIT和GOGC

假设自动渣滓搜集器设置在大少数状况下是足够的,那么带有GOMEMLIMIT的软内存治理机制可以使咱们防止不欢快的状况。

经常使用GOMEMLIMIT内存限度或许有用的例子:

防止经常使用GOMEMLIMIT的状况:

如上所述,经过深思熟虑的方法,咱们可以治理程序中的微调设置,例如渣滓搜集器和GOMEMLIMIT。但是,细心思考运行这些设置的战略无疑十分关键。

参考资料


如何优化Go语言的性能?

程序员5个重要问题时刻关注

1.规范

为了促进代码库的一致性、清晰性和可维护性,Go 团队建立和遵循编码风格、命名、格式化等开发方面的规范非常重要。

一些可能采用的规范包括、编码风格、Go 中的命名规范、遵循命名规范可以极大地提高代码库的可读性和可维护性、格式化规范、文档规范、测试和调试规范。在大型系统中,代码库可能会变得复杂并涉及许多不同的组件和开发人员,因此一致性尤为重要。但是,也需要在一致性和灵活性之间取得平衡,因为在特定情况下,可能需要偏离已经建立的规范,以实现特定目标或解决特定问题。

2.编写清晰、高效、易维护的 Go 代码

编写清晰、高效、易维护的 Go 代码的最佳实践非常重要,原因有很多(如清晰性、可维护性、效率、协作等)。

随着代码库的增长和变得越来越复杂,确保代码编写良好和易于维护变得尤为重要,因为糟糕的代码可能会对代码库的结构和整体质量产生负面影响。

以下是一些最佳实践:遵循已建立的编码风格指南。

编写测试:验证代码的正确性并确保其可靠性·使用 gofmt: 根据一组指南自动格式化 Go代码。

标识符名称:使用描述性的完整单词作为标识符名称,而不是缩写。

3.优化Go程席性能的技巧

总体而言,优化Go程序的性能非常重要,因为它可以帮助确保程序的运行顺畅、高效和可扩展,并提供良好的用户体验。

内存优化与渣滓搜集

通过使用优化Go程序性能的技巧,开发人员可以确保他们的程序快速、高效和可扩展,满足用户的需求以下是一些优化Go代码的基本方法,或者你可以考虑以下选项:

避免不必要的分配

使用sync包来提高并发性能

使用性能分析工具来识别性能瓶颈

使用strings/bytes包进行字符串/字节操作

4.有效地在Go中使用并发和并行

Go是一种设计时考虑了并发性的编程语言。想象一下你的代码在一个孤独的CPU核心上运行。现在,想象它在多个核心上同时运行,就像一台运转良好的机器。使用并发和并行可以让你的代码表现得像一个老板,而不是一个“慢吞吞”的人。你可以采用以下一些技巧:

根据需要使用适当的goroutine模式,例如workerpools、pipeline、fan-out或fan-in 。

使用sync包进行同步:sync包提供了改进Go代码e并发性能的工具,例如互斥锁和原子操作。

不要过度使用并发:虽然并发和并行可以是强大的工具,但它们也可能会增加代码的复杂性。

5.与分布式系统有效地工作

有效地处理分布式系统对于提高代码或业务的可维护性、可扩展性和可靠性非常重要。

对干需要24/7可用或对业务运营至关重要的程序来说,这一点尤为重要。

分布式系统旨在进行水平扩展,这意味着它们可以通过向系统添加更多节点来处理增加的工作负载。以下是处理分布式系统的一些提示:

设计容错性:分布式系统旨在具有容错能力,这意味着即使一个或多个节点失败,它们仍然可以继续运行。

使用适当的数据存储:分布式系统通常依赖干数据存储来运行,因此选择适当的数据存储机制非常重要。

Java程序性能优化-性能的参考指标

性能的参考指标

为了能够科学地进行性能分析 对性能指标进行定量评测是非常重要的 目前 一些可以用于定量评测的性能指标有

执行时间 一段代码从开始运行到运行结束 所使用的时间

CPU时间 函数或者线程占用CPU的时间

内存分配 程序在运行时占用的内存空间

磁盘吞吐量 描述I/O的使用情况

网络吞吐量 描述网络的使用情况

响应时间 系统对某用户行为或者事件做出响应的时间 响应时间越短 性能越好

返回目录 Java程序性能优化 让你的Java程序更快 更稳定

编辑推荐

Visual C++音频/视频技术开发与实战

Oracle索引技术

lishixinzhi/Article/program/Java/gj//

免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。

标签: Go

“内存优化与渣滓搜集-深化探求优化程序性能的最佳通常-Go (内存优化与渣子有关吗)” 的相关文章

Go-言语中并发的弱小效劳 (go语言语法很奇怪啊)

Go-言语中并发的弱小效劳 (go语言语法很奇怪啊)

施展效率和照应才干 并发是现代软件开发中的一个基本概念,它使程序能够同时口头多个义务,提高效率和照应才干。在本文中,咱们将讨论并发在现代软件开发中的关键性,并深化了解Go处置并发义务的共同...

etcd-的依赖问题最终得到解决-Go (etcd的英文全称)

etcd-的依赖问题最终得到解决-Go (etcd的英文全称)

几年前,我经常接触到一组微服务相关组件:gRPC、gRPC 网关、etcd、Protobuf 和 protoc-gen-go。一开始,它们都能很好地协同工作,并且随着新版本的发布而持续更新。...

Go-内存调配优化-在结构体中充沛应用内存 (go 内存)

Go-内存调配优化-在结构体中充沛应用内存 (go 内存)

在经常使用Golang启动内存调配时,咱们须要遵照一系列规定。在深化了解这些规定之前,咱们须要先了解变量的对齐形式。 Golang的unsafe包中有一个函数Alignof,签名如下: f...

的原因-Go-Map-语言不支持并发读写 (的原因英语)

的原因-Go-Map-语言不支持并发读写 (的原因英语)

在Go语言的设计中,为了防止数据竞态,不同于一些其他语言,map并没有提供内置的锁机制。这样设计的目的是为了鼓励开发者使用更加精细的同步措施,以适应不同的并发场景。 Map的数据结构...

内存效率-多用途-语言中使用切片代替数组的优点-动态大小-Go (内存使用效率)

内存效率-多用途-语言中使用切片代替数组的优点-动态大小-Go (内存使用效率)

引言 在 Go 语言中,数组是一种固定长度的数据结构,而切片则是一种可变长度的数据结构。虽然数组和切片都可以存储相同类型的数据元素,但切片在使用上有其独到的优势,本文将通过介绍切片的特性来解释为...

14条超乎想象的Go接口最佳实践

14条超乎想象的Go接口最佳实践

近年来,越来越多的开发者开始关注 Go 语言,它以其高效、简洁和高并发性而闻名。在 Go 语言中,接口是一个非常强大的特性,它可以帮助我们定义和实现不同的行为。本文将介绍 Go 语言中接口设计的一...

并非你构想得那么便捷-Go中的switch的六种用法 (并非你构想得到的成语)

并非你构想得那么便捷-Go中的switch的六种用法 (并非你构想得到的成语)

Go以其繁复而著称,但并不是每团体都相熟这种言语中switch语句的多样性。首先,假设你对Go的switch语句还不相熟,它或许与其余言语相比有些不同。 上方是一个便捷的示例来展现它是什么...