for ranger 遍历

标签:Go语言首次发布:2024-03-24最近修改:2024-04-02

遍历 slice

遍历 slice 前会先获以 slice 的长度 len_temp 作为循环次数,循环体中,每次循环会先获取元素值,如果 for range 中接收 index 和 value 的话,则会对 index 和 value 进行一次赋值。

面试题

下面函数通过遍历切片,打印切片的下标和元素值,请问性能上有没有可优化的空间?

go
func RangeSlice(slice []string) {    for index, value := range slice {        _, _ = index, value    }}

参考答案:

  • 在大多数情况下,对基本类型(如 int、float、bool 等)的赋值操作消耗相对较低。但如果 slice 的元素类型是较大的结构体或字符串,那么赋值操作的成本会更高,因为这涉及到更多的内存复制。
  • 遍历过程中每次迭代会对 index 和 value 进行赋值,如果数据量大时,对 value 的赋值操作可能是多余的,可以在 for-range 中忽略 value 值,使用 slice[index]引用 value 值。优化后如下:
text
func RangeSlice(slice []string) {    for index, value := range slice {        fmt.Println(index,slice[index])    }}

基准测试

要准确地量化这两种方式的性能差异,可以编写基准测试来对比两种方法的性能。在 Go 中,可以使用testing包来编写这样的测试。基准测试可以在相同的环境下多次执行每种情况,并测量其性能表现。代码如下:

go
func BenchmarkRangeAssignment(b *testing.B) {  slice := make([]string, 10000)  for i := range slice {      slice[i] = strconv.Itoa(i)  }    b.ResetTimer()  for i := 0; i < b.N; i++ {      for index, value := range slice {          _, _ = index, value      }  }}func BenchmarkIndexAccess(b *testing.B) {    slice := make([]string, 10000)    for i := range slice {        slice[i] = strconv.Itoa(i)    }        b.ResetTimer()    for i := 0; i < b.N; i++ {        for index := range slice {            _, _ = index, slice[index]        }    }}

测试结果如下:

text
BenchmarkRangeAssignment-8        478634              2436 ns/opBenchmarkIndexAccess-8            479850              2345 ns/op
  • BenchmarkRangeAssignment:每次操作大约需要 2436 纳秒。
  • BenchmarkIndexAccess:每次操作大约需要 2345 纳秒。
  • 这两种遍历切片的方法在性能上非常接近,但是使用 range 语句直接访问 slice[index] (BenchmarkIndexAccess)的方式略微快一些。这就说明

遍历 map

遍历 map 时没有指定循环次数,循环体与遍历 slice 类似。由于 map 底层实现与 slice 不同,map 底层使用 hash 表实现,所以插入数据位置是随机的。

面试题

下面函数通过遍历 Map,打印 Map 的 key 和 value,请问性能上有没有可优化的空间?

go
func RangeMap(myMap map[int]string) {    for key := range myMap {        _, _ = key, myMap[key]    }}

参考答案:函数中 for-range 语句中只获取 key 值,然后跟据 key 值获取 value 值,虽然看似减少了一次赋值,但通过 key 值查找 value 值的性能消耗可能高于赋值消耗。所以能否优化取决于 map 所存储的数据量数据本身的复杂程度。优化后的代码:

go
func RangeMapOptimized(myMap map[int]string) {    for key, value := range myMap {        _, _ = key, value    }}

基准测试

那么到底是直接对 key 和 value 赋值更快还是通过哈希查找获得 value 更快呢?测试代码如下:

go
// prepareMapData 初始化mapfunc prepareMapData(size int) map[int]string {    m := make(map[int]string, size)    for i := 0; i < size; i++ {        m[i] = "Value" + strconv.Itoa(i)    }    return m}// RangeMap的基准测试func BenchmarkRangeMap(b *testing.B) {    testMap := prepareMapData()    b.ResetTimer()    for i := 0; i < b.N; i++ {        RangeMap(testMap)    }}// RangeMapOptimized的基准测试func BenchmarkRangeMapOptimized(b *testing.B) {    testMap := prepareMapData()    b.ResetTimer()    for i := 0; i < b.N; i++ {        RangeMapOptimized(testMap)    }}

我分别进行了小数据量(map 的长度为 10)和大数据量(map 的长度为 1000)的测试。

小数据量(map 的长度为 10)的测试结果如下:

text
BenchmarkRangeMap-8              7199294               168.7 ns/opBenchmarkRangeMapOptimized-8    12079117               100.5 ns/op

大数据量(map 的长度为 1000)的测试结果如下:

text
BenchmarkRangeMap-8                73364             15924 ns/opBenchmarkRangeMapOptimized-8      115476             10158 ns/op
  • 可以看到不管是小数据量还是大数据量的 map,直接同时获取 key 和 value 的性能会更好(比另一种方式差不多快 1.5 倍)。
  • 因为对于每个迭代项,map 内部的哈希查找只会执行一次。而如果只获取 key,然后手动通过 key 去查找 value,这实际上导致了额外的哈希查找开销。

遍历 channel

遍历 channel 是最特殊的,这是由 channel 的实现机制决定的:channel 遍历是依次从 channel 中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果 channel 已被关闭,则会解除阻塞并退出循环。