当开发一个新的项目时,由于访问量级比较少,对于程序的性能来说不是太过重要。当随着业务的迭代升级,通过增加服务器来支撑业务,如果一个 server 程序用了公有云 1000 台服务器,而且都是大型机器,成本上升就不是一个级别。为此总结最近优化的一些小技巧来提升 GO 程序的性能,毕竟能减少几台是几台,都是钱。
而在优化的过程中,看了一下代码,切片用的地方还真不少,但性能却不高,为此总结一下切片的优化过程:
在使用 slice 时,如果知道切片的容量与大小,可以进行赋值操作,相比 append 减少返回新的切片
func BenchmarkForA(b *testing.B) {
list := make([]*int, 0)
for i := 0; i < 100; i++ {
a := i
list = append(list, &a)
}
b.ResetTimer()
for j := 0; j < b.N; j++ {
newList := make([]*int, 100, 100)
for i, v := range list {
newList[i] = v
}
}
b.StopTimer()
}
func BenchmarkForB(b *testing.B) {
list := make([]*int, 0)
for i := 0; i < 100; i++ {
a := i
list = append(list, &a)
}
b.ResetTimer()
for j := 0; j < b.N; j++ {
newList := make([]*int, 0, 100)
for _, v := range list {
newList = append(newList, v)
}
}
b.StopTimer()
}
复制代码
同样是给一个切片添加元素,赋值操作性能提升 35%
再看一下 append 的源码,返回的是一个新的切片:
当如果两个 slice 合并时,可以用 copy 减少内存分配,提高性能
func BenchmarkAppendA(b *testing.B) {
list := make([]*int, 0)
for i := 0; i < 100; i++ {
a := i
list = append(list, &a)
}
b.ResetTimer()
for j := 0; j < b.N; j++ {
existList := make([]*int, 0)
for i := 1; i <= 5; i++ {
a := i + 100
existList = append(existList, &a)
}
for _, v := range list {
existList = append(existList, v)
}
}
b.StopTimer()
}
func BenchmarkAppendB(b *testing.B) {
list := make([]*int, 0)
for i := 0; i < 100; i++ {
a := i
list = append(list, &a)
}
b.ResetTimer()
for j := 0; j < b.N; j++ {
existList := make([]*int, 0)
for i := 1; i <= 5; i++ {
a := i + 100
existList = append(existList, &a)
}
newList := make([]*int, 105)
copy(newList, existList)
copyList := make([]*int, 0, 100)
for _, v := range list {
copyList = append(copyList, v)
}
copy(newList[len(existList):], copyList)
existList = newList
}
b.StopTimer()
}
复制代码
要给一个切片扩容,但很多同学都就会用 append,其实用 copy 性能可提升 50%:
很多时候我们都需要网络服务加载一些内容,返回结果都是 byte 类型,经常会把 byte 转换 string,但是 string 操作会增加一次 copy 操作,因此我们可以通过 unsafe.pointer 进行转换
func unsafeToString(bytes []byte) *string {
hdr := &reflect.StringHeader{
Data: uintptr(unsafe.Pointer(&bytes[0])),
Len: len(bytes),
}
return (*string)(unsafe.Pointer(hdr))
}
func BenchmarkByteToStringA(b *testing.B) {
b.ResetTimer()
for j := 0; j < b.N; j++ {
aa := []byte("hello world")
str := unsafeToString(aa)
fmt.Sprintf("%v", *str)
}
b.StopTimer()
}
func BenchmarkByteToStringB(b *testing.B) {
b.ResetTimer()
for j := 0; j < b.N; j++ {
aa := []byte("hello world")
str := string(aa)
fmt.Sprintf("%v", str)
}
b.StopTimer()
}
复制代码
如果 byte 的内容较大时,优化效果明显:
但这里值得注意的是,string 类型是不可以修改的,而 byte 是可以修改的,所以这时对底层数组的值进行修改,将会造成严重的错误
在优化的过程中,看到好多 scanObject,findObject 占用 CPU,这是因为常驻于内存中结构体指针的数目太大了,所以减小垃圾回收压力的一个方法就是减少常驻于内存的结构体指针。
然而优化前的程序 slice 元素几乎用的都是指针,指针又指向一个结构体,指针虽小但每次都增加堆的分配,再看看以下的例子:
type AInt32 struct {
TemplateType int32
DataStr dataStr
}
type dataStr struct {
numStr string
}
func BenchmarkSliceA(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
list := make([]AInt32, 10000, 10000)
for j := 0; j < 10000; j++ {
a := AInt32{
TemplateType: int32(j),
DataStr: dataStr{
numStr: strconv.Itoa(i),
},
}
list[i] = a
}
}
b.StopTimer()
}
func BenchmarkSliceB(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
list := make([]*AInt32, 10000, 10000)
for j := 0; j < 10000; j++ {
a := AInt32{
TemplateType: int32(j),
DataStr: dataStr{
numStr: strconv.Itoa(i),
},
}
list[i] = &a
}
}
b.StopTimer()
}
复制代码
输出结果:
SliceA 明显优于 SliceB 的例子,这就是问题所在,产生的指针太多,导致 GC 不断的查询,添加标记 CPU 一直居高不下。
注意,但这方法不是万能,如果结构过大,还是建议用指针,防止拷贝大内存。
总结
性能优化是修炼很好的一个经历,通过这次尝试,真的是收获良多,对切片与 GC 有了更深的理解,希望以上的少少技巧能帮助大家对性能优化有所帮助。
评论