Go range实现原理及性能优化剖析
1. 前言
range是Golang提供的一种迭代遍历手段,可操作的类型有数组、切片、Map、channel等,实际使用频率非常高。
探索range的实现机制是很有意思的事情,这可能会改变你使用range的习惯。
2. 热身
按照惯例,我们看几个有意思的题目,用于检测对range的了解程度。
2.1 题目一:切片遍历
下面函数通过遍历切片,打印切片的下标和元素值,请问性能上有没有可优化的空间?
func RangeSlice(slice []int) { for index, value := range slice { _, _ = index, value } }
程序解释:
函数中使用for-range对切片进行遍历,获取切片的下标和元素素值,这里忽略函数的实际意义。
参考答案:
遍历过程中每次迭代会对index和value进行赋值,如果数据量大或者value类型为string时,对value的赋值操作可能是多余的,可以在for-range中忽略value值,使用slice[index]引用value值。
2.2 题目二:Map遍历
下面函数通过遍历Map,打印Map的key和value,请问性能上有没有可优化的空间?
func RangeMap(myMap map[int]string) { for key, _ := range myMap { _, _ = key, myMap[key] } }
程序解释:
函数中使用for-range对map进行遍历,获取map的key值,并跟据key值获取获取value值,这里忽略函数的实际意义。
参考答案:
函数中for-range语句中只获取key值,然后跟据key值获取value值,虽然看似减少了一次赋值,但通过key值查找value值的性能消耗可能高于赋值消耗。能否优化取决于map所存储数据结构特征、结合实际情况进行。
2.3 题目三:动态遍历
请问如下程序是否能正常结束?
func main() { v := []int{1, 2, 3} for i:= range v { v = append(v, i) } }
程序解释:
main()函数中定义一个切片v,通过range遍历v,遍历过程中不断向v中添加新的元素。
参考答案:
能够正常结束。循环内改变切片的长度,不影响循环次数,循环次效在循环开始前就已经确定了。
3. 实现原理
对于for-range语句的实现,可以从编译器源码中找到答案。
编译器源码gofrontend/go/statements.cc/For_range_statement::do_lower()
方法中有如下注释。
// 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 // }
可见range实际上是一个C风格的循环结构。range支持数组、数组指针、切片、map和channel类型,对于不同类型有些细节上的差异。
3.1 range for slice
下面的注释解释了遍历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前会先获以slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value的话,则会对index和value进行一次赋值。
由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的。
另外,数组与数组指针的遍历过程与slice基本一致,不再赘述。
3.2 range for map
下面的注释解释了遍历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时没有指定循环次数,循环体与遍历slice类似。由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。
3.3 range for channel
遍历channel是最特殊的,这是由channel的实现机制决定的:
// The loop we generate: // for { // index_temp, ok_temp = <-range // if !ok_temp { // break // } // index = index_temp // original body // }
channel遍历是依次从channel中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。
注:
- 上述注释中index_temp实际上描述是有误的,应该为value_temp,因为index对于channel是没有意义的。
- 使用for-range遍历channel时只能获取一个返回值。
4. 编程Tips
- 遍历过程中可以适情况放弃接收index或value,可以一定程度上提升性能
- 遍历channel时,如果channel中没有数据,可能会阻塞
- 尽量避免遍历过程中修改原数据
5. 总结
- for-range的实现实际上是C风格的for循环
- 使用index,value接收range返回值会发生一次数据拷贝
赠人玫瑰手留余香,如果觉得不错请给个赞~
本篇文章已归档到GitHub项目,求星~ 点我即达
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Netty NioEventLoop 启动过程源码分析
原文链接:https://wangwei.one/posts/netty-nioeventloop-analyse-for-startup.html 前面 ,我们分析了NioEventLoop的创建过程,接下来我们开始分析NioEventLoop的启动和执行逻辑。 Netty版本:4.1.30 启动 在之前分析 Channel绑定 的文章中,提到过下面这段代码,先前只讲了 channel.bind() 绑定逻辑,跳过了execute() 接口,现在我们以这个为例,开始分析NioEventLoop的execute()接口,主要逻辑如下: 添加任务队列 绑定当前线程到EventLoop上 调用EventLoop的run()方法 private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { // 通过eventLoop来执行channel绑定的Task cha...
- 下一篇
HashMap 怎么 hash?又如何 map?
HashMap是 Java 中 Map 的一个实现类,它是一个双列结构(数据+链表),这样的结构使得它的查询和插入效率都很高。HashMap 允许 null 键和值,它的键唯一,元素的存储无序,并且它是线程不安全的。 由于 HashMap 的这些特性,它在 Java 中被广泛地使用,下面我们就基于 Java 8 分析一下HashMap 的源码。 双列结构:数组+链表 首先 HashMap 是一个双列结构,它是一个散列表,存储方式是键值对。 它继承了 AbstractMap,实现了 Map<K,V> Cloneable Serializable 接口。 HashMap 的双列结构是数组 Node[]+链表,我们知道数组的查询很快,但是修改很慢,因为数组定长,所以添加或者减少元素都会导致数组扩容。而链表结构恰恰相反,它的查询慢,因为没有索引,需要遍历链表查询。但是它的修改很快,不需要扩容,只需要在首或者尾部添加即可。HashMap 正是应用了这两种数据结构,以此来保证它的查询和修改都有很高的效率。 HashMap 在调用 put() 方法存储元素的时候,会根据 key 的 ha...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题