性能文章>一些提升go性能的小技巧>

一些提升go性能的小技巧转载

1月前
162410

导读

当开发一个新的项目时,由于访问量级比较少,对于程序的性能来说不是太过重要。当随着业务的迭代升级,通过增加服务器来支撑业务,如果一个 server 程序用了公有云 1000 台服务器,而且都是大型机器,成本上升就不是一个级别。为此总结最近优化的一些小技巧来提升 GO 程序的性能,毕竟能减少几台是几台,都是钱。

 

而在优化的过程中,看了一下代码,切片用的地方还真不少,但性能却不高,为此总结一下切片的优化过程:

 

正文

slice 赋值比 append 性能优:

在使用 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%

一些提升go性能的小技巧数据图表-heapdump性能社区

再看一下 append 的源码,返回的是一个新的切片:

一些提升go性能的小技巧数据图表-heapdump性能社区

给 slice 扩容

当如果两个 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 to string

很多时候我们都需要网络服务加载一些内容,返回结果都是 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 是可以修改的,所以这时对底层数组的值进行修改,将会造成严重的错误

 

GC 优化

在优化的过程中,看到好多 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[j] = 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[j] = &a
		}
	}
	b.StopTimer()
}

输出结果:

SliceA 明显优于 SliceB 的例子,这就是问题所在,产生的指针太多,导致 GC 不断的查询,添加标记 CPU 一直居高不下。

 

注意,但这方法不是万能,如果结构过大,还是建议用指针,防止拷贝大内存。

 

总结

性能优化是很好的一个经历,通过这次尝试,真的是收获良多,对切片与 GC 有了更深的理解,希望以上的少少技巧能帮助大家对性能优化有所帮助。

分类:标签:
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
记一次提升18倍的性能优化
背景最近负责的一个自研的 Dubbo 注册中心经常收到 CPU 使用率的告警,于是进行了一波优化,效果还不错,于是打算分享下思考、优化过程,希望对大家有一些帮助。自研 Dubbo 注册中心是个什么东西,我画个简图大家稍微感受一下就好,看不懂也没关系,不影响后续的理解。Consume
我好像发现了一个Go的Bug?
从一次重构说起这事儿还得从一次重构优化说起。最近在重构一个路由功能,由于路由比较复杂,需求变化也多,于是想通过责任链模式来重构,刚好这段时间也在 Sentinel-Go 中看到相关源码。用责任链模式,最大的好处是可以针对每次请求灵活地插拔路由能力,如:这样实现会在每次请求到来时去new
惨,给Go提的代码被批麻了
hello大家好,我是小楼。不知道大家还记不记得我上次找到了一个Go的Benchmark执行会超时的Bug?就是这篇文章《我好像发现了一个Go的Bug?》。之后我就向Go提交了一个PR进行修复,本想等着代码被Merge进去,以后也可以吹牛说自己是个Go的Contributor,但事情并不顺利
Go能实现AOP吗?
hello~大家好,我是小楼,今天分享的话题是Go是否能实现AOP?背景写Java的同学来写Go就特别喜欢将两者进行对比,就经常看到技术群里讨论,比如Go能不能实现Java那样的AOP啊?Go写个事务好麻烦啊,有没有Spring那样的@Transactional注解啊?遇到这样的问题我通
一些提升go性能的小技巧
导读当开发一个新的项目时,由于访问量级比较少,对于程序的性能来说不是太过重要。当随着业务的迭代升级,通过增加服务器来支撑业务,如果一个 server 程序用了公有云 1000 台服务器,而且都是大型机器,成本上升就不是一个级别。为此总结最近优化的一些小技巧来提升 GO 程序的性能,毕竟能减少几台是
对不起,我错了,这题不简单!
hello,大家好呀,我是小楼。前几天不是写了这篇文章《发现一个开源项目优化点,点进来就是你的了》嘛。文章介绍了Sentinl的自适应缓存时间戳算法,从原理到实现都手把手解读了,而且还发现Sentinel-Go还未实现这个自适应算法,于是我就觉得,这简单啊,把Java代码翻译成Go不就可以混
【全网首发】从一个死锁问题探讨Go和Java的读写锁
hello,大家好呀,我是小楼。最近我又双叒叕写了个BUG,一个线上服务死锁了,不过幸亏是个新服务,没有什么大影响。出问题的是Go的读写锁,如果你是写Java的,不必划走,更要看看本文,本文的重点在于Java和Go的读写锁对比,甚至看完后你会有一个隐隐的感觉:Go的读写锁是不是有BUG?故