在1986年的经典论文《没有银弹》中,弗雷德·布鲁克斯提出,从某种意义上说,提升程序员生产力的空间其实相当有限。他的推理思路是:编程任务中蕴含着一部分本质性/概念性1的复杂度,这部分复杂度从根本上无法被任何潜在的技术进步(如语言或工具)所攻克。随后,他运用阿姆达尔定律的论证方式指出,由于1/X的复杂度是本质性的,因此通过技术改进最多只能获得X倍的提升。
在文章结尾处,布鲁克斯断言编程中至少1/2(即大部分)的复杂度是本质性的,从而将所有技术性编程创新所能带来的潜在改进上限限定为最多2倍2:
所有针对软件过程中偶然性问题的技术攻击,从根本上都受限于生产力方程:
任务时间 = 对 i 求和 { 频率_i × 时间_i }
如果如我所信,任务的概念性组件目前占据了大部分时间,那么任何仅针对概念表达层面的任务组件所采取的行动,都无法带来巨大的生产力提升。
布鲁克斯给出了程序员生产力提升的上限。然而,在实践中,要正确陈述这一上限,就必须能够构想出那些因当前技术解决过程中摩擦过大而无人会合理尝试去解决的问题。
若无法预知未来,这一估算便不可能实现。假如我们知晓未来,或许会发现程序员能高效利用的计算能力或存储存在某种实际限制,从而约束了程序员可用的资源;但要得出偶然性复杂度的上限,仍需正确推断程序员将如何利用比当今多出亿万倍的资源——这难度之大,几乎等同于不可能。
此外,对于可能存在的每一类工具,人们都必须有效预见所有可能的创新。布鲁克斯对此的策略是审视现有工具类别,并针对每一类断言它们要么无效,要么虽有效但已无发展空间。这种观点之所以错误,不仅因为它低估了尚未存在、尚未见效或他不熟悉的工具类别(例如,他否定了形式化方法,却甚至未想到提及模糊测试器、不进行完全形式化验证的静态分析工具、Valgrind 等工具),还因为布鲁克斯认为每一类有重大改进的工具都已穷尽,而事实证明它们无一如此。例如,布鲁克斯在“脚本语言”兴起之前,以及垃圾回收语言主导绝大多数编程之前3,就断言编程语言基本已发展完毕。尽管你偶尔会听到类似说法,但很少有人愿意用 C 语言编写 Web 应用,因为现代语言带来的收益不可能超过使用现代语言的 2 倍。
布鲁克斯否定的另一项是人工智能,他声称“语音识别所用的技术与图像识别似乎鲜有共同之处,而两者又与专家系统所用技术不同”。然而,如今这显然不再成立——神经网络对图像识别和语音识别都极为有效。至于它们能否成为高效的编程工具,尚有待验证,但布鲁克斯反对人工智能的一个关键论据已被推翻,而认为大幅改进的 GPT-2 能为程序员带来显著生产力提升,并非牵强之见。当然,期望布鲁克斯预见到神经网络能同时有效处理语音和图像识别并不合理,但正因如此,他否定人工智能以及计算机科学其他所有领域的未来进步,才显得不合理。
Brooks 也低估了实践以及支持实践的工具所带来的收益。仅举一例,看看老一辈编程大师所倡导的观点:有 Ken Thompson 认为语言安全性毫无用处,他认为 bug 之所以出现是因为人们编写了脆弱的代码,如果不想有 bug 就不该那样写;还有 Jamie Zawinski 认为,在时间紧迫的情况下,自动化测试是浪费时间,“不经过测试就能一次搞定,这本身就很有价值”。Brooks 承认测试的重要性,但他提到的唯一可能的测试改进,是那些能让初学者更轻松进行测试的专家系统。如果你看看现代中等规模软件项目的复杂性,它们远远超出了 80 年代见过的任何软件项目。如果你认真思考一下,用老派的正确性实践来应对这些项目意味着什么,我认为从那种实践到现代实践所带来的速度提升,对于典型团队来说是无限的,因为大多数采用那些实践的团队,在面对许多大公司已经独立解决的问题时(例如,生产一个具有特定 SLO 的分布式数据库),根本无法交付一个可用的产品。有人可能会质疑“无限速度提升”的说法,但任何参与过对正确性要求严格的复杂项目的人,都会使用 那些能带来巨大开发速度提升的工具和技术,轻松超过 80 年代实践的 2 倍以上。这种可能性似乎 Brooks 从未想到过,因为他似乎认为,由于测试中固有的本质复杂性,测试方面不可能有重大的改进。
另一个基础工具/实践的例子是版本控制。支持多文件提交、分支、以及只要开发者不修改相同行就能自动合并的版本控制系统,是相当现代的发明。在90年代,微软处于软件开发的前沿,但他们直到Win2k之后才成功开发出支持所需仓库规模(Win2k开发需要3000万行代码)并支持分支的版本控制系统。分支的模拟方式仅仅是复制整个源代码树,然后手动尝试合并这些副本。修改源代码树需要特殊批准,由于手动合并的痛苦,整个Win2k团队(5000人,包括1400名开发者和1700名测试人员)在顺利时每天只能合并100个变更(不顺利时,当构建团队因修复构建中断而停滞,合并数为0)。这距离Brooks写作已经过去了十年,但更好的版本控制工具、测试工具和实践、以及硬件提速带来的更快测试等,仍然能带来一个数量级的效率提升。值得注意的是,除了未能预见到版本控制和测试工具后来会带来巨大的生产力提升外,Brooks还声称硬件提速不会显著提高开发者的效率,尽管硬件速度被认为是Win2k开发速度的主要限制因素。Brooks无法想象有人能构建像Win2k这样复杂的项目,从而真正利用更快的硬件。当然,在Brooks时代的工具和实践下,构建像Win2k这样复杂的项目几乎是不可能的,但工具和实践进步如此之快,以至于仅仅十年后,尽管由于“石器时代”的工具和实践,开发速度与我们今天习惯的相比仍如蜗牛般缓慢,但这样的项目已经变得可行。
再来看上文提到的另一个子部分:布鲁克斯没有将CI/CD列为潜在的生产力提升手段,是因为他根本无法想象会有工具能够实现现代构建实践。在1995年的著作中,布鲁克斯提到微软有人告诉他,他们每天进行夜间构建。对此,布鲁克斯表示,实现至少每天一次的构建可能工作量太大,并指出贝尔北方研究所相当合理地每周进行一次构建。就在布鲁克斯写下这些内容后不久,谷歌成立,而谷歌的工程师甚至无法想象满足于微软那样的设置,更不用说每周构建一次了。他们必须开发大量定制软件,才能将谷歌规模的单体仓库推向今天被视为现代实践的水平,但他们确实做到了。我曾工作过的一家成立于1995年的初创公司也建立了自己的CI基础设施,允许从主分支持续合并和构建,因为任何着眼于“还能做什么”而非认为“一切能做的都已做完”的人都会这样做。对于大型项目来说,仅仅拥有CI/CD并保持干净的构建,相较于每周构建一次,就能轻松实现2倍的生产力提升,这比布鲁克斯声称“一半的复杂性是本质性的”所允许的提升幅度还要大。值得庆幸的是,谷歌、我曾工作的初创公司以及许多其他地方的工程师们并没有相信“不可能获得2倍提升”的说法,而是实际构建了能够带来巨大生产力提升的工具。
从某种意义上说,审视《没有银弹》与我们之前审视Unix时发现的情况颇为相似:Unix大师们声称我们应该像70年代那样编写软件,并且他们发明的语言与任何语言一样安全。早在计算机发明之前,长辈们就一直告诉下一代,他们已经做完了所有能做的事,下一代将无法取得更多成就。在计算机时代,我们在编程领域之外也看到了无数类似的预测,比如克里夫·斯托尔如今臭名昭著的预言:互联网不会改变任何东西。
远见者们描绘着这样的未来:远程办公的员工、互动式图书馆、多媒体教室。他们谈论电子市政厅和虚拟社区。商业交易将从办公室和购物中心转向网络和调制解调器。数字网络的自由将使政府更加民主。
一派胡言。难道我们的计算机权威们连基本常识都没有吗?事实是,没有任何在线数据库能取代你的日报……电子出版又如何?试试在光盘上读书吧。充其量,那不过是件令人不快的苦差事:笨重电脑的刺眼微光取代了书本亲切的页面。而且你没法把那台笔记本电脑带到海滩上。然而,麻省理工学院媒体实验室主任尼古拉斯·尼葛洛庞帝预测,我们很快就能直接通过互联网购买书籍和报纸。呃,当然。……还有网络商业。我们被告知可以即时浏览目录购物——只需点击鼠标就能获得超值优惠。我们将在网上订购机票、预订餐厅、洽谈销售合同。商店将变得过时。那么,为什么我当地的购物中心一个下午的生意,就比整个互联网一个月的交易量还多?
如果你稍作替换,就会发现斯托尔和布鲁克斯说的是同一回事。诚然,技术在过去改变了事物,但我无法想象新技术会如何改变事物,所以它们根本就不会改变。
即使不了解编程的任何具体细节,我们也能看出这类论点在历史上从未站住脚,并且有充分理由相信,这一次长辈们实际上也并非正确。
布鲁克斯在脱离实践者身份后,仍持续撰写关于软件的著作相当长一段时间,但自1964年转入学术界后,他并未费心跟进业界动态——这从我们看过的1986年那篇文章中已显而易见,而若翻阅他2010年的著作《设计的设计》,则更为明显:书中依赖的仍是早期文章和书籍中使用的相同案例,其新内容的主体竟源自他自建的一栋房子。我们已看到,试图将自身知识泛化到土木工程领域的程序员,往往会说出任何土木工程二年级学生都能看出荒谬的言论,而事实证明,试图从房屋建造技巧中提炼软件工程设计技术的深刻洞见同样行不通——但由于布鲁克斯并未跟上业界步伐,这便是他所能提供的全部。尽管存在超越时代与行业的永恒洞见,但布鲁克斯提出的却是非常具体的建议,例如将软件团队运作得像鸡尾酒会手术团队那样——这源于他对如何改进IBM在50年代开发实践的思考。然而事实证明,业界早已远远超越IBM上世纪50年代的软件实践,那些相对于IBM50年代做法而言的改进理念,在70年后已毫无实用价值。
回到这篇文章的主题,用事后诸葛亮的眼光审视他关于偶然复杂性的具体论述,我们可以看到布鲁克斯在1986年声称“我们已经基本榨干了高级语言能带来的所有生产力提升”,这与1955年某个汇编语言程序员说“汇编已经是最好的语言了”4如出一辙。他关于其他类别的论断也类似。这些论断主要暴露的是想象力的匮乏。当布鲁克斯谈论概念复杂性时,他指的是1986年他所熟悉的概念构建块(针对他当时视为编程问题的问题)的使用复杂性。任何人都不应认为布鲁克斯1986年的编程观念是根本性的,正如不应认为1955年汇编程序员的观念是根本性的一样。人们常嘲笑那个杜撰的“640K内存对任何人都足够了”的名言,但布鲁克斯声称在所有潜在生产力改进的类别中,我们已经完成了大部分可能做到的事——这与之类似,而且并非杜撰!
展望未来,可能属于偶然的复杂性比例实际上是无限的。有人可能会说,如果只看当下,这些术语并非毫无意义。但尽管因领域而异,我个人从未处理过任何不严重受偶然复杂性主导的重大问题,这使得“本质复杂性”这个概念在我参与过的任何值得讨论的问题上都毫无意义。
附录:具体问题
让我们看看这个“本质复杂性”论断在我最近工作中做的几件事上是否成立:
- 从多台主机通过scp读取并下载日志,然后解析日志以了解问题范围
- 查询我雇主所有软件实例中每类软件两年的指标数据,然后生成各种图表,以了解软件运行状况及计算机资源使用情况
日志
如果分解这个任务,我们有以下步骤:
- 从几十万台机器通过scp将日志传输到本地服务器
- 为此编写了一个Python脚本以实现并行处理,错误处理比pssh/parallel-scp更稳健
- 编写脚本耗时约1分钟
- 在日志下载期间处理其他工作
- 解析下载的日志(数TB)
- 使用Rust脚本完成,编写耗时几分钟(此处因性能原因选择Rust而非Python——仅用惯用Python打开日志并逐行扫描,若不想将任务分发到多台机器,速度已慢于预期)
1986年,我或许会用telnet或ftp替代scp。当时还没有现代脚本语言(Perl诞生于1987年,而第一个被部分人视为现代版本的Perl5于1994年发布),因此要编写具备并行能力和“足够好”错误处理的代码,所需时间可能比现在多出一个数量级。事实上,仅实现连接池管理并附带勉强可用的错误处理,就可能比完成整个任务(不包括后台下载日志的时间)多花一个数量级的时间。
接下来是解析日志。用“1TB”这样的绝对数字来比较并不公平,我们姑且称之为“足够让我们在意性能的规模”(后续在指标示例中会更详细讨论规模问题)。如今,我们拥有多种高性能语言,能轻松编写快速、安全的代码,并借助库的力量(例如正则表达式库5)快速编写脚本解析和分类日志,将任务分配到电脑的所有核心上(我认为Zig也能轻松实现,但我选择Rust是因为团队中有足够多的Rust程序员)。
而在1986年,没有类似的语言,更重要的是,我无法轻松找到、下载并编译合适的库,必须手动编写所有解析代码,原本几分钟的任务能在一小时内完成就算幸运了。此外,如果我不知道如何使用某个库,或者甚至不知道可以借助库,我能在StackOverflow上轻松找到解决方案,这极大减少了意外复杂性。不用说,1986年根本没有类似谷歌搜索StackOverflow解决方案的途径。
更关键的是,即便在今天,这个典型的DevOps/SRE任务——相比1986年同类任务至少提速一个数量级——仍然几乎全是意外复杂性。
如果数据已导出到指标栈,或者集中式日志系统的工作方式稍有不同,整个任务将变得微不足道。即便两者都不成立,只要日志格式更统一,获取日志后我甚至无需编写任何代码;rg或ag就足够了。如果计算我花在任务核心概念上的时间,那少到难以估算。
查询指标
我们只需要一个反例,但通过更复杂的任务来观察布鲁克斯的论点如何随任务复杂度扩展,或许更具启发性。若想跳过这个冗长的例子,点击此处跳转至下一节。
我们可以将我的指标查询任务分解为以下子任务:
- 编写一组 Presto SQL 查询,每条查询需有效扫描约 100 TB 的数据,而完整数据集规模约为 100 PB(若未通过维护仅包含相关数据子集的表来缩减规模)
- 编写首条查询约需 30 秒,查询完成需数分钟,消耗约 1 CPU 年的计算资源
- 编写 ggplot 代码绘制我感兴趣的各种属性
- 耗时不确定,但比查询完成时间短,因此未增加此任务的总耗时
如今完成首个子任务的速度已快得无法估算其数量级差异,但我们可以分解该任务以了解其简化程度。
将 100 PB 这样的绝对数值直接移植到 1986 年并不公平,但仅就构建一个能像我为消费者软件公司所做的那样(收集软件资源使用与效率的各类数据)全面采集并持久化数据的管道而言,这在 1986 年会被视为荒谬。这里揭示了“意外本质复杂性为生产力提升设定上限”这一概念的根本缺陷:包含过多意外复杂性的任务在当时甚至不会被考虑为可行。Brooks 所看到的意外复杂性上限,实则受限于其想象力,而非本质因素。
Brooks 明确否定了计算能力提升对生产力的促进作用(“一个人究竟能有效利用多少 MIPS?“后文详述),但存储与 CPU 性能(更不用说网络速度与内存)所构成的意外复杂性,已大到足以限制 Brooks 所能构想的问题空间。
在此例中,假设我们在 1986 年已拥有足够存储空间来保存待查询数据。下一步则是调配约 1 CPU 年的计算资源,并在数分钟内完成查询。与存储问题类似,这在 1986 年同样荒谬6,因此我们遇到了第二项非本质复杂性——其规模之大足以让 1986 年的人完全无法构想此问题。
接下来就是编写查询。如果我为Cray-2编写代码并希望保持高效,我可能会用Cray的Fortran 77方言来写查询。我能在300秒内完成一个查询吗?绝无可能;用Scala/Scalding我都远远做不到,用Python/PySpark恐怕也够呛。我认为这是增益最小的环节,但即便如此,效率提升仍远超一个数量级。
数据处理完成后,我们需要生成图表。即便用今天的技术,不用ggplot至少会让我损失两倍的生产力。我试过所有号称功能相当的(任何语言的)主流绘图库,每个要么存在多个致命bug,连ggplot中我认为基础的图表都无法正常渲染,要么过于底层,被迫手动处理ggplot中轻而易举的操作,导致效率损失超过两倍。2020年,仅这一个库的存在就让我在这一步节省了两倍时间。回到1986年,在图形语法概念及其合理实现出现之前,即便假设有某种神奇的能快速完成我当前绘图操作的工作站硬件(我的机器渲染图表时已经慢得令人抓狂;Cray-2根本不可能在合理时间内完成渲染),我在绘图上至少会损失两个数量级的时间。
从1986年至今,这个问题中意外复杂性的降低幅度之大,我甚至无法估算。然而,这个问题中仍包含如此大量的意外复杂性,以至于连猜测本质复杂性所占比例都变得困难。要完整记录我能想到的所有意外复杂性,至少需要两万字,但为了略窥其貌,我列举几项。
- SQL;这看起来简单,但实际上极其复杂(链接)
- 还有,Presto SQL
- 各种 Presto 的限制,有些来自 Presto 本身,有些源于我们操作 Presto 的具体方式以及所使用的版本
- 当我以特定方式同时使用
numeric_histogram和cross join unnest时,会触发 Presto 内部数据结构断言失败。因为编写暴露 bug 的查询、等待失败、再重写太浪费时间,所以我有一套心理启发式方法,用来猜测任何同时使用这两种结构的查询是否会遇到这个 bug,并据此避免编写两个查询。如果启发式方法适用,我会改用更冗长但执行较慢的查询,而不是更直接的查询 - 我们按日期分区数据,但 Presto 在连接表时会丢弃这个分区信息,导致跨长时间段连接数据时产生非常大且昂贵的连接,尽管原则上这可以是一系列廉价连接;如果连接大到导致查询崩溃,我会编写一个类似小型查询编译器的东西,逐日执行查询,然后根据需要对数据进行后处理,而不是编写简单的查询
- 有很多情况,查询中的某种优化可以使查询可行,而无需跨天拆分查询(例如,如果我想将主机级指标数据与包含主机所在集群的表连接,跨多年数据连接会很慢,但我也知道哪些类型的主机在哪些集群中,这有时可以让我过滤掉主机级指标数据中的某些主机,比如核心数和总内存,从而让连接的大输入变小,使查询无需手动分区也能成功)
- 我们有一个“快”但内存限制“低”的 Presto 集群,和一个“慢”但内存限制“高”的集群,所以我需要心理估算每个查询每节点所需的内存,以便将其调度到正确的集群
- 等等
- 当我以特定方式同时使用
- 出于性能考虑,何时应在 Presto 中计算 CDF 或直方图,而不是留给 ggplot 在最后计算
- 我需要下采样多少数据(如果需要的话),以便 ggplot 能够处理,以及这如何影响分析
- 各种 ggplot 相关事项
- 散点图中需要多少个点,才应停止使用
size = [数字]并改用单像素绘制,因为将点绘制为圆形太慢 - 点的最小允许不透明度是多少
- 如果由于这个限制,散点图中的密度超过了可见梯度,我需要将图像放大多少以适当降低密度(何时这样做而不是使用热力图,值得单独写一篇文章)
- 等等
- 散点图中需要多少个点,才应停止使用
- 以上所有内容都涉及我用来编写和检查查询的工具,但还有关于所有数据问题的心理模型,在编写查询时必须考虑这些,以生成有效结果,包括时钟偏差、Linux 记账 bug、指标管道问题、底层数据源问题导致的数据问题等
- 等等
对于Presto和ggplot,我脑子里都默默装着上百个知识点才能让查询和绘图顺利运行,而我选择使用它们,是因为它们是我所知且可用的、开销最低的工具。如果有人问我,必须处理的复杂性中有多少是本质性的,我会说这个比例低到根本无法估算。对某些查询而言,这个比例甚至可以说是零——我的工作之所以必要,仅仅是因为某个任性的怪癖,如果没有这个怪癖,根本无需任何操作。但即便在那些看似必须进行某种查询的情况下,我认为本质复杂性也不可能超过我所面对复杂性的1%。
重新审视布鲁克斯关于计算机性能的观点——尽管我在2020年仍要应对硬件性能限制带来的复杂性,并且渴望拥有更快的计算机——但布鲁克斯早在1986年就断言,更快的硬件几乎无法提升开发者的生产力:
随着个人工作站处理能力和内存容量的确定且快速增长,软件艺术能期待什么收益?一个人能有效利用多少MIPS?程序和文档的编写与编辑已完全被当今的速度所支持。编译确实需要提升,但机器速度提升10倍肯定……
但这种说法至少在两个层面上是错误的。首先,如果我能使用更快的计算机,大量偶然复杂性就会消失(如果计算机足够强大,我就不需要像Presto这样复杂的工具;我完全可以在本地计算机上直接运行查询)。如今我们拥有快得多的计算机,但更快的计算机仍会让许多复杂的工程任务变得微不足道。正如詹姆斯·黑格所指出的,在80年代中期,由于性能限制,编写拼写检查器曾是一个严峻的工程问题。
第二,(仅举例而言)ggplot之所以存在,完全是因为计算机速度足够快。从事性能优化的人常抱怨:工具X在基本操作与当前硬件速度之间存在两到十个数量级的效率差距7。但有多少程序员能发挥现代多路机器一半以上的潜在性能?我猜不到千分之一,肯定不到百分之一。而且性能知识与其他知识并非独立——控制年龄和经验因素后,它与非“系统”领域的知识呈负相关,因为花在学习实现计算机一半潜力所需的偶然复杂性上的时间,就是没花在学习“直接”适用的领域知识上的时间。当我们审视需要大量领域知识的软件(如ggplot)或规模大到需要大团队实现的软件(如IntelliJ8)时,如果机器慢几个数量级,且编写可用软件必须榨干机器大部分性能,那么绝大多数这类软件根本不会存在。幸运的是,硬件性能大幅提升,使得绝大多数开发者可以忽略与性能相关的偶然复杂性,转而专注于当今高效工作所需的其他偶然复杂性。
更快的计算机既减少了工具用户遇到的偶然复杂性,也减少了工具创建者需要处理的偶然复杂性,从而让更高效的工具得以诞生。
2022年更新
很多人说这篇文章错了,因为布鲁克斯显然说的是X,而且他并没有表达我在文中引用的那些意思。但人们对布鲁克斯真正想表达的X有各种不同解读,因此这些反驳在整体上是自相矛盾的——他们认为布鲁克斯“显然”指某个特定意思,但如果真那么明显,人们就不会对布鲁克斯的意思有这么多不同看法了。
这当然是不可避免的,因为布鲁克斯的文章就像罗夏测试,陈述了大量不同甚至矛盾的观点。
感谢Peter Bhat Harkins、Ben Kuhn、Yuri Vishnevsky、Chris Granger、Wesley Aptekar-Cassels、Sophia Wisdom、Lifan Zeng、Scott Wolchok、Martin Horenovsky、@realcmb、Kevin Burke、Aaron Brown、@up_lurk和Saul Pwanson的评论/更正/讨论。
-
我在下一节讨论的事故。首先,让我们考虑本质。
软件实体的本质是一个由相互交织的概念构成的构造:数据集、数据项之间的关系、算法以及函数调用。这种本质是抽象的,因为概念构造在多种不同的表示形式下是相同的。尽管如此,它高度精确且极其详尽。
我相信构建软件的难点在于这一概念构造的规格说明、设计和测试,而非表示它以及验证表示忠实性的劳动。当然,我们仍然会犯语法错误;但与大多数系统中的概念错误相比,它们不过是小巫见大巫。
[返回] 2. 奇怪的是,在同一篇文章中,他还声称,在十年内,没有任何单项改进能带来10倍的提升。虽然这在技术上并不与他基于阿姆达尔定律的论点以及“大部分”(即至少一半)复杂性是本质性/概念性的主张相矛盾,但尚不清楚他为何要加入这一论断。
当布鲁克斯在1995年《没有银弹:重装上阵》中重新审视他的文章时,他声称自己是对的,因为他使用了1986年提出的三个主张中最弱的一个形式:在十年内,没有任何单项改进能带来一个数量级的提升。然而,他随后重申了1986年提出的最强形式的主张,并在1995年再次提出,称这一次,没有任何一组技术改进能真正将生产力提升超过2倍:
这是我的观点,仅此而已:工作中偶然性或表示性的部分现在已降至总量的一半或更少。由于这一比例是一个事实问题,其数值原则上可以通过测量来确定。如果无法测量,我的估计可以通过更知情和更近期的估计来修正。值得注意的是,没有人在公开或私下通信中声称偶然性部分高达9/10。
顺便说一句,我觉得有趣的是,他说没有人质疑这个9/10的数字。根据本文的内容,对于我的日常工作,我会将这个比例远高于9/10;如果我在1986年试图解决同样的问题,这个比例会高到人们甚至无法想象这个问题的存在。作为在硬件领域工作十年的副作用,我也做过一些与1986年某些人面临的工作相差无几的事情(微码、为DOS编写的汇编和C语言),我会轻易地将这些工作的比例也定为高于9/10。
他的后续回应中另一个让我感兴趣的部分是,他引用了哈雷尔1992年的《咬住银弹》,其中一点是,十年内实现一个数量级改进的截止日期是武断的。布鲁克斯对此的回应是:
十年期限还有其他原因:候选子弹所宣称的效果都具有某种即时性……未来40年我们必将取得实质性进展;40年内提升一个数量级绝非魔法。
但布鲁克斯在1995年重新审视这一论点时表示,若复杂性的十分之九是本质性的,那么即使将其降至零(这需要魔法)也无法获得超过一个数量级的改进,且未对时间跨度附加任何条件:
“NSB”无可争议地指出,如果工作中的偶然部分不足总量的十分之九,将其缩减至零(这需要魔法)也无法带来一个数量级的效率提升。
他的原始论文与1995年的续篇都写得颇具魅力,且包含某种局部逻辑——若你不加细思并遗忘文中其他内容,每个段落听起来都还算合理。与原文一样,吹毛求疵者可能会辩称这在技术上并不矛盾——毕竟布鲁克斯可能是在说:
- 至多十分之九的复杂性是偶然的(若忽略后来“二分之一”的主张——阅读此文时必须暂时搁置记忆与怀疑)
- 40年内消除100%的偶然复杂性并不令人意外
虽然这在技术上自洽(再次强调,若忽略矛盾部分),且是一套可提出的主张,但这意味着从1986年起40年后(即2026年),工具、语言或其他任何改进来源带来的效率提升空间完全归零并非不可能。但这显然是荒谬的。若审视布鲁克斯论文的其他章节并综合其推理,我们会发现更多矛盾与荒谬之处。
[返回] 3. 我们在此看到的另一个问题是布鲁克斯对类别间泾渭分明界限的坚持:本质性与偶然性复杂性;“解决方案类型”(如语言与“自建vs.采购”)等。
Brooks 承认“构建 vs. 购买”是应对本质复杂性的一种途径。或许他会同意,购买一个正则表达式包能降低本质复杂性,因为这样我就不必在简单任务中,把所有与编写解析器相关的概念都记在脑子里。但假如我不购买正则表达式,而是使用一种将其内置在标准库中或随语言一起分发的语言呢?又或者,如果我不必自己编写并发原语,而是这些原语已经内置于语言中呢?再进一步,整个 HTTP 服务器 呢?在可以“购买”(如今很多情况下是免费的)的库与内置于语言的库之间,并不存在一条清晰的界限,因此语言本身提供的优势与可以“购买”的优势之间,也无法划出清晰的界限。但如果这里没有清晰的界限,那么就不可能说其中一种能降低本质复杂性而另一种不能,同时还能在本质复杂性与偶然复杂性之间维持一条清晰的界限(在回应 Brooks 时,Harel 曾论证这种清晰区分并不存在,而 Brooks 的回应是坚称确实存在清晰界限,尽管他并未提出新的论据)。
Brooks 反复坚持这些虚假的区分,意味着这篇论文中的推理无法组合使用。正如我们在另一条脚注中已经看到的,如果你将论文某一部分的推理与另一部分的推理结合起来应用,很容易得出荒谬的结论,有时甚至直接产生矛盾。
我怀疑这就是关于本质复杂性与偶然复杂性的讨论如此混乱的原因之一。这不仅仅是因为 Brooks 在含糊其辞、模棱两可,实际上他自身并不自洽,因此不存在也不可能存在一个连贯的结论。Michael Feathers 曾指出 人们通常无法正确识别本质复杂性;正如他所说,一个人的本质复杂性是另一个人的偶然复杂性。这正是我们从这篇论文中应该预料到的结果,因为心中记住论文不同部分的人最终会得出互不相容的观点。
批评布鲁克斯时也会遇到这个问题。不可避免地,会有人说布鲁克斯真正的意思完全不同。这确实没错。但布鲁克斯在表达那些不同含义的同时,也的确说过我所提及的那些话。为辩护我在正文中提出的观点,这是一个在1986年可能持有的连贯观点。布鲁克斯的许多陈述即使作为独立语句来看也说不通,更不用说与他文章其他部分相互参照了。例如,“未来十年内没有任何单一改进能带来数量级提升”这一说法毫无意义,因为布鲁克斯没有定义、也没有人能明确说明什么是“单一改进”。而且如上所述,如果布鲁克斯试图主张的就是这个意思,他的文章读起来会非常奇怪,基本上说不通。对布鲁克斯的其他解读还存在另一个问题:即使布鲁克斯努力将这些观点定义清楚,它们本身也是毫无意义的。一个还是两个改进能带来数量级提升,这又有什么关系呢?如果是两个改进,我们两个都用就是了。
[返回] 4. 顺便说一句,这并非只发生在1955年。我曾与一些本世纪的人共事,他们告诉我汇编语言的效率基本和任何高级语言一样高。这对本博客的几乎每位读者来说可能听起来很荒谬,但如果你和整天编写微码或汇编的人交谈,偶尔会遇到相信这一点的人。
认为你个人使用的工具已经是最好的,这是一个容易陷入的陷阱。
[返回] 5. 另一个怪癖是,尽管布鲁克斯承认代码复用和库能提高生产力,但他声称语言和工具已经基本到头了,但这两个主张不能同时成立,因为库与语言/工具之间并没有明确的界限。[返回] 6. 让我们随意以一颗配备浮点协处理器、能实现200 kFLOPS的摩托罗拉68k处理器作为消费级CPU可能拥有的算力参考(FLOPS因多种原因并非好指标,但这只是为了估算获得1 CPU年计算资源所需的条件,而且布鲁克斯本人也把MIPS当作有意义的术语使用)。相比之下,Cray-2能达到1.9 GFLOPS,即大约10000倍的性能(我认为如果进行更合理的比较而非使用不可比的FLOPS数据,实际差距可能更小,但这里我们慷慨一点)。一年有525600 / 5 = 105120个五分钟时段,因此要在五分钟内获得1 CPU年的计算量,我们需要105120 / 10000 = 每查询10台Cray-2,这还不包括跨Cray-2聚合结果的开销。
认为一家1986年的消费软件公司会有足够的Cray-2闲置,让任何程序员随时都能快速运行CPU年级别的查询来做数据分析,这是不合理的。有消息称,Cray-2从1985年到1990年的整个生产周期中只制造了27台。即使我的雇主拥有全部这些机器,并且它们在1986年就已全部造好,也仍然不足以提供我在2020年所能获得的这种临时查询能力。
如今,一家初创公司的人甚至可以拿十年前的情况做类似比较。过去,你必须运营一个集群,这对初创公司来说成本高得令人望而却步,除非这家公司非常专业;但现在,你只需使用Snowflake,基本上就能获得Presto的功能,而且只需为你使用的计算能力付费(外加一笔可观的加价),而无需花钱拥有一个集群,也无需雇佣所有必要的员工来确保集群的正常运行。
[返回] 7. 实际上,我每次发布新文章时都会遇到这种情况。我在Google文档中写文章,然后将其复制到运行在tmux中的emacs里,而tmux又运行在Alacritty中。我的文章很小,足以放入L2缓存,因此我可以拥有64B/3.5周期的写入带宽。然而,复制+粘贴操作可能需要大约1分钟,速度慢到我可以看着文本被粘贴进来。由于我的芯片正在拼命工作以确保复制+粘贴完成,它以4.2GHz的全非睿频频率运行,提供76.8GB/s的写入带宽。对于一篇40kB的文章,1分钟等于666B/s。76.8G/666 ≈ 8个数量级的性能被浪费了。[返回] 8. 在这个具体例子中,我相信会有人争辩说,Visual Studio在2000年时相当不错,并且在速度慢得多的计算机上也能运行(而且其调试器可以说比当前版本更好)。但在Linux上没有类似的工具,也没有任何工具能与今天VSCode这类易于学习的编程编辑器相媲美,这类编辑器既提供编程专用功能(而不是记事本的增强版),又不像一个完整的IDE那样庞大。[返回]