Golang 内存管理简单技巧详解

目录
  • 引言
  • 预先分配切片
  • 结构中的顺序字段
  • 使用 map[string]struct{} 而不是 map[string]bool

引言

除非您正在对服务进行原型设计,否则您可能会关心应用程序的内存使用情况。内存占用更小,基础设施成本降低,扩展变得更容易/延迟。

尽管 Go 以不消耗大量内存而闻名,但仍有一些方法可以进一步减少消耗。其中一些需要大量重构,但很多都很容易做到。

预先分配切片

数组是具有连续内存的相同类型的集合。数组类型定义指定长度和元素类型。数组的主要问题是它们的大小是固定的——它们不能调整大小,因为数组的长度是它们类型的一部分。

与数组类型不同,切片类型没有指定长度。切片的声明方式与数组相同,但没有元素计数。

切片是数组的包装器,它们不拥有任何数据——它们是对数组的引用。它们由指向数组的指针、段的长度及其容量(底层数组中的元素数)组成。

当您追加到一个没有新值容量的切片时 - 会创建一个具有更大容量的新数组,并将当前数组中的值复制到新数组中。这会导致不必要的分配和 CPU 周期。

为了更好地理解这一点,让我们看一下以下代码段:

func main() {
    var ints []int
    for i := 0; i < 5; i++ {
        ints = append(ints, i)
        fmt.Printf("Address: %p, Length: %d, Capacity: %d, Values: %v\n",
            ints, len(ints), cap(ints), ints)
    }
}

输出如下:

Address: 0xc0000160c8, Length: 1, Capacity: 1, Values: [0]
Address: 0xc0000160f0, Length: 2, Capacity: 2, Values: [0 1]
Address: 0xc00001e080, Length: 3, Capacity: 4, Values: [0 1 2]
Address: 0xc00001e080, Length: 4, Capacity: 4, Values: [0 1 2 3]
Address: 0xc00001a140, Length: 5, Capacity: 8, Values: [0 1 2 3 4]

查看输出,我们可以得出结论,无论何时必须增加容量(增加 2 倍),都必须创建一个新的底层数组(新的内存地址)并将值复制到新数组中。

有趣的事实是,容量增长的因素曾经是容量 <1024 的 2 倍,以及 >= 1024 的 1.25 倍。从 Go 1.18 开始,这已经变得更加线性

name               time/op
Append-10          3.81ns ± 0%
PreallocAssign-10  0.41ns ± 0%
name               alloc/op
Append-10           45.0B ± 0%
PreallocAssign-10   8.00B ± 0%
name               allocs/op
Append-10            0.00
PreallocAssign-10    0.00

查看上述基准,我们可以得出结论,将值分配给预分配的切片和将值附加到切片之间存在很大差异。

两个 linter 有助于预分配切片:

  • prealloc: 一种静态分析工具,用于查找可能被预分配的切片声明。
  • makezero: 一种静态分析工具,用于查找未以零长度初始化且稍后与 append 一起使用的切片声明。

结构中的顺序字段

您之前可能没有想到这一点,但结构中字段的顺序对内存消耗很重要。

以下面的结构为例:

type Post struct {
    IsDraft     bool      // 1 byte
    Title       string    // 16 bytes
    ID          int64     // 8 bytes
    Description string    // 16 bytes
    IsDeleted   bool      // 1 byte
    Author      string    // 16 bytes
    CreatedAt   time.Time // 24 bytes
}
func main(){
    p := Post{}
    fmt.Println(unsafe.Sizeof(p))
}

上述函数的输出为 96(字节),而所有字段相加为 82 字节。额外的 14 个字节来自哪里?

现代 64 位 CPU 以 64 位(8 字节)块的形式获取数据。如果我们有一个较旧的 32 位 CPU,它将执行 32 位(4 字节)的块。

第一个周期占用 8 个字节,IsDraft 字段占用 1 个字节,并有 7 个未使用字节。它不能占据一个字段的“一半”。

第二和第三个循环取 Title 字符串,第四个循环取 ID,依此类推。再次使用 IsDeleted 字段,它需要 1 个字节并有 7 个未使用的字节。

真正重要的是按字段的大小从上到下对字段进行排序。对上述结构进行排序,大小减少到 88 个字节。最后两个字段 IsDraftIsDeleted 被放在同一个块中,从而将未使用的字节数从 14 (2x7) 减少到 6 (1 x 6),在此过程中节省了 8 个字节。

type Post struct {
    CreatedAt   time.Time // 24 bytes
    Title       string    // 16 bytes
    Description string    // 16 bytes
    Author      string    // 16 bytes
    ID          int64     // 8 bytes
    IsDeleted   bool      // 1 byte
}
func main(){
    p := Post{}
    fmt.Println(unsafe.Sizeof(p))
}

在 64 位架构上占用 <8 字节的 Go 类型:

  • bool:1 个字节
  • int8/uint8:1 个字节
  • int16/uint16:2 个字节
  • int32/uint32/rune:4 字节
  • float32:4 字节
  • byte:1个字节

无需手动检查结构并按大小对其进行排序,而是使用 linter 找到这些结构并(用于)报告“正确”排序。

  • maligned: 不推荐使用的 linter,用于报告未对齐的结构并打印出正确排序的字段。它在一年前被弃用,但您仍然可以安装旧版本并使用它。
  • govet/fieldalignment: 作为 gotools 和 govet linter 的一部分,fieldalignment 打印出未对齐的结构和结构的当前/理想大小。

要安装和运行 fieldalignment:

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment -fix <package_path>

在上面的代码中使用 govet/fieldalignment:

fieldalignment: struct of size 96 could be 88 (govet)

使用 map[string]struct{} 而不是 map[string]bool

Go 没有内置集合,通常使用 map[string]bool{} 来表示集合。尽管它更具可读性,这一点非常重要,但将其作为一个集合使用是错误的,因为它有两种状态(假/真),并且与空结构相比使用了额外的内存。

空结构体 (struct{}) 是没有额外字段的结构体类型,占用零字节存储空间。

我不建议这样做,除非您的 map/set 包含大量值并且您需要获得额外的内存或者您正在为低内存平台进行开发。

使用 100 000 000 次写入地图的极端示例:

func BenchmarkBool(b *testing.B) {
    m := make(map[uint]bool)
    for i := uint(0); i < 100_000_000; i++ {
        m[i] = true
    }
}
func BenchmarkEmptyStruct(b *testing.B) {
    m := make(map[uint]struct{})
    for i := uint(0); i < 100_000_000; i++ {
        m[i] = struct{}{}
    }
}

得到以下结果,在整个运行过程中非常一致:

name            time/op
Bool          12.4s ± 0%
EmptyStruct   12.0s ± 0%
name            alloc/op
Bool         3.78GB ± 0%
EmptyStruct  3.43GB ± 0%
name            allocs/op
Bool          3.91M ± 0%
EmptyStruct   3.90M ± 0%

使用这些数字,我们可以得出结论,使用空结构映射的写入速度提高了 3.2%,分配的内存减少了 10%。

此外,使用 map[type]struct{} 是实现集合的正确解决方法,因为每个键都有一个值。使用 map[type]bool,每个键都有两个可能的值,这不是一个集合,如果目标是创建一个集合,则可能会被误用。

然而,可读性大多数时候比(可忽略的)内存改进更重要。与空结构体相比,使用布尔值更容易掌握查找:

m := make(map[string]bool{})
if m["key"]{
 // Do something
}
v := make(map[string]struct{}{})
if _, ok := v["key"]; ok{
    // Do something
}

参考链接:Easy memory-saving tricks in Go | Emir Ribic (ribice.ba)

以上就是Golang 内存管理简单技巧详解的详细内容,更多关于Golang 内存管理的资料请关注我们其它相关文章!

(0)

相关推荐

  • golang切片内存应用技巧详解

    在 Go 语言中切片是使用非常频繁的一种聚合类型,它代表变长的序列,底层引用一个数组对象.一个切片由三个部分构成:指针.长度和容量.指针指向该切片自己第一个元素对应的底层数组元素的内存地址. 切片的类型声明如下: type slice struct { array unsafe.Pointer len int cap int } 多个切片之间可以共享底层数组的数据,并且引用的数组区间可能重叠.利用切片 的这个特性我们可以在原有内存空间中对切片进行反转.筛选和去重等操作,这样就不用声明一个指向新内

  • golang内存对齐的概念及案例详解

    什么是内存对齐 为保证程序顺利高效的运行,编译器会把各种类型的数据安排到合适的地址,并占用合适的长度,这就是内存对齐. 每种类型的对齐值就是它的对齐边界,内存对齐要求数据存储地址以及占用的字节数都要是它的对齐边界的倍数.所以下述的int32要错开两个字节,从4开始存,却不能紧接着从2开始. 也可以这样解释: CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的.块大小成为memory access granularity(粒度). 如果

  • 浅谈Golang内存逃逸

    目录 1.什么是内存逃逸 2.什么是逃逸分析 3.小结 4.逃逸分析案例 1.函数返回局部指针变量 2.interface类型逃逸 1.interface产生逃逸 2.指向栈对象的指针不能在堆中 3.闭包产生逃逸 4. 变量大小不确定及栈空间不足引发逃逸 5.总结 1.什么是内存逃逸 在一段程序中,每一个函数都会有自己的内存区域分配自己的局部变量,返回值,这些内存会由编译器在栈中进行分配,每一个函数会分配一个栈帧,在函数运行结束后销毁,但是有些变量我们想在函数运行结束后仍然使用,就需要把这个变量

  • 解决golang内存溢出的方法

    最近在项目中出现golang内存溢出的问题,master刚开始运行时只有10多M,运行几天后,竟然达到了10多个G.而且到凌晨流量变少内存也没有明显降低,内存状态呈现一种很不健康的曲线. 像这种情况肯定是golang内存溢出了,为此我持续排查了两天,终于找到问题所在,特此记录下. 准备工作 一台较好的环境测试机,单台运行无污染. 压测工具,无论服务是http还是websocket服务,都必须准备好压测工具模拟最真实的用户场景. 将master引入net/http/pprof包,通过http访问获

  • golang数组内存分配原理

    目录 编译时数组类型解析 ArrayType types2.Array types.Array 编译时数组字面量初始化 编译时数组索引越界检查 运行时数组内存分配 总结 编译时数组类型解析 ArrayType 数组是内存中一片连续的区域,在声明时需要指定长度,数组的声明有如下三种方式,[...]的方式在编译时会自动推断长度. var arr1 [3]int var arr2 = [3]int{1,2,3} arr3 := [...]int{1,2,3} 在词法及语法解析时,上述三种方式声明的数组

  • Golang 内存管理简单技巧详解

    目录 引言 预先分配切片 结构中的顺序字段 使用 map[string]struct{} 而不是 map[string]bool 引言 除非您正在对服务进行原型设计,否则您可能会关心应用程序的内存使用情况.内存占用更小,基础设施成本降低,扩展变得更容易/延迟. 尽管 Go 以不消耗大量内存而闻名,但仍有一些方法可以进一步减少消耗.其中一些需要大量重构,但很多都很容易做到. 预先分配切片 数组是具有连续内存的相同类型的集合.数组类型定义指定长度和元素类型.数组的主要问题是它们的大小是固定的——它们

  • Performance 内存监控使用技巧详解

    目录 Performance 介绍 使⽤ 内存问题的具体体现 监控内存的⼏种⽅式 TimeLine Performance 介绍 为什么使⽤Performance呢?GC 的⽬的是为了实现内存空间的良性循环,⽽良性循环的基⽯是合理的使⽤内存空间. 由于 ECMAScript 并没有提供操作内存的 API,所以内存分配是否合理我们不可知.Performance 提供了多种⽅式,在程序运⾏时可以时时监控,确定内存分配是否合理. 使⽤ 具体步骤 打开浏览器输⼊⽬标⽹址 进⼊开发⼈员⼯具⾯板 开启录制功

  • GoLang内存泄漏原因排查详解

    目录 背景 临时性内存泄漏 通道理解 背景 Go 语言中有对应的Go 内存回收机制,在Go采用 并发三色标记清除  算法, 但是由于实际的过程中 发现会有一些内存泄漏的常见,内存泄漏 分为: 临时性 和 永久性内存泄漏. 初步排查过程中: 发现Linux使用top 发现内存随着时间会持续的增加没有稳定在一个合理值中. 在使用 pprof ,BBC 等 Go的内存泄漏工具进行排查 临时性内存泄漏 指的释放内存 不及时,对应的内存在更晚时候释放,这类问题主要是 string,slice 和底层的Bu

  • python内存管理机制原理详解

    python内存管理机制: 引用计数 垃圾回收 内存池 1. 引用计数 当一个python对象被引用时 其引用计数增加 1 ; 当其不再被变量引用时 引用计数减 1 ; 当对象引用计数等于 0 时, 对象被删除(引用计数是一种非常高效的内存管理机制) 2. 垃圾回收 垃圾回收机制: ① 引用计数 , ②标记清除 , ③分带回收 引用计数 : 引用计数也是一种垃圾收集机制, 而且也是一种最直观, 最简单的垃圾收集技术.当python某个对象的引用计数降为 0 时, 说明没有任何引用指向该对象, 该

  • 养成良好的C++编程习惯之内存管理的应用详解

    开篇导读    虽然本系列文章定位为科普读物,但本座相信它们不但适合新手们学习借鉴,同时也能引发老鸟们的反思与共鸣.欢迎大家提出宝贵的意见和反馈 ^_^ 在开篇讲述本章主要内容之前,本座首先用小小篇幅论述一下一种良好的工作习惯 -- 积累.提炼与求精.在工作和学习的过程中,不断把学到的知识通过有效的方式积累起来,形成自己的知识库,随着知识量的扩大,就会得到从量变到质变的提升.另外还要不断地对知识进行提炼,随着自己知识面的扩大以及水平的提升,你肯定会发现原有知识库存在着一些片面.局限.笨拙甚至错误

  • myeclipse8.5优化技巧详解

    还在为自己的配置低而抛弃MyEclipse8.5?还在为那低下的速度而苦恼吗?下面我们看看myeclipse8.5优化技巧的具体方法. 取消自动validation validation有一堆,什么xml.jsp.jsf.js等等,我们没有必要全部都去自动校验一下,只是需要的时候才会手工校验一下! 取消方法: windows–>perferences–>myeclipse–>validation 除开Manual下面的复选框全部选中之外,其他全部不选 手工验证方法: 在要验证的文件上,单

  • Linux共享内存实现机制的详解

    Linux共享内存实现机制的详解 内存共享: 两个不同进程A.B共享内存的意思是,同一块物理内存被映射到进程A.B各自的进程地址空间.进程A可以即时看到进程B对共享内存中数据的更新,反之亦然.由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以. 效率: 采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝.对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]: 一次从输入文件到

  • PHP内存溢出优化代码详解

    相信很多人做大批量数据导出和数据导入的时候,经常会遇到PHP内存溢出的问题,在解决了问题之后,总结了一些经验,整理成文章记录下. 优化点 1.优化SQL语句,避免慢查询,合理的建立索引,查询指定的字段,sql优化这块在此就不展开了. 2.查询的结果集为大对象时转数组处理,框架中一般有方法可以转,如Laravel中有toArray(),Yii2中有asArray(). 3.对于大数组进行数据切割处理,PHP函数有array_chunk().array_slice(). 4.对于大型的字符串和对象,

  • Docker Secret的管理和使用详解

    一.什么是Docker Secret (一)情景展现 我们知道有的service是需要设置密码的,比如mysql服务是需要设置密码的: version: '3' services: web: image: wordpress ports: - 8080:80 volumes: - ./www:/var/www/html environment: WORDPRESS_DB_NAME=wordpress WORDPRESS_DB_HOST: mysql WORDPRESS_DB_PASSWORD:

  • golang字符串本质与原理详解

    目录 一.字符串的本质 1.字符串的定义 2.字符串的长度 3.字符与符文 二.字符串的原理 1.字符串的解析 2.字符串的拼接 3.字符串的转换 总结 一.字符串的本质 1.字符串的定义 golang中的字符(character)串指的是所有8比特位字节字符串的集合,通常(非必须)是UTF-8 编码的文本. 字符串可以为空,但不能是nil. 字符串在编译时即确定了长度,值是不可变的. // go/src/builtin/builtin.go // string is the set of al

随机推荐