Dominik Honnef(之前搞错成 Russ Cox 了)在 What’s happening in Go tip (2013-08-23) 中介绍了一些关于 Go 语言的一些变化。这些变化包含了语法、性能、潜在风险和工具链。并且,这些新的东西可能会随着 Go 1.2 版本一同发布。为了方便中文读者,翻译在此。
————翻译分隔线————
Go tip(2013-08-23)带来的变化
上周我发布了关于 Go tip 的变化的系列文章的第一篇。得到了大量的肯定,因此这是第二篇。感谢你的支持,并且希望你能像喜欢第一篇文章一样喜欢本文。
有哪些变化
这次,将对以下内容进行探讨:
- 关于切片的新语法
- 性能改进
- 快速的,常量时间的 P-256 椭圆曲线
- godoc 到哪去了?
关于切片的新语法
相关 CL:CL 10743046,CL 12931044
首先让我们来看看附加在 Go 1.2 中的、可能是最具争议的变化:允许设置 slice 的容量的新切片语法。不过在讨论为什么它存在争议之前,先来了解一下它究竟是什么。
所有人应当都已经熟悉了切片操作 mySlice[i:j],从 i 到 j 的范围创建一个新的 slice。mySlice 和新的 slice 将共享底层的数组,并且对其中一个 slice 操作会影响另一个。这是预期的众所周知的行为。
但一些人可能并未意识到,这两个 slice 也会共享容量。一个 slice 有长度和容量;当前可以看到的元素的数量和可以访问到的元素的数量。在使用 append()¹ 时,容量有着重要的作用:当对一个 slice 进行附加时,首先会检查底层数组是否留有足够的空间(容量)。如果有,则创建一个有着更大长度的新 slice 指向相同的底层数组,相同的内存空间。
¹: 如果容量允许,也可以手工重建 slice 并传递长度。也就是 append() 初步完成的事情。
考虑下面的代码片段:
orig := make([]int, 10, 20) subSlice := orig[5:10] fmt.Printf("len(orig) = %d, cap(orig) = %d, len(subSlice) = %d, cap(subSlice) = %d\n", len(orig), cap(orig), len(subSlice), cap(subSlice)) // 输出:len(orig) = 10, cap(orig) = 20, len(subSlice) = 5, cap(subSlice) = 15 orig = append(orig, 1) fmt.Println(orig) // 输出:[0 0 0 0 0 0 0 0 0 0 1] subSlice = append(subSlice, 2) fmt.Println(orig) // 输出:[0 0 0 0 0 0 0 0 0 0 2]
这证明了 subSlice,即使切片为 orig 的一个窗口,仍然访问相同的内存,由于切片不能收缩,所以至少在前 5 个元素是这样。也同样证明在使用 append 会导致“诡异”的结果,导致原先的 slice 中的元素被覆盖。
那么,为什么这在真实的代码中是一个问题呢?设想当你基于 []byte 编写了一个自己的内存分配器——当有处理大量的可能降低 GC 性能的小内存分配时,这种方式很常见。当用户请求了 N 字节的内存,你返回了一个从 []byte 上的某处切片出来的 slice,长度为 N。然后用户做了一些傻事情:他对其进行 append 操作。跟之前我们看到的一样,这个 append 将会超过 slice 的长度把内存“泄漏”出去,同时也超出内存分配器预期的那样。你改写了其他的内存!
一个安全的选择是允许实现自定义的内存分配器,不过通过限制返回的 slice 的容量来防止内存被侵蚀,这样 append 就不会在不应该的地方修改内存。而这个安全的选择就是新的切片语法:mySlice[i:j:k] —— 前面的两个元素跟以前一样,从 i 到 j 进行切片。第三个元素表示容量,这里的容量为 k – i 的结果。
简单来说,k – i 作为容量也就意味着 k – 1 是可以被新的 slice 访问到的映射到底层数组绝对序号,跟序号 j – 1 是长度一样。让 k 等于 j,就创建了一个不能向后访问超过其长度的 slice。现在就可以编写一个返回 N 字节的内存,并且不允许向后访问的分配器了。
在介绍中,我说这个补充是存在争议的。你可能已经猜到了原因:如果你继续思考“为什么我需要这个”,你就会猜到。这个特性极少会用到。只有极少的用例会需要它,标准库中也没有多少用到的例子。但这并不意味着这个特性不应当存在,虽然它解决了一个有效的问题,不过也被质疑切片语法不应当被扩展。像 setcap() 这样的函数被建议用作避免向 Go 添加新的语法,不过最终 Go 团队决定采纳新的切片语法。如果你感兴趣,这里有前前后后相关的讨论。当然,这会包含在 Go 1.2 中。
还有设计文档,描述了原始的动机和思路。
性能改进和垃圾回收
相关 CL:CL 11573043,CL 9129044,CL 12894043,CL 8179043,CL 9492044,CL 9432046,CL 8819049,CL 9459044,CL 12603049,CL 12708046,CL 10079043,CL 8670044,CL 12680046,CL 12694048,CL 11874043,CL 12072045,CL 9915043,CL 12662043,CL 9462044
将会在 Go 1.2 中看到许多的性能改进。这些改进主要包括两类:更好的代码和编译器带来的直接的性能提升,和减少垃圾带来的内存分配器和垃圾回收器的工作的减少。
Brad Fitzpatrick 在探索高性能 HTTP 的时候,已经在 net/http 包和相关的常用包中进行了许多的工作。他的大多数修改都落在了“减少垃圾”分类中。由于大多数修改都十分简单,我仅列出 CL 编号以便你检出代码。全部这些在描述部分都包括了性能测试:CL 9129044(更快的 JSON 编码),CL 12894043(更快的 ZIP 压缩),CL 8179043,CL 9492044 和 CL 9432046(若干 HTTP 的改进)。
不管怎样,有些 CL 确实很有趣。一组 CL 是 CL 8819049,CL 9459044 和 CL 12603049 —— 前面两个为 bufio 添加了缓冲池和缓冲重用,而最后一个移除了前面所说的缓冲池和缓冲重用,将其用一个简单的 Reset 方法替代了。这样做的原因是,这些缓冲区可以被用户控制,并且可能绕过 API 来损坏其他用户的数据,产生如“use after free”这样的错误,而这类错误由于 Go 明确的避免提供手工的方式来释放内存,会导致难以调试。并且,它为不必要的复杂代码引起额外的性能开销埋下了伏笔。
新添加的 Reset 方法用来达到相似的性能改进,强制用户激活重用 buffer,而不是依赖于包完成这个。net/http 就是众多用户中的一个,使用 Reset 的实现在 CL 12708046。务必注意,性能测试是与池化的 bufio 进行对比,而不是 Go 1.1。
对于原始的讨论,参阅 Russ Cox 提交的 issue 6086。
Brad 名下的最后一个修改是 CL 10079043,聚合了多个正在进行的 DNS 查询。或者换种说法:即使同一个 DNS 查询被执行了上百次,且都未结束的时候,其实只向服务器发送了一个真正的查询请求。
不过不仅 Brad 做了工作,其他人也贡献了大量的改进。
必须提到的一个是 CL 8670044,它为 Windows 实现了集成的网络池,使得在这个平台上的网络操作性能提升了 30% —— 在 Linux 上 Go 1.1 已经实现了的奢侈品。
另一个必须提及的应当是 CL 11573043,它极大的提升了 sync.Cond 的速度 —— 并且完全剔除了处理过程中的内存分配。
CL 12680046 和 CL 12694048 为 encoding/binary 添加了快速路由,处理整数类型的 slice,使得这一常见类型的处理又快,成本又低廉。
DES 加密得到了五倍的速度提升(不过,悲哀的是,它仍然很慢),感谢 CL 11874043 和 CL 12072045。
bzip2 解压缩的速度快了 30%(CL 9915043),而 net 包中的 DNS 客户端产生了少于 350 字节的更少的垃圾(CL 12662043)。
最后,但是并不是不重要的,io.Copy 现在通过 ReaderFrom 的 WriterTo 来区分优先顺序,当类型实现了着两个函数时,这会带来极大的可以看到的性能改进(且在 ioutil.Discard 中几乎没有操作)。
快速的,常量时间的 P-256 椭圆曲线
相关 CL:CL 10551044
一个读者向我指出,添加了常量时间的 P-256 实现是一个非常有趣的变化,而这一实现比之前的也快了不少。
当然性能很好,不过真正的要点在“常量时间”:函数对于所有可能的输入都花费相同的时间,这避免了计时攻击,一种通过计时信息进行密码破解的旁路攻击方式。
godoc 到哪去了?
使用 Go tip 总是要求你对开发过程更加熟悉,并且随时准备着损坏、诡异的行为和突然的改变。
然而,也不是每个使用 Go tip 的人都有时间检查整个开发记录,特别是在被告知“尝试 Go tip 看看你的问题是不是能解决”的时候。例如,有一大票人最近在编译了 Go tip 以后,都很惊讶和迷惑,godoc 不再工作了,不光是没有二进制文件了,有的甚至还提示了一个错误:
readTemplate: open /Users/rsc/g/go/lib/godoc/codewalk.html: no such file or directory
在两种情况里,修复方式都是运行`go get code.google.com/p/go.tools/cmd/godoc` —— godoc 已经被移动到了 go.tools 子版本库中。
未来
呼~从 Go 1.1 开始就一直在赶开发进度,但是终点还很遥远。幸运的是当前的 Go tip 的开发已经减速了,不再产生许多有趣的变化。而哪些重要的事情发生变化时,例如 godoc 的变化,我们讲在文章里提及。
有心的读者可能已经留意到之前的文章承诺了共享链接的支持。不幸的是,为了给更加重要的改变留出空间,也为了让大家能更好的理解切片语法的变化,在本文中并未提及。
下周有什么计划?更多的变化!
Leave a Reply