字节跳动青训营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.总结


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


  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
目录
相关文章
|
9天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
46 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
29天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
29天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
102 71
|
28天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
106 67
|
4天前
|
算法 安全 Go
Go 语言中实现 RSA 加解密、签名验证算法
随着互联网的发展,安全需求日益增长。非对称加密算法RSA成为密码学中的重要代表。本文介绍如何使用Go语言和[forgoer/openssl](https://github.com/forgoer/openssl)库简化RSA加解密操作,包括秘钥生成、加解密及签名验证。该库还支持AES、DES等常用算法,安装简便,代码示例清晰易懂。
32 12
|
7天前
|
监控 算法 安全
解锁企业计算机监控的关键:基于 Go 语言的精准洞察算法
企业计算机监控在数字化浪潮下至关重要,旨在保障信息资产安全与高效运营。利用Go语言的并发编程和系统交互能力,通过进程监控、网络行为分析及应用程序使用记录等手段,实时掌握计算机运行状态。具体实现包括获取进程信息、解析网络数据包、记录应用使用时长等,确保企业信息安全合规,提升工作效率。本文转载自:[VIPShare](https://www.vipshare.com)。
19 0
|
21天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数
|
29天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
40 7
|
1月前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
101 62
|
29天前
|
存储 Go
go语言中映射
go语言中映射
38 11