若是咱们要遍历某个数组,Map集合,Slice切片等,Go语言(Golang)为咱们提供了比较好用的For Range方式。range是一个关键字,表示范围,和for配合使用能够迭代数组,Map等集合。它的用法简洁,并且map、channel等也都是用for range的方式,因此在编码中咱们使用for range
进行循环迭代是最多的。对于这种最常使用的迭代,尤为是和for i=0;i<N;i++
对比,性能怎么样?咱们进行下示例分析,让咱们对for range
循环有个更深的理解,便于咱们写出性能更高的程序。html
for range
的使用很是简单,这里演示下两种集合类型的使用。git
package main import "fmt" func main() { ages:=[]string{"10", "20", "30"} for i,age:=range ages{ fmt.Println(i,age) } }
这是针对 Slice 切片的迭代使用,使用range
关键字返回两个变量i,age
,第一个是 Slice 切片的索引,第二个是 Slice 切片中的内容,因此咱们打印出来:github
0 10 1 20 2 30
关于Go语言 Slice 切片的,能够参考我之前写的这篇 Go语言实战笔记(五)| Go 切片golang
下面再看看map(字典)的for range
使用示例。api
package main import "fmt" func main() { ages:=map[string]int{"张三":15,"李四":20,"王武":36} for name,age:=range ages{ fmt.Println(name,age) } }
在使用for range
迭代map的时候,返回的第一个变量是key
,第二个变量是value
,也就是咱们例子中对应的name
和ages
。咱们运行程序看看输出结果。数组
张三 15 李四 20 王武 36
这里须要注意的是,for range map
返回的K-V
键值对顺序是不固定的,是随机的,此次多是张三-15
第一个出现,下一次运行多是王武-36
第一个被打印了。
关于Map更详细的能够参考我之前的一篇文章 Go语言实战笔记(六)| Go Map。app
好比对于 Slice 切片,咱们有两种迭代方式:一种是常规的for i:=0;i<N;i++
的方式;一种是for range
的方式,下面咱们看看两种迭代的性能。frontend
func ForSlice(s []string) { len := len(s) for i := 0; i < len; i++ { _, _ = i, s[i] } } func RangeForSlice(s []string) { for i, v := range s { _, _ = i, v } }
为了测试,写了这两种循环迭代 Slice 切片的函数,从实现上看,他们的逻辑是同样的,保证咱们能够在一样的状况下测试。函数
import "testing" const N = 1000 func initSlice() []string{ s:=make([]string,N) for i:=0;i<N;i++{ s[i]="www.flysnow.org" } return s; } func BenchmarkForSlice(b *testing.B) { s:=initSlice() b.ResetTimer() for i:=0; i<b.N;i++ { ForSlice(s) } } func BenchmarkRangeForSlice(b *testing.B) { s:=initSlice() b.ResetTimer() for i:=0; i<b.N;i++ { RangeForSlice(s) } }
这事Bench基准测试的用例,都是在相同的状况下,模拟长度为1000的 Slice 切片的遍历。而后咱们运行go test -bench=. -run=NONE
查看性能测试结果。oop
BenchmarkForSlice-4 5000000 287 ns/op BenchmarkRangeForSlice-4 3000000 509 ns/op
从性能测试能够看到,常规的for循环,要比for range
的性能高出近一倍,到这里相信你们已经知道了缘由,没错,由于for range
每次是对循环元素的拷贝,因此集合内的预算越复杂,性能越差,而反观常规的for循环,它获取集合内元素是经过s[i]
,这种索引指针引用的方式,要比拷贝性能要高的多。
既然是元素拷贝的问题,咱们迭代 Slice 切片的目的也是为了获取元素,那么咱们换一种方式实现for range
。
func RangeForSlice(s []string) { for i, _ := range s { _, _ = i, s[i] } }
如今,咱们再次进行 Benchmark 性能测试,看看效果。
BenchmarkForSlice-4 5000000 280 ns/op BenchmarkRangeForSlice-4 5000000 277 ns/op
恩,和咱们想的同样,性能上来了,和常规的for循环持平了。缘由就是咱们经过_
舍弃了元素的复制,而后经过s[i]
获取迭代的元素,既提升了性能,又达到了目的。
对于Map来讲,咱们并不能使用for i:=0;i<N;i++
的方式,固然若是你有所有的key
元素列表除外,因此大部分状况下咱们都是使用for range
的方式。
func RangeForMap1(m map[int]string) { for k, v := range m { _, _ = k, v } } const N = 1000 func initMap() map[int]string { m := make(map[int]string, N) for i := 0; i < N; i++ { m[i] = fmt.Sprint("www.flysnow.org",i) } return m } func BenchmarkRangeForMap1(b *testing.B) { m:=initMap() b.ResetTimer() for i := 0; i < b.N; i++ { RangeForMap1(m) } }
以上示例是map遍历的函数以及benchmark测试,我都写在一块儿了,运行测试看一下效果。
BenchmarkForSlice-8 5000000 298 ns/op BenchmarkRangeForSlice-8 3000000 475 ns/op BenchmarkRangeForMap1-8 100000 14531 ns/op
相比 Slice 来讲,Map的遍历的性能更差,能够说是惨不忍睹。好,咱们开始下优化,思路也是减小值得拷贝。测试中的RangeForSlice也慢的缘由是我把RangeForSlice还原成了值得拷贝,以便于对比性能。
func RangeForMap2(m map[int]string) { for k, _ := range m { _, _ = k, m[k] } } func BenchmarkRangeForMap2(b *testing.B) { m := initMap() b.ResetTimer() for i := 0; i < b.N; i++ { RangeForMap2(m) } }
再次运行下性能测试看下效果。
BenchmarkForSlice-8 5000000 298 ns/op BenchmarkRangeForSlice-8 3000000 475 ns/op BenchmarkRangeForMap1-8 100000 14531 ns/op BenchmarkRangeForMap2-8 100000 23199 ns/op
额,是否是发现点不对,方法BenchmarkRangeForMap2
的性能明显降低了,这个能够从每次操做的耗时看出来(虽然性能测试秒执行的次数仍是同样)。和咱们上面测试的Slice不同,此次不止没有提高,反而降低了。
继续修改Map2
函数的实现为:
func RangeForMap2(m map[int]Person) { for range m { } }
什么都不作,只迭代,再次运行性能测试。
BenchmarkForSlice-8 5000000 301 ns/op BenchmarkRangeForSlice-8 3000000 478 ns/op BenchmarkRangeForMap1-8 100000 14822 ns/op BenchmarkRangeForMap2-8 100000 14215 ns/op
*咱们惊奇的发现,什么都不作,和获取K-V
值的操做性能是同样的,和Slice彻底不同,不是说 for range
值拷贝损耗性能呢?都哪去了?你们猜一猜,能够结合下一节的原理实现
经过查看https://github.com/golang/gofrontend源代码,咱们能够发现for range
的实现是:
// Arrange to do a loop appropriate for the type. We will produce // for INIT ; COND ; POST { // ITER_INIT // INDEX = INDEX_TEMP // VALUE = VALUE_TEMP // If there is a value // original statements // }
而且对于Slice,Map等各有具体不一样的编译实现,咱们先看看for range slice
的具体实现
// The loop we generate: // for_temp := range // len_temp := len(for_temp) // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = for_temp[index_temp] // index = index_temp // value = value_temp // original body // }
先是对要遍历的 Slice 作一个拷贝,获取长度大小,而后使用常规for
循环进行遍历,而且返回值的拷贝。
再看看for range map
的具体实现:
// The loop we generate: // var hiter map_iteration_struct // for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { // index_temp = *hiter.key // value_temp = *hiter.val // index = index_temp // value = value_temp // original body // }
也是先对map
进行了初始化,由于map
是*hashmap
,因此这里实际上是一个*hashmap
指针的拷贝。
结合着这两个具体的for range
编译器实现,能够看看为何for range slice
的_
优化方式有用,而for range map
的方式没用呢?欢迎你们留言回答。
本文为原创文章,转载注明出处,「总有烂人抓取文章的时候还去掉个人原创说明」欢迎扫码关注公众号
flysnow_org
或者网站
http://www.flysnow.org/,第一时间看后续精彩文章。「防烂人备注
*……&¥」以为好的话,顺手分享到朋友圈吧,感谢支持。