在《Stray Pointers》上畅谈 Forth、C、CGI、Python、Go 与 AWK
本文是我与内容丰富、颇有趣味的播客Stray Pointers主持人 Jim Lawless 对谈的文字整理稿,已做轻度编辑并去除了口头禅um。访谈录制于 1 月 15 日(美国时间),我们聊了 Forth、C、CGI、Python、Go 和 AWK。
Jim: 今晚很高兴邀请到工程经理兼软件工程师 Ben Hoyt。Ben,欢迎来做客!
Ben: 非常感谢。
Jim: 你好 Ben,我注意到你是《The AWK Programming Language》新版的技术编辑/审校,顺着这条线索了解下来,发现你在编程语言方面的经历相当有意思。所以想聊聊你的编程背景——你是怎么入行的,最早用的是哪些语言?
Ben: 是啊,得追溯到很久以前了。我还是小孩的时候就开始接触编程了,大概是八十年代末、九十年代初吧。我记忆中第一台电脑是Sega SC-3000,一台键盘一体机,内置了BASIC。我父亲——他现在还健在,不过已经退休了——本职是牧师,业余是个编程爱好者,晚上就捣鼓Forth。
他在那台机器上捣鼓过一阵,好像还把 Forth 跑起来了,做了不少尝试,所以我们从小就接触到了这些东西。不过那会儿我主要还是在上面玩游戏,玩吃豆人之类的。家里到处都是计算机杂志,杂志后面会附一些 BASIC 小程序,通常是些无厘头的小游戏,你得照着杂志把代码一字不落敲进去。我试过几个,但从来没成功过——只要敲错一个字符,整个程序就跑不起来。不过我想,正是这些经历,再加上父亲的影响,让我慢慢入了门。
Jim: 我对你对 Forth 的兴趣特别好奇。你十几岁的时候就和 Hans Bezemer 一起在Forth Dimensions杂志上发表过文章,是这样吗?
Ben: 对,没错。我想那是我第一次公开发表技术文章。后来我一直很喜欢技术写作,现在也有自己的博客。但那篇文章现在回头看有点操之过急,同行评议也不充分,不过就当时而言还算不错。那篇文章讲的是 Forth 中的查找表有多好用,是我和他合写的。
Jim: 你们是怎么认识 Hans 的?他在 Forth 圈子里现在也算是小有名气,做了那个4tH 编译器——数字 4 加上 T-H。
Ben: 是的,我想那时候他就已经小有名气了,入圈很早。其实那之后我们就再没怎么联系过,但当时不知怎么在 comp.lang.forth 还是某个新闻组上聊上了,我说我在研究查找表,觉得这东西特别棒。他说他也喜欢,要不我们一起给Forth Dimensions写篇文章?我记不清是不是他先提的——我想应该是他提议合写的。
那是个不错的机会,于是我写了些代码,又整理了下关于它们比其他方法快多少的结论。现在想来我当时有点不太严谨——只要手指放对位置、笑容摆对角度,它们就更快,但在大多数情况下其实并非如此,可我还是那么写了。所以我说那篇文章有点早熟。当时就是觉得这东西很炫、很新奇,一心想用查找表,它们就成了包治百病的锤子,什么问题都想拿它敲一下。
Jim: 确实。像每个真正的 Forth 爱好者一样,你也自己实现了一个 Forth。能聊聊这段经历吗?
Ben: 是的。父亲对 Forth 痴迷,自己写过一个甚至几个 Forth 编译器。所以我认真学编程时,最早掌握的两门语言就是 x86 汇编和 Forth。后来在 DOS 时代,我用 8086 汇编写了自己的 Forth。Forth 最棒的一点就是小巧,你用很少的代码——大概两三千行——就能写出一个可自举的完整编译器。最底层有一小块用汇编写的内核,他们称之为原语,也就是构成 Forth 内核的基本操作,在此之上其余部分都可以用 Forth 本身来写。所以我的第一个版本就是用A86 汇编器写的内核——如果你听说过的话,那是个很不错的工具——再在上面用 Forth 搭建其余部分。
Jim: 是的,那是个相当流行的共享软件汇编器。
Ben: 没错。我想我当时一直用的是免费版,因为还是个孩子,没钱付费。不过那确实是个很棒的小汇编器。我的 Forth 第一个版本的内核是用 A86 写的,上面再叠 Forth 代码。过了几个版本后,我琢磨明白了 Forth 社区里说的元编译——也就是用 Forth 来构建 Forth,用 Forth 来编译自己的编译器。要做到这点,基本需要一个用 Forth 写的汇编器,所以我当时从网上找了一个,拿来改了改,几乎是重敲了一遍来搞懂它。那会儿很多东西都是这样学来的。
Jim: Bill Ragsdale 在 80 年代初曾在Dr Dobbs上发表过一个著名的 6502 Forth 汇编器。你用的是那个模型,还是有更适合 x86 的版本?
Ben: 不是,我用的是一个专门针对 x86 的版本。我已经不记得名字了,让我看看代码注释里有没有……看样子当时甚至没在注释里注明来源。总之它是基于一个 x86 汇编器的,具体是哪一个我记不清了。
Jim: 好,那么在搭完这些 Forth 系统之后,你用 Forth 做了些什么?写过哪些软件?
Ben: 这就是 Forth 有意思的地方:你不用它写什么正经软件,你就是不停地写 Forth 编译器。[笑] 我写过什么呢?一些小游戏和图形程序。在捣鼓 Forth 的同时,我也迷上了demoscene——给不了解的听众解释一下,这算是程序员的一个亚文化,大家热衷于写各种图形、音乐小样来炫技。还有各种比赛,比如 4K Demo——看谁能在 4K 大小的代码里塞进最炫的图形效果。我当时在新西兰通过BBS等渠道接触到这个圈子,也是在 BBS 上学了不少东西,那些大多是图形和游戏相关的,所以我也在 Forth 里做了些类似的尝试。
另外一件用 Forth 做的事——这是几年以后的事了——是给 386 写了一个自己的 32 位操作系统,一个 Forth 操作系统。非常轻量——我说操作系统,其实极度精简,就只有键盘驱动、磁盘 I/O 和屏幕驱动,基本就这些。
Forth 真的塑造了我的思维方式,可能到现在依然如此。虽然我现在已经不用它了,但它确实影响深远。它有点像Lisp,那种能颠覆思维、从哲学层面就很酷的语言。它让我一直偏爱小而快、轻量、能从上到下彻底理解的东西,一直理解到内核那一层。
不过做 demoscene 的时候我也接触了Turbo Pascal——很多 sceners 都用它。也浅尝过C,但那会儿对我来说还是有点晦涩。大概也就是那段时间,我高中毕业上了大学,学的是电气工程。说起来有个好笑的事——我们电气工程专业有个很奇怪的规定:在三年学制的开头可以选一门编程课,只要通过那门 C 语言小课——做点 C 练习、通过一个小测试、回答几个问题——通过了之后,整个专业剩余时间就不用再上任何编程课了。我当时就通过了,结果这是个错误决定,因为我虽然会写代码,却不懂软件工程和更宏观的东西。所以直到大学毕业工作后,我才真正掌握 C,学会如何组织大型程序、做软件工程,算是边工作边学。C 才是我第一门真正意义上的语言。
Jim: 那时你算是电气工程师?
Ben: 我学的是电气工程,但在第一份工作里就慢慢偏向了编程,而当时公司正好需要这方面的人,所以就一直做下来了,电气那块就丢掉了——可以说根本没用上所学专业。
Jim: 那是嵌入式 C,还是面向常规应用的?
Ben: 问得好。我第一份工作中大概有一半是嵌入式,做些小单片机的 bootloader 或微控制器的小驱动。这让我想到最近常听人说“嵌入式 Linux”,我就想:那根本不算嵌入式!你说的可是 2GHz 主频、1GB 内存的处理器!我说的那种嵌入式,我当年做的,是只有 2KB 内存的 16 位或 8 位小单片机。公司当时用的芯片之一是 MSP430,后来才慢慢过渡到 ARM7,开始接触一些 32 位的东西。
Jim: 当时你在团队里工作吗?
Ben: 公司非常小,所谓的团队,其实就只有另一位非常优秀的电气工程师兼固件开发者——Gary,我的第一位导师,人很好——他在编程中真正的软件工程方面给了我很多帮助。
Jim: 在只有两个开发者的团队里也能学到这些?我是说版本控制、代码共享、构建流水线之类的?
Ben: 那些是后来的事了。他教了我一些软件工程和计算机科学的基础,或者说我在那里耳濡目染学到了一些。那会儿他不太信版本控制那一套,所以我也没学。直到后来才接触——顺便说一句,我现在是坚定支持版本控制的。
Jim: 这可是好东西。
Ben: 这可是太好的东西了!我现在什么都用Git。所以版本控制和持续集成之类都是我职业生涯后期才接触到的。那会儿学的主要是编程基本功和如何组织代码。所以我用 C 做了一些嵌入式工作,但那也是我当时唯一真正会用的专业工具,于是不得不为我们当时做的那个气象系统写一个网页后端。需要渲染网页、画些漂亮的图表、输出一些 GIF 文件,而我只会 C,所以就用 C 写了 CGI 脚本。这对做 CGI 和网页来说是个糟糕透顶的选择,但确实把活儿干完了。我敢肯定我离职后接手的人肯定没少骂我,不过那个方案倒是实实在在撑了很多年。
Jim: 后来又学了哪些编程语言和技术?
Ben: 后来我和两个兄弟一起创业,做了一个叫microPledge的项目——细节可以展开说,但这里主要说编程——就是通过这个项目学会了 Python。其实之前我就对 Python 感兴趣,心想用它做 Web 应用肯定比 C 强得多,事实也确实如此。所以通过和兄弟们的这次创业,我学会了 Python。
那时我其中一个兄弟已经是相当扎实的 Python 开发者了,所以我不仅学了 Python,还学了测试、版本控制——这些都是那次创业经历的一部分。创业本身失败了,但在学习上却很成功,让我在 Python 和软件工程方面获得了职业技能。Python 就是那门语言,到今天我仍在用,也是我现在工作中的主要语言。所以那段早年的经历非常宝贵。
Jim: 你提到过你为 Python 贡献了os.scandir?
Ben: 是的,那是几年以后的事了——让我看看时间线。我在 2016 年写过相关文章,但实际开始要早好几年——应该是 2012 年。当时我想递归遍历目录结构,那会儿我在 Windows 上用 Python。我注意到 Windows 资源管理器里右键查看文件夹大小时,会非常快地统计出来,你能看到它在扫描,但速度很快。而用 Python 的os.walk做同样的事却慢得多。我就开始琢磨为什么会这样?Python 理应能和资源管理器的速度一样快才对。
结果发现,由于os.walk的设计和实现方式,它做了大量额外的系统调用——除了 Windows 用来获取文件名的FindFirst和FindNext调用外,还对每个文件都额外做了一次os.stat调用。其实根本不需要这些额外的stat调用。Python 等于是调用后又把一半信息扔掉了,然后再去做额外的调用,而scandir让你在递归遍历目录时直接拿到所需信息,不用把一半信息浪费掉。
Jim: 不过这个方案是跨平台的,对吧?
Ben: 是的。它在 Windows 和 Linux 上用了不同的系统调用,但思路是一样的。Windows 的FindFirst和FindNext会返回更多信息:直接给出所有文件属性。Linux 则不会给出大小等信息,只给出文件类型——是目录还是文件。但即便如此,在 Linux 上也能省掉一次os.stat调用,用来判断——噢,这是目录,要递归进去;不是,就不进去。所以在 Windows 上快了很多,Linux 上也有明显提速。而在网络文件系统上更快,因为可以想见,减少网络调用次数的好处更大。
Jim: 那os.walk就被弃用了?
Ben: 没有。os.walk是用scandir重新实现的。所以scandir算是更底层的工具,整个想法就是为了给os.walk提速。我们得以用scandir重写了os.walk,整体结构差不多,只是把原来的listdir加os.stat换成了scandir。这样完全不用改os.walk的接口,大家在脚本里继续用它,就能白白获得性能提升。这件事让我印象很深,觉得是个很酷的思路:你可以彻底换掉一个东西的内部实现——事后看似乎显而易见——但所有人都能免费享受到速度提升。当时有大量脚本在用os.walk,所以很多人都受益了。
Jim: 你提到写过 CGI 程序,又和兄弟们创业用 Python 做 Web,那我猜你为了搭建能跑起来的网站,肯定还得掌握其他技术:HTML、JavaScript、数据库之类的?
Ben: 是的,学了不少。数据库和 HTML——HTML 其实在做 C 的那份工作时就学过了——但SQL数据库对我来说确实是和兄弟们创业时才接触的新东西。第一次碰到关系模型时有点烧脑,思维方式很不一样。花了一段时间才摸出门道,但确实非常强大,从那以后就一直在用 SQL 数据库。所以肯定有数据库,也接触了一些 JavaScript。那会儿 JavaScript 还只是个附加品,而不是像现在这样整个网站都用它来写。直到今天我还是不太喜欢人们动不动就先上 JavaScript,其实很多东西用 HTML 就能搞定。
Jim: 你在一篇叫“小网络”的文章里也谈到过这个。
Ben: 确实如此。这也呼应了我之前说的:我一直喜欢小而快、轻量的东西。而现在的网页往往臃肿、庞大、缓慢,也有人在意这些,想方设法压缩图片、减少拖慢速度的 JavaScript 之类——我很喜欢这些努力。现在围绕这个也形成了一点潮流:“小网络”是个比较宽泛的说法,“独立网络”也是另一个说法,大家自己动手做网站,更多手作,更关注回归本源。
Jim: 不过不只是网页。现在还有用 Electron 写的桌面应用,本质上是用厚重的网页技术栈做的桌面程序,所以把臃肿也带到了桌面上。
Ben: 是的,确实如此。我也能理解复用这些技术有其价值。我觉得部分是个打包问题,我也看到有人在做这方面的尝试——不再引入 Electron,而是直接利用操作系统自带的浏览器技术,或者说机器上已有的浏览器,来做桌面应用。我忘了那些工具叫什么,但如果要走这条路,这似乎是个更好的方向。
Jim: 之后你的技术栈又有哪些演进?接下来学了哪些技术?
Ben: 往后跳一点说,有Postgres、JSON这些 Web 技术,但很多年里我主要还是用 Python。不过大概——让我想想,博客上写的是 2017 年——2017 年我学了 Go,也就是 Google 的Go 语言。起初是为了一个个人项目学的,但很快下一份工作就大量使用 Go,所以在职业上也马上派上了用场,一直用到现在。
Jim: 你为什么会选 Go?外面有那么多语言可学,Go 有什么吸引你的地方?
Ben: 我喜欢它的样子。喜欢它背后有那些贝尔实验室的老将。也可能是看到The Go Programming Language这本书是 Brian Kernighan 和仍在 Google Go 团队的 Alan Donovan 合著的——光是 Brian Kernighan 的名字就很吸引人,而且 Go 的三位创造者中有两位是贝尔实验室出身。设计 Go 的三个人是 Ken Thompson、Rob Pike 和 Robert Griesemer,其中两位我记得都是老Unix和贝尔实验室的人。
当然,最初吸引我的倒不是这个,我在Hacker News等地方看到过它,就很喜欢它的设计哲学。语法简单,是一种保持简单(Keep It Simple, Stupid)的语言设计态度,但即便在当时,它的工具链就已经非常专业、非常好用。Go 团队对此也很坦诚:语言本身没什么新东西,有几个独特概念,但没什么革命性或开创性的东西。真正的卖点是工具链、整体的整合度以及库的质量。编译速度快。只要敲go test就能自动跑完所有测试,设个环境变量敲go build就能交叉编译——就是这类体验。
Jim: 你说后来找到了用 Go 的工作。是你主动去找 Go 的岗位,还是碰巧遇上了?
Ben: 算是个幸运的巧合,但最后我多少也是因为他们用 Go 才选择了那份工作。某种程度上也是他们选中了我——面试就是围绕 Go 的,我记得还做了一道 Go 题目,所以他们也看重我的 Go 能力。
Jim: 后来你用 Go 为AWK编程语言写了一个解释器和转译器。你最早是在哪儿接触到 AWK 的?
Ben: 我最早在哪儿接触到 AWK?我很早就知道有 AWK 这么个东西,它是个老工具,70 年代的,但我其实一直没怎么用过——部分原因是我早年一直在 DOS 和 Windows 环境下,不怎么接触 Linux 和 Unix,所以 AWK 也不在视野里,但我早就听说过。后来开始在 Mac 上开发,Mac 是基于 Unix 的,再后来用正经的 Linux,就越来越多地看到 AWK。
真正让我入坑的是读了 Brian Kernighan 和 AWK 三位创始人——Al Aho、Peter Weinberger 和 Brian Kernighan——合著的The AWK Programming Language的前两章。他们一起写了这本书,我感觉主要还是 Brian 执笔——至少我的印象如此——我开始读这本书,发现它的风格和The C Programming Language也就是那本 K&R 如出一辙:简洁、信息密集。
读到第二章结束时,你就已经掌握了 AWK 的全部内容,他们已经把 AWK 教完了,剩下的章节都是深入细节,用 AWK 写个小编译器、写个小数据库系统——能用 AWK 做的这些事相当酷。这让我对 AWK 的印象从那种晦涩难懂的一行式语法,变成了:哦,原来概念这么简单:逐行读取、按字段切分、过滤器匹配就执行动作。概念简单,底下却是一门完整的编程语言。这一下就让我脑子转起来了。
那段时间我正捣鼓解析和编程语言解释执行,就把这两件事结合起来,想着可以用 Go 写个 AWK 的小子集。于是就动手了——起初只是个极简版本,只支持单个过滤器和单个动作,然后一点点往上加,直到有一天我想,干脆做个完整的 POSIX 实现算了。那时我们在纽约——我和妻子、家人在纽约住了大概 10 年——每天上下班要在公交/地铁上花一小时。于是我每天早上在公交上坐个三四十分钟,就在那儿敲我的GoAWK项目。所以它真的是在开往纽约的新泽西公交车上写出来的。
Jim: 你后来是怎么参与到《The AWK Programming Language》新版第二版中的?
Ben: 是这样——我想我和 Brian Kernighan 的第一次联系,是看到他的一次访谈里提到 AWK,说他在更新自己的那个——大家称之为“唯一正统的 AWK”——那个版本的 AWK,要加入对CSV(逗号分隔值)文件的支持。这也是我很想给 GoAWK 加的功能。或者那会儿我已经加了?时间线记不清了。我确实给 GoAWK 加了 CSV 支持,具体时间忘了,但看到那个访谈时我想,哇,我也在做同样的事。
于是我去看了 Brian 的代码,Kernighan 的代码,心想太好了,他也在做一样的事。然后我发现了一些性能问题——我认为是性能问题——我觉得也确实是合理的性能问题——是关于他处理UTF-8的方式。那是他做的另一件事:时隔多年,给 AWK 加上 CSV 和 UTF-8、Unicode支持。顺带一提,如果你处理的是 UTF-8,AWK 其实本来就能很好地支持 Unicode,因为 UTF-8 本就是 Unicode 的一种 8 位编码,只要不做逐字符的 Unicode 处理,一切都正常。所以大多数 AWK 脚本即便在旧版 AWK 上也能正常处理 UTF-8,但有些功能确实需要逐字符处理,这就是他要加的东西。
比如你要取字符串长度,在支持 Unicode 逐字符处理时,你想要的是字符串中 Unicode 字符的个数,而不是字节数。而性能问题在于,strlen函数——AWK 中的length函数——变成了线性时间操作,也就是与字符串长度成正比,因为它必须逐字节遍历并解码 UTF-8,才能算出有多少个字符。
于是对字符串的length操作就从原来的常数时间、超快的 O(1) 操作,变成了与字符串长度成正比的线性时间操作。大多数时候这没问题,因为字符串都很短,但如果你用 AWK 去处理 JSON 文件,或是做一些本来不该用 AWK 做但偏有人会做的事——比如超长字符串——然后又在循环里反复调用length,很快就会变成平方级的问题,本来是 O(N),因为在循环里调length,就意外变成了 O(N²)。长话短说,我就把这个问题指出来了。
我之前也把 GoAWK 改成了类似的 Unicode 感知模式,让length变成了线性时间操作,结果有位 GoAWK 用户反馈说:“啊,这把我的脚本搞崩了,我那个小 JSON 处理器处理一个中等大小的 JSON 文件竟然要花 25 分钟!”就是因为这种性能急剧放大的行为。所以我在 GoAWK 里回滚了那个改动,心想算了,这太棘手,还是回到按字节处理吧。正因为我在 GoAWK 上已经踩过这个坑,我就去联系了 Kernighan 说:“看,我在 GoAWK 上遇到了这个性能问题,你这样改很可能会把某些 AWK 用法——比如处理 JSON——的性能拖垮。”他承认这确实是个问题,但决定不回滚,他说:“我想保留这个行为,我觉得不会影响太多脚本。”他很可能是对的——对大多数实际使用场景不会有影响,但那里确实埋了个小小的性能陷阱。
就这样我开始和 Kernighan 聊 AWK,几年后我想他看到了 GoAWK 上的名字,也认可这个项目,所以当他重版那本书——《The AWK Programming Language》第二版——时,就发邮件联系我说:“嘿,你想不想审一下这本书,做技术审校之一?”
Jim: 那一定感觉很棒。
Ben: 是啊,真的很酷。能在 Kernighan 的书里作为技术审校之一署上自己的名字,还是挺让人自豪的。
Jim: 完全理解。那 Ben Hoyt 接下来有什么打算?
Ben: 我现在在Canonical工作,他们大量使用 Go 和 Python。其实我进 Canonical 也是通过 GoAWK 项目。对我来说那只是个业余项目,但 Canonical 里一位职位不低的人联系我说:“嘿,我在 Canonical 做个小东西时用到了你的 GoAWK 项目,挺酷的。顺便问下,你在找工作吗?”
而那会儿我确实在留意工作机会,所以就这样进了 Canonical。其实我在工作中并没有真正把 GoAWK 用在生产环境,Canonical 也没有在实质意义上使用它,但它帮我拿到了这份工作。所以我现在在 Canonical 做云、网络和基础设施相关的工作,目前主要用 Python,但他们也大量使用 Go,所以我参与的一些项目也会用到 Go。两门主力语言都在用。
Ben: 我一直在关注 Rust,喜欢它的承诺——我喜欢它的理念,但讨厌它的语法。就是那种通过定义、通过类型系统来保证安全,以及对所有权的追踪之类的承诺。但我还没学过,所以也不便多评。看起来学习曲线非常陡峭,学过 Rust 的人也都承认这一点,语法也相当繁杂,所以对我吸引力不是很大,不过向那些开始使用它的人致敬。
不过 Zig,不知为何从我看到的情况来看,很符合我的思维模型,所以我对 Zig 挺有好感。在我看来,Zig 是更好的 C,而 Rust 有点像是更好的 C++,大致是这么个感觉。所以我喜欢 Zig,喜欢我所看到的 Zig——虽然还没真正用过——喜欢它的显式性,内存分配都显式写出来。它也借鉴了 Go 的一些特性。我挺看好 Zig,想去学学。
Jim: 还有其他想征服的领域吗?
Ben: 让我想想——我有个小业余项目,是个叫GiftyWeddings.com的网站,一个婚礼礼单网站,做了挺久的一个有趣小项目。它每个月能带来几美元收入,你知道,就赚点零花钱的水平——但我主要是把它当作尝试新语言和新工具的试验场。所以每隔一段时间我就会重写后端或前端,试试新工具。这是个不错的玩法——我就是通过这个项目学会 Go 的,当初就是为了它才学 Go。
最近我在关注htmx,听说过这个库/工具集吗?它似乎正变得越来越受欢迎,我挺喜欢它的工作方式——保留现有的后端工具和 HTML,只是对其做扩展,而不是“用一堆 JavaScript 把它们全替换掉”。当然它底层还是用了 JavaScript,但模型很不一样——是的,我挺看好 htmx。想试试看,也许就会在我的 GiftyWeddings 项目上试一下。
Jim: 很好。Ben,感谢你今晚抽出时间,和你聊得很愉快。
随机一篇博客
评论
登录后参与讨论