如果我们要遍历某个数组,Map集合,Slice切片等,Go语言(Golang)为我们提供了比较好用的For Range方式。range是一个关键字,表示范围,和for配合使用可以迭代数组,Map等集合。它的用法简洁,而且map、channel等也都是用for range的方式,所以在编码中我们使用进行循环迭代是最多的。对于这种最常使用的迭代,尤其是和对比,性能怎么样?我们进行下示例分析,让我们对循环有个更深的理解,便于我们写出性能更高的程序。
基本用法
的使用非常简单,这里演示下两种集合类型的使用。
这是针对 Slice 切片的迭代使用,使用关键字返回两个变量,第一个是 Slice 切片的索引,第二个是 Slice 切片中的内容,所以我们打印出来:
关于Go语言 Slice 切片的,可以参考我以前写的这篇 Go语言实战笔记(五)| Go 切片
下面再看看map(字典)的使用示例。
在使用迭代map的时候,返回的第一个变量是,第二个变量是,也就是我们例子中对应的和。我们运行程序看看输出结果。
这里需要注意的是,返回的键值对顺序是不固定的,是随机的,这次可能是第一个出现,下一次运行可能是第一个被打印了。
关于Map更详细的可以参考我以前的一篇文章 Go语言实战笔记(六)| Go Map。
常规for循环对比
比如对于 Slice 切片,我们有两种迭代方式:一种是常规的的方式;一种是的方式,下面我们看看两种迭代的性能。
为了测试,写了这两种循环迭代 Slice 切片的函数,从实现上看,他们的逻辑是一样的,保证我们可以在同样的情况下测试。
这事Bench基准测试的用例,都是在相同的情况下,模拟长度为1000的 Slice 切片的遍历。然后我们运行查看性能测试结果。
从性能测试可以看到,常规的for循环,要比的性能高出近一倍,到这里相信大家已经知道了原因,没错,因为每次是对循环元素的拷贝,所以集合内的预算越复杂,性能越差,而反观常规的for循环,它获取集合内元素是通过,这种索引指针引用的方式,要比拷贝性能要高的多。
既然是元素拷贝的问题,我们迭代 Slice 切片的目的也是为了获取元素,那么我们换一种方式实现。
现在,我们再次进行 Benchmark 性能测试,看看效果。
恩,和我们想的一样,性能上来了,和常规的for循环持平了。原因就是我们通过舍弃了元素的复制,然后通过获取迭代的元素,既提高了性能,又达到了目的。
Map 遍历
对于Map来说,我们并不能使用的方式,当然如果你有全部的元素列表除外,所以大部分情况下我们都是使用的方式。
飞雪无情的博客
以上示例是map遍历的函数以及benchmark测试,我都写在一起了,运行测试看一下效果。
相比 Slice 来说,Map的遍历的性能更差,可以说是惨不忍睹。好,我们开始下优化,思路也是减少值得拷贝。测试中的RangeForSlice也慢的原因是我把RangeForSlice还原成了值得拷贝,以便于对比性能。
再次运行下性能测试看下效果。
额,是不是发现点不对,方法的性能明显下降了,这个可以从每次操作的耗时看出来(虽然性能测试秒执行的次数还是一样)。和我们上面测试的Slice不一样,这次不止没有提升,反而下降了。
继续修改函数的实现为:
什么都不做,只迭代,再次运行性能测试。
*我们惊奇的发现,什么都不做,和获取值的操作性能是一样的,和Slice完全不一样,不是说 值拷贝损耗性能呢?都哪去了?大家猜一猜,可以结合下一节的原理实现
for range 原理
通过查看https://github.com/golang/gofrontend源代码,我们可以发现的实现是:
并且对于Slice,Map等各有具体不同的编译实现,我们先看看的具体实现
先是对要遍历的 Slice 做一个拷贝,获取长度大小,然后使用常规循环进行遍历,并且返回值的拷贝。
再看看的具体实现:
也是先对进行了初始化,因为是,所以这里其实是一个指针的拷贝。
结合着这两个具体的编译器实现,可以看看为什么的优化方式有用,而的方式没用呢?欢迎大家留言回答。
领取专属 10元无门槛券
私享最新 技术干货