栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

字节跳动青训营Day03 - Go语言性能优化建议
    • 1.slice 预分配内存
    • 2.map 预分配内存
    • 3.使用 strings.Builder
    • 4.使用空结构体节省内存
    • 5.使用 atomic 包
    • 6.总结

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

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.总结
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能

感谢字节跳动大佬,张雷✔

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/890423.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号