遍历 slice
遍历 slice 前会先获以 slice 的长度 len_temp 作为循环次数,循环体中,每次循环会先获取元素值,如果 for range 中接收 index 和 value 的话,则会对 index 和 value 进行一次赋值。
面试题
下面函数通过遍历切片,打印切片的下标和元素值,请问性能上有没有可优化的空间?
func RangeSlice(slice []string) { for index, value := range slice { _, _ = index, value }}参考答案:
- 在大多数情况下,对基本类型(如 int、float、bool 等)的赋值操作消耗相对较低。但如果 slice 的元素类型是较大的结构体或字符串,那么赋值操作的成本会更高,因为这涉及到更多的内存复制。
- 遍历过程中每次迭代会对 index 和 value 进行赋值,如果数据量大时,对 value 的赋值操作可能是多余的,可以在 for-range 中忽略 value 值,使用 slice[index]引用 value 值。优化后如下:
func RangeSlice(slice []string) { for index, value := range slice { fmt.Println(index,slice[index]) }}基准测试
要准确地量化这两种方式的性能差异,可以编写基准测试来对比两种方法的性能。在 Go 中,可以使用testing包来编写这样的测试。基准测试可以在相同的环境下多次执行每种情况,并测量其性能表现。代码如下:
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] } }}测试结果如下:
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,请问性能上有没有可优化的空间?
func RangeMap(myMap map[int]string) { for key := range myMap { _, _ = key, myMap[key] }}参考答案:函数中 for-range 语句中只获取 key 值,然后跟据 key 值获取 value 值,虽然看似减少了一次赋值,但通过 key 值查找 value 值的性能消耗可能高于赋值消耗。所以能否优化取决于 map 所存储的数据量和数据本身的复杂程度。优化后的代码:
func RangeMapOptimized(myMap map[int]string) { for key, value := range myMap { _, _ = key, value }}基准测试
那么到底是直接对 key 和 value 赋值更快还是通过哈希查找获得 value 更快呢?测试代码如下:
// 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)的测试结果如下:
BenchmarkRangeMap-8 7199294 168.7 ns/opBenchmarkRangeMapOptimized-8 12079117 100.5 ns/op大数据量(map 的长度为 1000)的测试结果如下:
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 已被关闭,则会解除阻塞并退出循环。