“他应该瞪大眼睛盯着诺亚,好好学习,看他们是怎样把那么多东西装到一个小小的方舟上的。”
——西德尼·史密斯,《爱丁堡评论》
今天的程序员写代码,通常以千行为单位,大型项目以万行甚至百万行为单位。我们很少会停下来想:这么多代码,能不能塞进那枚小小的CPU里?能不能在有限的内存中跑起来?答案通常是"当然能"——因为我们的设备内存早已以GB计,存储空间以TB计,空间似乎是最不需要担心的问题。
但在布鲁克斯所处的年代,情况完全不同。内存以KB计,每一千字节都有明确的租金价格。程序占用的空间不是抽象的数字,而是用户每月账单上实实在在的支出。当空间本身就是成本时,"削足适履"不是锦上添花的技术优化,而是项目能否存活的根本约束。
正是在这样的背景下,布鲁克斯借用了诺亚方舟的比喻——在有限的空间里装载整个世界,需要的不是更大的方舟,而是更聪明的排列。
一、规模成本的真实面目:OS/360的惨痛教训
1.1 内存租金与整体预算
布鲁克斯用1970年代的一个案例揭示了空间成本的残酷:
IBM APL交互式软件系统,月租金400美元,运行时至少占用160K字节内存。在Model 165上,内存租金约12美元/每月每千字节。如果程序全天候运行,用户每月需支付400美元软件租金和1920美元内存租金——内存成本是软件本身的近5倍。
当时常有人批评"2M内存的机器上,操作系统就占了400K"。但布鲁克斯指出,这种批评就像批评波音747耗资两千七百万美元一样无知——关键问题不是"它花了多少",而是"它能干什么"。
“当系统设计者认为对用户而言,常驻程序内存的形式比加法器、磁盘等更加有用时,他会将硬件实现中的一部分移到内存上。相反的做法是非常不负责任的。所以,应该从整体上来进行评价。”
这与第一章"焦油坑"的呼应: 从简单程序到编程系统产品,成本膨胀9倍。这9倍成本中,空间成本占据了不可忽视的份额。规模本身不是坏事,但不必要的规模是不可取的。
OS/360的设计者非常仔细地为核心程序设定了规模目标。每个人都汇报了自己的核心大小,都在目标范围之内。但问题出在了视野之外:
“在为每个单元设立核心规模的同时,我们没有同时设置访问的目标。当程序员发现自己的单元核心未能达到要求时,他会把它分解成链接库。这个过程本身增加了程序整体的规模,并降低了运行速度。最重要的是,我们的管理控制系统既没有度量,也没有捕获这些问题。”
结果: 第一次性能仿真运行时,Fortran H在带磁鼓的Model 65上,每分钟只能模拟编译5条语句。控制程序模块进行了大量不必要的磁盘访问。
“和制订驻留空间预算一样,应该制订总体规模的预算;和制订规模预算一样,应该制订后台存储访问的预算。”
| 预算类型 | OS/360的做法 | 后果 |
|---|---|---|
| 核心内存预算 | ✅ 有 | 核心大小达标 |
| 整体规模预算 | ❌ 无 | 链接库膨胀,总空间失控 |
| 后台访问预算 | ❌ 无 | 磁盘访问爆炸,性能崩溃 |
1.2 功能边界与全局视角
OS/360的另外两个教训更为隐蔽,且相互关联。
教训二: 在每个模块分配功能之前,空间预算已经编制完成。结果是:
“任何在规模上碰到问题的程序员,会检查自己的代码,看是否能将其中一部分扔给其他人。因此,控制程序所管理的缓冲区成为了用户空间的一部分。更严重的是,所有的控制模块都有相同的问题,彻底影响了系统的稳定和安全性。”
核心问题: 当模块的"大小"被限定但"功能"未被精确定义时,程序员会本能地把功能"踢"给其他模块,以保住自己的空间指标。这就像让五个室友合租一间房,每人限定占用2平方米——结果不是大家精简行李,而是把东西塞进别人的区域。
“在指明模块有多大的同时,确切定义模块的功能。”
教训三: 项目规模本身很大,缺乏管理和沟通,以至于每个团队成员认为自己是争取小红花的学生,而不是构建系统软件产品的人员。为了满足目标,每个人都在局部优化自己的程序,很少会有人停下来,考虑一下对客户的整体影响。
这与第七章"巴别塔"的呼应: 第七章说"沟通是大型项目失败的首要原因",第九章说**“缺乏沟通导致每个人都在局部优化,没人考虑整体影响”**。巴别塔的工程师因为语言不通而无法协作,OS/360的工程师因为缺乏全局视野而各自为战——两种病症,同一种病根。
“在整个实现的过程期间,系统结构师必须保持持续的警觉,确保连贯的系统完整性。培养开发人员从系统整体出发、面向用户的态度是软件编程管理人员最重要的职能。”
二、装进方舟的艺术:空间、时间与数据
规模预算的制定只是第一步。真正让系统"装进方舟"的,是空间技能——在约束条件下进行创造性取舍的能力。
2.1 空间-时间折衷:从系统预算到算法课本
布鲁克斯指出了一个在很大范围内都成立的基本规律:
“对于给定的功能,空间越多,速度越快。”
这句话听起来熟悉吗?在数据结构与算法的课程里,我们学过几乎相同的道理——只是换了一套术语:用哈希表做查找,是以空间复杂度的上升换取时间复杂度的下降;用链表代替连续数组,是以访问时间的增加换取插入删除空间的节省。空间复杂度和时间复杂度从来不是独立的坐标轴,而是一条可以来回滑动的权衡曲线。
布鲁克斯在第九章说的正是这条曲线的系统级版本。OS/360的程序员为了保住自己的"核心内存预算"(降低空间占用),把代码拆成链接库、踢给其他模块(增加磁盘访问时间)——这本质上是一次用时间换空间的决策,只是没有人去计算这次交换是否划算。
| 策略 | 数据结构中的例子 | OS/360中的对应 | 代价 |
|---|---|---|---|
| 空间换时间 | 哈希表、缓存、索引 | 常驻内存、预加载 | 消耗更多内存 |
| 时间换空间 | 链表、压缩存储、惰性计算 | 链接库、覆盖加载 | 增加访问延迟 |
关键洞察: 数据结构的复杂度分析教我们如何在单个算法里做这道选择题;布鲁克斯教我们如何在整个系统里做同样的选择题——而且必须有人为这道题的全局后果负责。
在此基础上,布鲁克斯提出了另一个影响深远的策略问题:为用户保留多少选择?
“程序可以有很多的选择功能,每个功能仅占用少量的空间。也可以设计成拥有若干选项分组,根据选项组来剪裁程序。任何一系列特殊选项被合并在一起进行分组时,程序需要的空间较少。这很像小汽车。如果把照明灯、点烟器和时钟作为整个配件来标明价格,则成本会比单独提供这些选择所需要的成本低。所以,设计人员必须决定用户可选项目的粗细程度。”
| 策略 | 特点 | 空间效率 | 灵活性 |
|---|---|---|---|
| 细粒度选项 | 每个功能独立开关 | 低(重复代码多) | 高(用户自由组合) |
| 粗粒度分组 | 功能打包成组 | 高(共享代码多) | 低(用户只能选组) |
项目经理的两件事: 一是确保团队得到培训,而不是只依赖个人经验;二是建立公共构件库,对于每项功能至少准备两个实现——一个运行速度较快,一个短小精炼。
这与第八章"胸有成竹"的呼应: 第八章说高级语言可以将生产率提高5倍。第九章说,公共构件库是另一种形式的生产率提升——不是通过语言抽象,而是通过消除重复劳动。
2.2 数据的表现形式:从数据结构到系统架构
第九章的最后一部分,是整本书中最富诗意、也最具洞察力的段落之一。布鲁克斯在这里超越了"空间管理"的技术层面,触及了编程的本质。
“如果提供了程序流程图,而没有表数据,我仍然会很迷惑。而给我看表数据,往往就不再需要流程图,程序结构是非常清晰的。”
这个论断的颠覆性在于: 传统教学告诉我们"先画流程图,再写代码"。但布鲁克斯说,真正揭示程序本质的不是流程图,而是数据。流程图展示的是"程序怎么做",数据表展示的是"程序处理什么"。当你理解了数据的结构和关系,程序的行为自然浮现。
这与数据结构课程的核心思想完全呼应。 当你为一个问题选择了错误的数据结构——比如用数组做频繁插入删除,或者在需要快速查找时用了线性链表——你的程序就会像OS/360那样"膨胀"且低效:不是内存占用失控,就是运行时间爆炸。而当你重新表达数据——比如改用树形结构、哈希映射或者列式存储——你可能获得布鲁克斯所说的"战略上的突破",而非仅仅是技巧上的优化。
布鲁克斯进一步区分了两种技艺改进:
| 类型 | 例子 | 影响 |
|---|---|---|
| 技巧上的提高 | 更快的循环、更优的寄存器分配 | 线性改进,通常10%-50% |
| 战略上的突破 | 快速傅立叶变换、将比较算法从n²降到n log n | 数量级改进,通常10倍-100倍 |
“更普遍的是,战略上突破常来自数据或表的重新表达——这是程序的核心所在。缺乏空间而绞尽脑汁的编程人员,常常能通过从自己的代码中挣脱出来,回顾、分析实际情况,仔细思考程序的数据,最终获得非常好的结果。实际上,数据的表现形式是编程的根本。”
这与第四章"概念完整性"的呼应: 第四章说概念完整性是系统设计中最重要的因素。第九章说,概念完整性不仅体现在架构层面,更体现在数据层面——当数据的表达形式被重新设计时,整个程序的概念结构会随之清晰。
三、膨胀的隐秘逻辑:为什么系统总在变胖
3.1 温水煮青蛙:渐进式侵蚀的四个阶段
布鲁克斯揭示了一个可怕的模式:规模膨胀从来不是一次性的灾难,而是渐进式的侵蚀。
| 阶段 | 表现 | 心理机制 |
|---|---|---|
| 第一阶段:无害的添加 | “就加几行代码” | 低估累积效应 |
| 第二阶段:指标的转移 | “核心达标就行,其他不管” | 局部优化替代全局目标 |
| 第三阶段:性能的崩溃 | 系统变慢、资源耗尽 | 问题爆发时已经积重难返 |
| 第四阶段:重构的代价 | 重写比修改更便宜 | 技术债务到期 |
OS/360的教训告诉我们,第三阶段的问题在第一阶段就已经埋下。当管理控制系统"既没有度量,也没有捕获这些问题"时,膨胀就像暗流,直到仿真运行时才浮出水面。
3.2 "不必要的规模"的三种现代形态
布鲁克斯所说的"不必要的规模",在今天有三种更隐蔽的形态:
| 形态 | 1970年代的表现 | 现代的表现 |
|---|---|---|
| 功能膨胀 | 每个选项独立存在,重复代码多 | 微服务过度拆分,每个服务都自带全套基础设施 |
| 依赖膨胀 | 链接库无节制增长 | 引入一个工具库,连带引入几十个间接依赖 |
| 数据冗余 | 缓冲区重复分配 | 多副本缓存、重复日志、未清理的临时数据 |
3.3 局部优化的组织根源
第九章最深刻的问题不是技术性的,而是组织性的:为什么明知会损害全局,个人仍然选择局部优化?
答案在于激励机制的错位:
- 如果绩效考核基于"我的模块是否达标",而不是"系统整体是否最优"
- 如果晋升取决于"我完成了多少功能",而不是"我为系统精简了什么"
- 如果沟通成本高于局部优化的收益,那么局部优化就是理性的选择
这与第二章"人月神话"的呼应: 第二章说增加人手不能线性缩短工期,因为沟通成本二次方增长。第九章说,当沟通成本过高时,个人会选择局部优化来规避沟通——不是因为他们不关心全局,而是因为关心全局的代价太高了。
四、从控制到预防:现代工程的回应
4.1 云时代的回归与微服务的粗细之争
布鲁克斯时代的内存成本是显性的——每月每千字节12美元,直接体现在账单上。云时代的空间成本一度变得"隐形"——存储便宜、内存充裕,"削足适履"似乎成了过时的焦虑。
但成本从未真正消失,只是换了形态:
| 1970年代 | 现代对应 |
|---|---|
| 内存租金 | 云服务器实例费用 |
| 磁盘访问次数 | 数据库查询次数与延迟 |
| 程序驻留空间 | 容器镜像大小、冷启动时间 |
| 后台存储访问 | 网络带宽、跨可用区流量 |
容器镜像大小的回归: 当容器镜像从几十兆膨胀到几个G时,部署时间、冷启动时间、节点资源占用都随之恶化。现代团队重新发现了"削足适履"的价值——多阶段构建、精简基础镜像、移除无用依赖,这些实践正是布鲁克斯"规模控制"思想的现代回响。
微服务架构本质上是对"功能换尺寸"策略的大规模应用:
细粒度选项(微服务过拆): 每个功能一个服务,独立部署、独立扩展。灵活性极高,但每个服务都自带通信框架、监控代理、配置管理——共享代码的缺失导致总空间(运维复杂度)爆炸。
粗粒度分组(适度聚合): 将相关功能聚合为领域服务,内部共享框架和库。灵活性降低,但每个服务的"自重"更轻,整体运维负担更小。
这与第九章原文的呼应: 布鲁克斯说"设计人员必须决定用户可选项目的粗细程度"。今天,架构师必须决定服务边界的粗细程度——同一个权衡,不同的战场。
4.2 数据重新表达的战略价值
布鲁克斯说"数据的表现形式是编程的根本"。在数据驱动的时代,这一洞察被放大到了极致:
传统数据库设计: 表结构反映业务实体,查询通过多表连接实现。
现代数据建模: 宽表、物化视图、列式存储——通过重新表达数据的物理结构,将运行时的连接计算转化为预计算,换取数量级的查询性能提升。
这不是索引优化或查询调优,而是布鲁克斯所说的"战略上的突破"——来自数据表现形式的重新表达。
4.3 给你的实践建议
个人层面:成为"有空间意识"的工程师
- 在写代码前先问"有必要吗":这个功能真的需要吗?这个依赖真的不可替代吗?
- 学会从代码中抽离,审视数据:当遇到复杂逻辑时,先问自己——如果重新设计数据结构,问题会不会变简单?
- 掌握两种实现:对于常用功能,准备两个版本——一个追求速度,一个追求精简。
团队层面:建立"反膨胀"的规模文化
| 机制 | 目的 | 实施方式 |
|---|---|---|
| 整体规模预算 | 防止局部达标、全局失控 | 不仅限制核心模块,也限制总依赖数、总镜像大小 |
| 功能定义先行 | 防止"踢皮球"式优化 | 每个模块在限定大小的同时,必须有明确的功能边界文档 |
| 空间-时间评审 | 在方案阶段就考虑取舍 | 技术方案评审时,强制要求说明空间和时间的选择理由 |
| 公共构件库 | 消除重复,共享优化成果 | 团队内部维护常用功能的"快版"和"精简版"实现 |
组织层面:投资"全局最优"的基础设施
- 调整绩效考核: 不仅考核"我的模块是否按时交付",也考核"我的模块对系统整体资源的影响"
- 建立资源看板: 让团队实时看到系统的空间占用趋势,就像看测试覆盖率一样自然
- 强制"瘦身迭代": 每个季度或半年,安排专门的迭代用于清理无用代码、移除冗余依赖、优化数据存储
- 培训数据建模: 不仅培训编程语言和框架,更培训"如何重新表达数据以获得战略突破"
结语
布鲁克斯在第九章结尾没有给出豪言壮语,因为"削足适履"本身就是一场与膨胀本能的永恒对抗。
但我想用布鲁克斯自己的一句话作为结尾——这句话可能是整本书中最被低估的洞察:
“给我看表数据,往往就不再需要流程图,程序结构是非常清晰的。实际上,数据的表现形式是编程的根本。”
在1975年,这是一个关于内存优化的技术建议。在今天,这是关于系统复杂性本质的哲学洞察。
无论是控制程序大小、设计微服务边界,还是构建数据平台——当我们被复杂性淹没时,答案往往不在"怎么做"的流程图里,而在"是什么"的数据结构中。 重新表达数据,就是重新理解问题。理解问题,才能找到真正优雅的解决方案。
诺亚方舟的教训从未过时:在有限的空间里装载整个世界,需要的不是更大的方舟,而是更聪明的排列。

160

被折叠的 条评论
为什么被折叠?



