字节跳动青训营Day03 - Go语言性能优化建议

简介: 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率1.slice 预分配内存在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时原理测试程序:


在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率


1.slice 预分配内存


在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时


原理

测试程序:


package benchslice
// 没有指定切片的容量
func NoPreAlloc(size int) {
  data := make([]int, 0)
  for k := 0; k < size; k++ {
    data = append(data, k) // 切片追加信息
  }
}
// 指定了切片的容量
func PreAlloc(size int) {
  data := make([]int, 0, size)
  for k := 0; k < size; k++ {
    data = append(data, k)
  }
}
func GetLastBySlice(origin []int) []int {
  return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
  result := make([]int, 2)
  copy(result, origin[len(origin)-2:])
  return result
}


单元测试:


package benchslice
import (
  "math/rand"
  "runtime"
  "testing"
  "time"
)
func BenchmarkNoPreAlloc(b *testing.B) {
  for n := 0; n < b.N; n++ {
    NoPreAlloc(100)
  }
}
func BenchmarkPreAlloc(b *testing.B) {
  for n := 0; n < b.N; n++ {
    PreAlloc(100)
  }
}
func generateWithCap(n int) []int {
  rand.Seed(time.Now().UnixNano())
  nums := make([]int, 0, n)
  for i := 0; i < n; i++ {
    nums = append(nums, rand.Int())
  }
  return nums
}
func printMem(t *testing.T) {
  t.Helper()
  var rtm runtime.MemStats
  runtime.ReadMemStats(&rtm)
  t.Logf("%.2f MB", float64(rtm.Alloc)/1024./1024.)
}
func testGetLast(t *testing.T, f func([]int) []int) {
  result := make([][]int, 0)
  for k := 0; k < 100; k++ {
    origin := generateWithCap(128 * 1024) // 1M
    result = append(result, f(origin))
  }
  printMem(t)
  _ = result
}
func TestLastBySlice(t *testing.T) {
  testGetLast(t, GetLastBySlice)
}
func TestLastByCopy(t *testing.T) {
  testGetLast(t, GetLastByCopy)
}


测试结果:


goos: windows
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkNoPreAlloc
BenchmarkNoPreAlloc-8             860905              1165 ns/op
BenchmarkPreAlloc
BenchmarkPreAlloc-8              3290400               389.3 ns/op
PASS


可以看到,明显提供容量信息的切片拥有更好的性能🧇


切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度)

切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的

切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:

当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间

当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组

因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能

另一个陷阱:大内存得不到释放


在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组

因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放

推荐的做法,使用 copy 替代 re-slice


2.map 预分配内存


测试程序:


package benchmap
// 没有预分配内存
func NoPreAlloc(size int) {
  data := make(map[int]int)
  for i := 0; i < size; i++ {
    data[i] = 1
  }
}
// 进行预分配内存
func PreAlloc(size int) {
  data := make(map[int]int, size)
  for i := 0; i < size; i++ {
    data[i] = 1
  }
}


单元测试:


package benchmap
import "testing"
func BenchmarkNoPreAlloc(b *testing.B) {
  for n := 0; n < b.N; n++ {
    NoPreAlloc(1000)
  }
}
func BenchmarkPreAlloc(b *testing.B) {
  for n := 0; n < b.N; n++ {
    PreAlloc(1000)
  }
}


执行单元测试,可以看到结果如下:


goos: windows
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkNoPreAlloc
BenchmarkNoPreAlloc-8              10000            175810 ns/op
BenchmarkPreAlloc
BenchmarkPreAlloc-8                15987             76016 ns/op
PASS


原理


不断向 map 中添加元素的操作会触发 map 的扩容

根据实际需求提前预估好需要的空间

提前分配好空间可以减少内存拷贝和 Rehash 的消耗


3.使用 strings.Builder


常见的字符串拼接方式


+

strings.Builder

bytes.Buffer

strings.Builder 最快,bytes.Buffer 较快,+ 最慢


测试代码:


package benchstring
import (
  "bytes"
  "strings"
)
func Plus(n int, str string) string {
  s := ""
  for i := 0; i < n; i++ {
    s += str
  }
  return s
}
func StrBuilder(n int, str string) string {
  var builder strings.Builder
  for i := 0; i < n; i++ {
    builder.WriteString(str)
  }
  return builder.String()
}
func ByteBuffer(n int, str string) string {
  buf := new(bytes.Buffer)
  for i := 0; i < n; i++ {
    buf.WriteString(str)
  }
  return buf.String()
}
func PreStrBuilder(n int, str string) string {
  var builder strings.Builder
  builder.Grow(n * len(str))
  for i := 0; i < n; i++ {
    builder.WriteString(str)
  }
  return builder.String()
}
func PreByteBuffer(n int, str string) string {
  buf := new(bytes.Buffer)
  buf.Grow(n * len(str))
  for i := 0; i < n; i++ {
    buf.WriteString(str)
  }
  return buf.String()
}


单元测试:


package benchstring
import "testing"
func BenchmarkPlus(b *testing.B) {
  for n := 0; n < b.N; n++ {
    Plus(1000, "string")
  }
}
func BenchmarkStrBuilder(b *testing.B) {
  for n := 0; n < b.N; n++ {
    StrBuilder(1000, "string")
  }
}
func BenchmarkByteBuffer(b *testing.B) {
  for n := 0; n < b.N; n++ {
    ByteBuffer(1000, "string")
  }
}
func BenchmarkPreStrBuilder(b *testing.B) {
  for n := 0; n < b.N; n++ {
    PreStrBuilder(1000, "string")
  }
}
func BenchmarkPreByteBuffer(b *testing.B) {
  for n := 0; n < b.N; n++ {
    PreByteBuffer(1000, "string")
  }
}


单元测试结果:


goos: windows
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkPlus
BenchmarkPlus-8                     1296           1018434 ns/op
BenchmarkStrBuilder
BenchmarkStrBuilder-8              56254             19591 ns/op
BenchmarkByteBuffer
BenchmarkByteBuffer-8              44292             25731 ns/op
BenchmarkPreStrBuilder
BenchmarkPreStrBuilder-8           84880             14419 ns/op
BenchmarkPreByteBuffer
BenchmarkPreByteBuffer-8           62517             18126 ns/op
PASS


速度差距一目了然


字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和


strings.Builder,bytes.Buffer 的内存是以倍数申请的。strings.Builder 和 bytes.Buffer底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder 直接将底层的 []byte 转换成了字符串类型返回


4.使用空结构体节省内存


空结构体不占据内存空间,可作为占位符使用


比如实现简单的 Set


Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值


测试程序:


package benchstruct
func EmptyStructMap(n int) {
  m := make(map[int]struct{})
  for i := 0; i < n; i++ {
    m[i] = struct{}{}
  }
}
func BoolMap(n int) {
  m := make(map[int]bool)
  for i := 0; i < n; i++ {
    m[i] = false
  }
}


单元测试:


package benchstruct
import "testing"
func BenchmarkEmptyStructMap(b *testing.B) {
  for n := 0; n < b.N; n++ {
    EmptyStructMap(10000)
  }
}
func BenchmarkBoolMap(b *testing.B) {
  for n := 0; n < b.N; n++ {
    BoolMap(10000)
  }
}


测试结果:


goos: windows
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkEmptyStructMap
BenchmarkEmptyStructMap-8            988           1531114 ns/op
BenchmarkBoolMap
BenchmarkBoolMap-8                   693           1673018 ns/op
PASS


5.使用 atomic 包


原理


锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多

sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量

对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{}

测试代码:


package benchatomic
import (
  "sync"
  "sync/atomic"
)
type atomicCounter struct {
  i int32
}
func AtomicAddOne(c *atomicCounter) {
  atomic.AddInt32(&c.i, 1)
}
type mutexCounter struct {
  i int32
  m sync.Mutex
}
func MutexAddOne(c *mutexCounter) {
  c.m.Lock()
  c.i++
  c.m.Unlock()
}


单元测试:


package benchatomic
import (
  "testing"
)
func BenchmarkAtomicAddOne(b *testing.B) {
  for n := 0; n < b.N; n++ {
    var counter = atomicCounter{}
    AtomicAddOne(&counter)
  }
}
func BenchmarkMutexAddOne(b *testing.B) {
  for n := 0; n < b.N; n++ {
    var counter = mutexCounter{}
    MutexAddOne(&counter)
  }
}


单元测试结果:


goos: windows
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkAtomicAddOne
BenchmarkAtomicAddOne-8         46656297                27.71 ns/op
BenchmarkMutexAddOne
BenchmarkMutexAddOne-8          23804658                65.13 ns/op
PASS


6.总结


  • 避免常见的性能陷阱可以保证大部分程序的性能


  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
目录
相关文章
|
3天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
16 2
|
5天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
2天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
9 4
|
2天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
11 1
|
2天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
9 2
|
2天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
10 2
|
3天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
14天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
31 3
|
16天前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
25 3
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
130 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库