惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
Last Week in AI
Last Week in AI
Blog — PlanetScale
Blog — PlanetScale
爱范儿
爱范儿
J
Java Code Geeks
A
About on SuperTechFans
F
Fortinet All Blogs
B
Blog
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
Y
Y Combinator Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
博客园_首页
博客园 - 叶小钗
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
Docker
云风的 BLOG
云风的 BLOG

Random Thoughts

尽职编程:AI Coding 时代的个体产出差异的来源 大公司与创业公司的 AI Coding 体验差异 LLM 训练与推理的基本理解 昨日的世界不再重来 Magic Brush - 画出你自己的产品宇宙 写在 AI Coding 奇点之后 尼泊尔布恩山小环线纪行 2025 投资组合年报 三十而笠 我的 8 年职业生涯回顾 我的颈椎病康复之旅 - 关于选择的故事 熊野古道中边路纪行 新加坡旅居三年再回首 高加索三国 - 阿塞拜疆行记 2024 投资组合年报 重新思考 Go:了解程序在线上是如何运行的 欧游散记 —— 环勃朗峰之旅 欧游散记 —— 特摩索斯古城 重新思考 Go:Channel 不是「消息队列」 重新思考 Go:Slice 只是「操作视图」 那一天,我决定踏出一步 True Story 少数价值 A Snapshot of Myself - 2021 RPC 漫谈: 连接问题 RPC 漫谈:序列化问题 RPC 漫谈: 限流问题 科学,技术与工程 Pond: Golang 通用对象池 Golang Interface 内部实现
Golang for-range 内部实现
Joway · 2021-01-20 · via Random Thoughts

最近在写一个编解码的功能时发现使用 Golang for-range 会存在很大的性能问题。

假设我们现在有一个 Data 类型表示一个数据包,我们从网络中获取到了 [1024]Data 个数据包,此时我们需要对其进行遍历操作。一般我们会使用 for-i++ 或者 for-range 两种方式遍历,如下代码:

type Data [256]byte

func BenchmarkForStruct(b *testing.B) {
	var items [1024]Data
	var result Data
	for i := 0; i < b.N; i++ {
		for k := 0; k < len(items); k++ {
			result = items[k]
		}
	}
	_ = result
}

func BenchmarkRangeStruct(b *testing.B) {
	var items [1024]Data
	var result Data
	for i := 0; i < b.N; i++ {
		for _, item := range items {
			result = item
		}
	}
	_ = result
}

输出结果:

BenchmarkForStruct-8     	 1697805	       652 ns/op
BenchmarkRangeStruct-8   	   60556	     19837 ns/op

可以看到通过索引来遍历的方式要比直接使用 for-range 快了近 30 倍。

索引遍历就是单纯地去访问数组的每个元素。而对于 for-range 循环,Golang 会根据迭代对象类型,已经 range 前的参数,对其进行不同形式的展开。对于以下 range 代码:

for i, elem := range a {}

编译器会将其转换成如下形式(伪代码), range.go

ha := a // 值拷贝
hn := len(ha) // 提前保存长度
hv1 := 0 // 当前遍历索引值
v1 := hv1 // 保存当前索引
v2 := nil // 保存当前值
for ; hv1 < hn; hv1++ {
    v1, v2 = hv1, ha[hv1] // 值拷贝
    ...
}

这里有几点需要额外注意:

  1. 编译器提前保存了元素长度,所以运行过程中即便长度变化,也不会影响循环次数
  2. ha := a 这一步会进行一次值拷贝,这里部分情况下可能会存在性能问题 (如上面的 [256]byte 类型,每次拷贝都有很大内存开销)
  3. v1, v2 = hv1, ha[hv1] 会对数组元素进行一次值拷贝
  4. v1, v2 预先创建,地址不会改变,对应到原始代码就是 for i, elem := range a {} 中的 i, elem 在每次循环时,都是同一个变量。

由此可以发现,当被迭代对象的元素为拷贝开销较大的类型时,使用 for-range 循环会存在很大的性能问题。此时更加建议使用标准 for 循环。