Go 处理大数组使用 for range 和 for 循环的区别
作者:机器铃砍菜刀 发布时间:2024-04-28 09:18:59
前言:
对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显。
我们知道,Go 的语法比较简洁。它并不提供类似 C 支持的 while、do...while 等循环控制语法,而仅保留了一种语句,即 for 循环。
for i := 0; i < n; i++ {
... ...
}
但是,经典的三段式循环语句,需要获取迭代对象的长度 n。鉴于此,为了更方便 Go 开发者对复合数据类型进行迭代,例如 array、slice、channel、map,Go 提供了 for 循环的变体,即 for range 循环。
副本复制问题
range 在带来便利的同时,也给 Go 初学者带来了一些麻烦。因为使用者需要明白一点:for range 中,参与循环表达式的只是对象的副本。
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
你认为这段代码会输出以下结果吗?
original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]
但是,实际输出是;
original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]
为什么会这样?原因是参与 for range 循环是 range 表达式的副本。也就是说,在上面的例子中,实际上参与循环的是 a 的副本,而不是真正的 a。
为了让大家更容易理解,我们把上面例子中的 for range 循环改写成等效的伪代码形式。
for i, v := range ac { //ac is a value copy of a
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
ac 是 Go 临时分配的连续字节序列,与 a 根本不是同一块内存空间。因此,无论 a 如何修改,它参与循环的副本 ac 仍然保持原始值,因此从 ac 中取出的 v 也依然是 a 的原始值,而不是修改后的值。
那么,问题来了,既然 for range 使用的是副本数据,那 for range 会比经典的 for 循环消耗更多的资源并且性能更差吗?
性能对比
基于副本复制问题,我们先使用基准示例来验证一下:对于大型数组,for range 是否一定比经典的 for 循环运行得慢?
package main
import "testing"
func BenchmarkClassicForLoopIntArray(b *testing.B) {
b.ReportAllocs()
var arr [100000]int
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr); j++ {
arr[j] = j
}
}
}
func BenchmarkForRangeIntArray(b *testing.B) {
b.ReportAllocs()
var arr [100000]int
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j] = j
_ = v
}
}
}
在这个例子中,我们使用 for 循环和 for range 分别遍历一个包含 10 万个 int 类型元素的数组。让我们看看基准测试的结果。
$ go test -bench . forRange1_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkClassicForLoopIntArray-8 47404 25486 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeIntArray-8 37142 31691 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 2.978s
从输出结果可以看出,for range 的确会稍劣于 for 循环,当然这其中包含了编译器级别优化的结果(通常是静态单赋值,或者 SSA 链接)。
让我们关闭优化开关,再次运行压力测试。
$ go test -c -gcflags '-N -l' . -o forRange1.test
$ ./forRange1.test -test.bench .
goos: darwin
goarch: amd64
pkg: workspace/example/forRange
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkClassicForLoopIntArray-8 6734 175319 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeIntArray-8 5178 242977 ns/op 0 B/op 0 allocs/op
PASS
当没有编译器优化时,两种循环的性能都明显下降, for range 下降得更为明显,性能也更加比经典 for 循环差。
遍历结构体数组
上述性能测试中,我们的遍历对象类型是 int 值的数组,如果我们将 int 元素改为结构体会怎么样?for 和 for range 循环各自表现又会如何?
package main
import "testing"
type U5 struct {
a, b, c, d, e int
}
type U4 struct {
a, b, c, d int
}
type U3 struct {
b, c, d int
}
type U2 struct {
c, d int
}
type U1 struct {
d int
}
func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) {
b.ReportAllocs()
var arr [100000]U5
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr)-1; j++ {
arr[j].d = j
}
}
}
func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) {
b.ReportAllocs()
var arr [100000]U4
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr)-1; j++ {
arr[j].d = j
}
}
}
func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) {
b.ReportAllocs()
var arr [100000]U3
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr)-1; j++ {
arr[j].d = j
}
}
}
func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) {
b.ReportAllocs()
var arr [100000]U2
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr)-1; j++ {
arr[j].d = j
}
}
}
func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) {
b.ReportAllocs()
var arr [100000]U1
for i := 0; i < b.N; i++ {
for j := 0; j < len(arr)-1; j++ {
arr[j].d = j
}
}
}
func BenchmarkForRangeLargeStructArrayU5(b *testing.B) {
b.ReportAllocs()
var arr [100000]U5
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j].d = j
_ = v
}
}
}
func BenchmarkForRangeLargeStructArrayU4(b *testing.B) {
b.ReportAllocs()
var arr [100000]U4
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j].d = j
_ = v
}
}
}
func BenchmarkForRangeLargeStructArrayU3(b *testing.B) {
b.ReportAllocs()
var arr [100000]U3
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j].d = j
_ = v
}
}
}
func BenchmarkForRangeLargeStructArrayU2(b *testing.B) {
b.ReportAllocs()
var arr [100000]U2
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j].d = j
_ = v
}
}
}
func BenchmarkForRangeLargeStructArrayU1(b *testing.B) {
b.ReportAllocs()
var arr [100000]U1
for i := 0; i < b.N; i++ {
for j, v := range arr {
arr[j].d = j
_ = v
}
}
}
在这个例子中,我们定义了 5 种类型的结构体:U1~U5,它们的区别在于包含的 int 类型字段的数量。
性能测试结果如下:
$ go test -bench . forRange2_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkClassicForLoopLargeStructArrayU5-8 44540 26227 ns/op 0 B/op 0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU4-8 45906 26312 ns/op 0 B/op 0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU3-8 43315 27400 ns/op 0 B/op 0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU2-8 44605 26313 ns/op 0 B/op 0 allocs/op
BenchmarkClassicForLoopLargeStructArrayU1-8 45752 26110 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeLargeStructArrayU5-8 3072 388651 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeLargeStructArrayU4-8 4605 261329 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeLargeStructArrayU3-8 5857 182565 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeLargeStructArrayU2-8 10000 108391 ns/op 0 B/op 0 allocs/op
BenchmarkForRangeLargeStructArrayU1-8 36333 32346 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 16.160s
我们看到一个现象:不管是什么类型的结构体元素数组,经典的 for 循环遍历的性能比较一致,但是 for range 的遍历性能会随着结构字段数量的增加而降低。
结论
对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显。
另外,由于在 Go 中切片的底层都是通过数组来存储数据,尽管有 for range 的副本复制问题,但是切片副本指向的底层数组与原切片是一致的。这意味着,当我们将数组通过切片代替后,不管是通过 for range 或者 for 循环均能得到一致的稳定的遍历性能。
来源:https://developer.51cto.com/article/707994.html


猜你喜欢
- 最近在玩一个叫Baba is you的游戏,很羡慕里面的一个转场特效,所以试着做了一下。主要使用了JS和CSS,特效主要是用CSS实现的。H
- 如下所示:import numpy as np a=np.random.randint(0,10,size=[3,3,3])print(a)
- sql server2005新增加了2大数据类型: 1.大值数据类型 2.xml 1.大值数据类型 Microsoft SQL Server
- MySQL 8.0.27 下载、安装与配置 超详细教程(Windows64位),供大家参考,具体内容如下1.官网下载1.下载地址:MySQL
- 1 打开cmd,不改变运行的目录:输入python 空格 调试好的python文件路径或者python 空格 将py
- 首先说明代码只是帮助理解,并未写出梯度下降部分,默认参数已经被固定,不影响理解。代码主要实现RNN原理,只使用numpy库,不可用于GPU加
- 最近关于浏览器的最重要的事情就是IE的极光0day漏洞了,这个漏洞导致包括Google在内的多家美国公司受到黑客的攻击,当然也有很多网站被黑
- 引入为什么要有垃圾回收机制Python中的垃圾回收机制简称(GC),我们在程序的运行中会产生大量的变量用于保存数据,而有时候有些变量已经没有
- 1、首先模拟python类似shell命令行操作的接口:python安装subprocess(本地)、paramiko(SSH远程)#-*-
- 1,场景:根据学生编号查询,返回该学生所在班级的所有学生。支持分页、自定义排序及结果集自动定位到查询条件的学生编号所在页。 CREATE P
- 对于添加一个文件的路径我用的第一个方法就是sys.path.append()博主比较懒,就直接截图了啊对于上级文件路径和再上一级的路径可以直
- 前言大家应该都有所体会,在windows系统使用久了就会产生一些“垃圾”文件。这些文件有的是程序的临时文件,有的是操作记录或日志等。垃圾随着
- Session每台电脑访问服务器,都有独立的session,key值都一样,内容不一样。1.session保存在服务器上。2.session
- // 和PHP一样的时间戳格式化函数// @param {string} format 格式
- 在Web标准中一个很重要的概念就是强调页面的结构与表现分离。说的通俗一点就是XHTML中应该没有样式化的东西,而且Web在浏览器中除内容外都
- 问题背景:有一批需要处理的文件,对于每一个文件,都需要调用同一个函数进行处理,相当耗时。有没有加速的办法呢?当然有啦,比如说你将这些文件分成
- 写在前面最近写周赛题, 逃不开的一种题型是设计数据结构, 也就是第三题, 做这种题需要的就是对语言中的容器以及常用排序查找算法的掌握, 而我
- var gb1 = 10; this.gb2 = 20; function gb3() {}; (function() { var ro =
- 本文实例讲述了JavaScript函数重载操作。分享给大家供大家参考,具体如下:上个星期四下午,接到了网易的视频面试(前端实习生第二轮技术面
- python爬虫要用到的库:请求库:实现 HTTP 请求操作urllib:一系列用于操作URL的功能。requests:基于 urllib