刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

    • go 泛型
    • go条件编译
    • 分布式从ACID、CAP、BASE的理论推进
    • go链接参数 ldflags
    • TCP网络连接以及TIME_WAIT的意义
    • Go异常处理
    • Go性能调优 pprof
    • Go语言设计模式
    • Go 切片的截取
    • Go runtime详解
    • go执行外部命令
    • 标准库container三剑客:head、list、ring
    • go与http代理
    • Go内存管理
    • Go垃圾回收机制
    • Go语言中的并发编程
    • Go协程调度原理及GPM模型
    • Go中逃逸现象, 变量+堆栈
    • Go面向对象的思维理解interface
    • Go中的Defer
    • Go和Python中的深浅拷贝
    • Go语言内存对齐
    • 流和IO多路复用
    • 单点Server的N种并发模型汇总
    • 控制goroutine的数量
    • 配置管理库—Viper
    • 高性能日志库zap
    • Go中的Mutex和RWMutex.md
    • sqlx的使用
    • 分布式id 库snowflake和sonyflake
    • sync.Pool 复用对象
    • sync.Once 单例模式
    • sync.Cond 条件变量
    • unsafe.Pointer 和 uintptr
    • go 信号量
    • go语言代码优化技巧
      • 1. sync.Pool
      • 2. string相关
        • 2.1 字符串拼接 strings.Builder
        • 2.2 字符串截取 strings.Repeat
      • 3. 使用协程池
      • 4. for 和 range 选择
      • 5. 减小锁的资源消耗
      • 6. 不要使用反射, 除非忍不住
      • 7. 结构体声明考虑内存对齐
      • 8. slice 相关
        • 8.1 创建slice和map声明cap
        • 8.2 slice 的截取[::]和拷贝
      • 9. 空占位符使用struct{}
      • 10. 考虑内存逃逸
      • 11. 返回值VS返回指针
    • go 接口型函数
    • 位运算
    • cgo教程
    • go调用lib和so动态库
  • go语言实现原理

  • gin框架

  • gorm

  • go测试

  • Go语言
  • go语言进阶
bigox
2022-08-02
目录

go语言代码优化技巧

# 1. sync.Pool

  • sync.Pool 除了最常见的池化提升性能的思路,最重要的是减少 GC 。

  • 常用于一些对象实例创建昂贵的场景。注意,Pool 是 Goroutine 并发安全的。

  • 可以作为保存临时取还对象的一个“池子”。

  • 特点

    1. Goroutine 并发安全的
    2. 存储的都是临时对象
    3. 自动移除, 清理完全是由runtime控制的, 随时都可能被无通知清除
    4. 当这个对象的引用只有sync.Pool持有时,这个对象内存会被释放
    5. 目的就是缓存并重用对象,减少GC的压力
    6. 自动扩容、缩容
    7. 不能对 Pool.Get 出来的对象做预判,有可能是新的(新分配的),有可能是旧的(之前人用过,然后 Put 进去的)
    8. 当用完一个从 Pool 取出的实例时候,一定要记得调用 Put,否则 Pool 无法复用这个实例,通常这个用 defer 完成;
  • 应用场景

    1. 当多个 goroutine 都需要创建同⼀个对象的时候,如果 goroutine 数过多,导致对象的创建数⽬剧增,进⽽导致 GC 压⼒增大。形成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒降低-并发更⼤”这样的恶性循环。
    2. 对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。
    3. 标准库中 encoding/json 也用到了 sync.Pool 来提升性能。
    4. 著名的 gin 框架,对 context 取用也到了 sync.Pool。
    5. fasthttp 大量使用sync.Pool

# 2. string相关

# 2.1 字符串拼接 strings.Builder

官网说: A Builder is used to efficiently build a string using Write methods. It minimizes memory copying.

  • 字符串拼接方法

    1. 使用 +
    2. 使用fmt.Sprintf
    3. 使用strings.Builder
    4. 使用strings.Buffer
    5. 使用bytes.Buffer
  • 从基准测试的结果来看,使用 + 和 fmt.Sprintf 的效率是最低的,和其余的方式相比,性能相差约 1000 倍,而且消耗了超过 1000 倍的内存。当然 fmt.Sprintf 通常是用来格式化字符串的,一般不会用来拼接字符串。

  • strings.Builder、bytes.Buffer 和 []byte 的性能差距不大,而且消耗的内存也十分接近,性能最好且消耗内存最小的是 preByteConcat,这种方式预分配了内存,在字符串拼接的过程中,不需要进行字符串的拷贝,也不需要分配新的内存,因此性能最好,且内存消耗最小。

  • string.Builder 和 +

    • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:

      10 + 2 * 10 + 3 * 10 + ... + 10000 * 10 byte = 500 MB 
      
      1
    • 而 strings.Builder,bytes.Buffer,包括切片 []byte 的内存是以倍数申请的。例如,初始大小为 0,当第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte 的内存(恰好大于 10 byte 的 2 的指数),第二次写入 10 byte 时,内存不够,则申请 32 byte 的内存,第三次写入内存足够,则不申请新的,以此类推。在实际过程中,超过一定大小,比如 2048 byte 后,申请策略上会有些许调整。

      • 2048 以前按倍数申请,2048 之后,以 640 递增,最后一次递增 24576 到 122880。总共申请的内存大小约 0.52 MB,约为上一种方式的千分之一。
  • strings.Builder 和 bytes.Buffer

    • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,但 strings.Builder 性能比 bytes.Buffer 略快约 10% 。一个比较重要的区别在于,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回了回来。

      • bytes.Buffer
      // To build strings more efficiently, see the strings.Builder type.
      func (b *Buffer) String() string {
      	if b == nil {
      		// Special case, useful in debugging.
      		return "<nil>"
      	}
      	return string(b.buf[b.off:])
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      • strings.Builder
      // String returns the accumulated string.
      func (b *Builder) String() string {
      	return *(*string)(unsafe.Pointer(&b.buf))
      }
      
      1
      2
      3
      4

# 2.2 字符串截取 strings.Repeat

一个子字符串表达式的结果(子)字符串和基础字符共享一个承载底层字节序列的内存块。不仅节省内存,而且还减少了CPU消耗。 但是有时候它会造成暂时性的内存泄露。

  • demo

    var s0 string // 一个包级变量
    
    // 一个演示目的函数。
    func f(s1 string) {
    	s0 = s1[:50]
    	// 目前,s0和s1共享着承载它们的字节序列的同一个内存块。
    	// 虽然s1到这里已经不再被使用了,但是s0仍然在使用中,
    	// 所以它们共享的内存块将不会被回收。虽然此内存块中
    	// 只有50字节被真正使用,而其它字节却无法再被使用。
    }
    
    func demo() {
    	s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
    	f(s)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 解决办法

    1. 将子字符串表达式的结果转换为一个字节切片,然后再转换回来。此种防止临时性内存泄露的方法不是很高效,因为在此过程中底层的字节序列被复制了两次,其中一次是不必要的。

      func f(s1 string) {
      	s0 = string([]byte(s1[:50]))
      }
      
      1
      2
      3
  1. [推荐]使用strings.Builder类型来防止一次不必要的复制。

    import "strings"
    
    func f(s1 string) {
        var b strings.Builder
        b.Grow(50)
        b.WriteString(s1[:50])
        s0 = b.String()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. 使用strings.Repeat, 此方法底层也是strings.Builder的封装

# 3. 使用协程池

  • 协程池作用

    1. 可以限制goroutine数量,避免无限制的增长。
    2. 减少栈扩容的次数。
    3. 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显。)
  • 推荐第三方库 ants (opens new window)

  • go对goroutine有一定的复用能力。所以要根据场景选择是否使用协程池,不恰当的场景不仅得不到收益,反而增加系统复杂性。

# 4. for 和 range 选择

  • range 在迭代过程中返回的是迭代值的拷贝
  • 如果每次迭代的元素的内存占用很低,那么 for 和 range 的性能几乎是一样,例如 []int。
  • 如果迭代的元素内存占用较高,例如一个包含很多属性的 struct 结构体,那么 for 的性能将显著地高于 range,有时候甚至会有上千倍的性能差异。对于这种场景,建议使用 for,如果使用 range,建议只迭代下标,通过下标访问迭代值,这种使用方式和 for 就没有区别了。
  • 如果想使用 range 同时迭代下标和值,则需要将切片/数组的元素改为指针,才能不影响性能。
  • 尽量使用for,而不是range

# 5. 减小锁的资源消耗

  • 对临界区加锁比较常见, 性能损耗也是非常严重的

  • 标准库中sync.map针对读操作的优化消除了rwlock,是一个标准的案例. 用原子操作代替互斥锁也是一种经典的lock-free技巧。

# 6. 不要使用反射, 除非忍不住

  • 反射可以帮助抽象和简化代码,提高开发效率。但是go语言反射效率不高.
  • 反射创建对象效率相差不大, 但是动态修改字段的值效率极低!

# 7. 结构体声明考虑内存对齐

  • CPU 访问内存时并不是逐个字节访问,而是以字长(word size)为单位访问,例如 32位的CPU 字长是4字节,64位的是8字节。如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。
  • 在实际开发中,我们可以通过调整变量位置,优化内存占用(一般按照变量内存大小顺序排列,整体占用内存更小)

# 8. slice 相关

# 8.1 创建slice和map声明cap

  • 尽可能的声明容量
  • 使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。
  • 扩容容量的选择遵循以下规则:
    • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
    • 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
  • 扩容消耗资源

# 8.2 slice 的截取[::]和拷贝

slice 使用方式不对容易造成内存的伪泄露、数据篡改等问题

切片截取子切片时,会造成临时内存泄露, 主要原因有两个

  1. 切片截取时,新旧切片会共用一个底层数组
  2. 切片的底层结构体指向数组的指针只是一个头指针
  • demo

    package main
    
    import "fmt"
    
    func main() {
    	a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    	c := a[1:2]
    	fmt.Println(len(c), cap(c))     // 1,9   c的数组头指针执行索引1,所以容量为9
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 解决办法

    1. 使用copy,不过要注意copy时的长度和容量问题
    2. 使用slice [1:2:3] 两个冒号语法截取:[startIndex:endIndex:max], 其中 max 的值一定要大于 endIndex
    • 新切片的容量就是max - startIndex,
    • 实际引用的数组时从数组startIndex索引开始到max索引为止,但不包括max索引处的元素,
    • 新切片的长度就是endIndex - startIndex

# 9. 空占位符使用struct{}

  • 空结构体在内存中不占用空间

  • 用法

    1. 与map结合实现set
    • Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。事实上,对于集合来说,只需要 map 的键,而不需要值。即使是将值设置为 bool 类型,也会多占据 1 个字节,那假设 map 中有一百万条数据,就会浪费 1MB 的空间
    • 将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。
    1. 制造伪迭代器

      for range make([]struct{}, 100) {
        fmt.Println("迭代器")
      }
      
      1
      2
      3
    2. 不发送数据的channel

      func worker(ch chan struct{}) {
      	<-ch
      	fmt.Println("do something")
      	close(ch)
      }
      
      func main() {
      	ch := make(chan struct{})
      	go worker(ch)
      	ch <- struct{}{}
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

# 10. 考虑内存逃逸

  • 控制变量不发生逃逸,将其控制在栈上,减少堆变量的分配,降低GC成本,提高程序性能。

  • 变量逃逸一般发生在如下几种情况:

    • 变量较大(栈空间不足)

    • 变量大小不确定(如slice长度或容量不定)

    • 返回地址

    • 返回引用(引用变量的底层是指针)

    • 返回值类型不确定(不能确定大小)

    • 闭包

# 11. 返回值VS返回指针

  • 值传递会拷贝整个对象,而指针传递只会拷贝地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,返回指针导致的GC开销可能会严重影响性能。
  • 一般情况下,对于需要修改原对象,或占用内存比较大的对象,返回指针。对于只读或占用内存较小的对象,返回值能够获得更好的性能。

持续完善...

#Go
上次更新: 2023/05/04, 11:49:33
go 信号量
go 接口型函数

← go 信号量 go 接口型函数→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式