Go 处理大数组使用 for range 和 for 循环的区别

目录
  • 副本复制问题
  • 性能对比
  • 遍历结构体数组
  • 结论

前言:

对于遍历大数组而言, 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 循环均能得到一致的稳定的遍历性能。

到此这篇关于Go 处理大数组使用 for range 和 for 循环的区别的文章就介绍到这了,更多相关Go 处理大数组内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go for range遍历二维数组的示例

    go for range 遍历二维数组 var arry [2][3] int for index,_ := range arry { fmt.Print(index) } 运行结果: 0 1   没有遍历所有的6个元素. 二维数组 arry 可以理解为:拥有两个 一维数组元素 的一维数组,所以以上只是遍历了其的两个元素,index分别是0 1,value是两个 拥有三个int类型元素 的一维数组. var arry [2][3] int for index,value := range arr

  • Go语言for range(按照键值循环)遍历操作

    Go 语言可以使用 for range 遍历数组.切片.字符串.map 及通道(channel).通过 for range 遍历的返回值有一定的规律: 数组.切片.字符串返回索引和值. map 返回键和值. 通道(channel)只返回通道内的值. 遍历数组.切片--获得索引和元素 在遍历代码中,key 和 value 分别代表切片的下标及下标对应的值.下面的代码展示如何遍历切片,数组也是类似的遍历方法: for key, value := range []int{1, 2, 3, 4} { f

  • go for range坑和闭包坑的分析

    看程序: package main import ( "fmt" "time" ) func main() { str := []string{"I","like","Golang"} for _, v := range str{ v += "good" } for k, v := range str{ fmt.Println(k, v) } time.Sleep(1e9) } 结果:

  • 详解Go语言中for range的"坑"

    前言 Go 中的for range组合可以和方便的实现对一个数组或切片进行遍历,但是在某些情况下使用for range时很可能就会被"坑",下面用一段代码来模拟下: func main() { arr1 := []int{1, 2, 3} arr2 := make([]*int, len(arr1)) for i, v := range arr1 { arr2[i] = &v } for _, v := range arr2 { fmt.Println(*v) } } 代码解析

  • golang中for range的取地址操作陷阱介绍

    Tips:for range创建了每个元素的副本,而不是直接返回每个元素的引用 例子1: package main import "fmt" func main() { slice := []int{0, 1, 2, 3} myMap := make(map[int]*int) for index, value := range slice { myMap[index] = &value } fmt.Println("=====new map=====")

  • go实现for range迭代时修改值的操作

    for range的val不能直接修改 因为地址不同 package main import "fmt" func main() { x := make([]int, 3) x[0], x[1], x[2] = 1, 2, 3 for i, val := range x { fmt.Println(&x[i], "vs.", &val) } } //输出 0x416020 vs. 0x41602c 0x416024 vs. 0x41602c 0x41

  • Go 处理大数组使用 for range 和 for 循环的区别

    目录 副本复制问题 性能对比 遍历结构体数组 结论 前言: 对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显. 我们知道,Go 的语法比较简洁.它并不提供类似 C 支持的 while.do...while 等循环控制语法,而仅保留了一种语句,即 for 循环. for i := 0; i < n; i++ { ... ... } 但是,经典的三段式循环语句,需要获取迭代对象的长度 n.鉴于此,为了更方便 Go 开发者对复合数据类型

  • C#+无unsafe的非托管大数组示例详解(large unmanaged array in c# without ‘unsafe’ keyword)

    C#申请一个大数组(Use a large array in C#) 在C#里,有时候我需要能够申请一个很大的数组.使用之.然后立即释放其占用的内存. Sometimes I need to allocate a large array, use it and then release its memory space immediately. 由于在C#里提供的 int[] array = new int[1000000]; 这样的数组,其内存释放很难由程序员完全控制,在申请一个大数组后,程序

  • PHP的array_diff()函数在处理大数组时的效率问题

    cisa 提交到 PHP 官方 BUG 页面上的方法 复制代码 代码如下: <?php /** * 解决 php 5.2.6 以上版本 array_diff() 函数在处理 * 大数组时的需要花费超长时间的问题 * * 整理:http://www.CodeBit.cn * 来源:http://bugs.php.net/47643 */ function array_diff_fast($data1, $data2) { $data1 = array_flip($data1); $data2 =

  • C++解决大数组栈内存不够问题的方法分析

    本文实例讲述了C++解决大数组栈内存不够问题的方法.分享给大家供大家参考,具体如下: 在c++中,我们可以直接通过下面的方式创建一个数组: const int N = 6; const int Nx = 100; const int Ny = 100; double phi[N][Nx][Ny]; double phi_b[N][Nx][Ny]; 但是,如果上述的Nx和Ny比较小还好说,一旦Nx和Ny很大时,就会报错,导致编译失败. 为解决这一问题,我们可以采用下面的几种方法来解决此问题: 1.

  • C#如何快速释放内存的大数组详解

    前言 本文告诉大家如何使用 Marshal 做出可以快速释放内存的大数组.最近在做 3D ,需要不断申请一段大内存数组,然后就释放他,但是 C# 对于大内存不是立刻释放,所以就存在一定的性能问题.在博客园看到了一位大神使用 Marshal 做出快速申请的大数组,于是我就学他的方法来弄一个.本文告诉大家这个类是如何使用. 在使用的时候,先来看下原来的 C# 的大数组性能.可以看到在不停gc,性能不好 static void Main(string[] args) { for (int i = 0;

  • TypeScript数组实现栈与对象实现栈的区别详解

    目录 前言 数组实现栈 实现思路 实现代码 编写测试代码 对象实现栈 实现代码 编写测试代码 二者的区别 十进制转二进制 前言 栈作为一种数据结构,它可以应用在很多地方,当你需要经常获取刚存放进去的数据时,那么栈这种数据结构将是你的首选. 栈的实现方式一般有两种:数组实现和对象实现,这两种实现方式最终实现的功能都是一样的,但是在性能上却有着很大的差别. 本文将详细讲解这两种实现方式的差异并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文. 数组实现栈 本文讲解的是栈用代码的实现,如

  • js删除数组中的元素delete和splice的区别详解

    例如有一个数组是 :var textArr = ['a','b','c','d']; 这时我想删除这个数组中的b元素: 方法一:delete 删除数组 delete textArr[1]  结果为: ["a",undefined,"c","d"] 只是被删除的元素变成了 undefined 其他的元素的键值还是不变. 方法二:aplice 删除数组 splice(index,len,[item]) 注释:该方法会改变原始数组. index:数组开

  • 谈一谈数组拼接tf.concat()和np.concatenate()的区别

    废话不多说啦,直接看代码吧! tf.concat t1 = [[1, 2, 3], [4, 5, 6]] t2 = [[7, 8, 9], [10, 11, 12]] tf.concat(0, [t1, t2]) ==> [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]] tf.concat(1, [t1, t2]) ==> [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]] # tensor t3 with sh

  • 浅谈JS数组内置遍历方法有哪些和区别

    目录 forEach()(ES6)方法 map()(ES6) 方法 flatMap()方法 for...in... for...of... filter(ES6)遍历数组 every()函数(ES6) find()函数(ES6) findIndex()函数 (ES6) forEach()(ES6)方法 forEach()(ES6)方法对数组的每个元素执行一次给定的函数. 1. 数组里的元素个数有几个,该方法里的回调就会执行几次     2. 第一个参数是数组里的元素,第二个参数为数组里元素的索引

  • 解析PHP中数组元素升序、降序以及重新排序的函数

    1,快速创建数组的函数range()比如range()函数可以快速创建从1到9的数字数组: 复制代码 代码如下: <?php $numbers=range(1,9);echo $numbers[1];?> 当然,使用range(9,1)则创建了9到1的数字数组.同时,range()还可以创建从a到z 的字符数组: 复制代码 代码如下: <?php $numbers=range(a,z);foreach ($numbers as $mychrs)    echo $mychrs."

随机推荐