# 第 9 章 · 削足适履

> 程序空间是用户成本的一部分，必须像硬件管元器件数量那样设规模预算；而真正能大幅省下空间的，是重新表达数据。

---

LLMS 索引： [llms.txt](/llms.txt)

---

题记借西德尼·史密斯的话调侃作者：他"应该瞪大眼睛紧盯着诺亚，然后……好好学习一下，看他们当时是怎样把那么多东西塞进一个小小的方舟上的"。这一章讲的就是怎么把十磅东西塞进五磅的口袋。

## 这一章讲了什么 {#summary}

第一部分把"程序空间"还原成一笔钱。他用 IBM 的 APL 交互式系统算账：软件租金每月 400 美元，运行时至少占用 160K 字节内存，而在 Model 165 上内存租金约每千字节每月 12 美元。若程序全天可用，用户要付 400 美元的软件费加 1920 美元的内存租用费；每天只用 4 小时，则是 400 加 320 美元。**内存不是免费的抽象资源，它按月计费。**

由此他反驳了一句当年流传很广的抱怨——"2M 内存的机器，操作系统就占了 400K"。他说这种批评"就好像批评波音 747 飞机，仅仅因为它耗资 2700 万美元一样无知"：首先要问的是"它能干什么"，这笔内存租金换来的易用性和系统利用率，是否比投在其他硬件、编程人员或应用上更有效。当系统设计者判断常驻内存对用户比加法器、磁盘更有价值时，把硬件实现的一部分移到内存里是负责任的做法；反过来才是。所以他的立场是：**不能一边提倡软硬件紧密集成，一边只拿规模本身去指责软件**。

但这不是为臃肿开脱。规模既然是用户成本的大头，开发人员就必须像硬件工程师设定元器件数量目标那样，为软件设定规模目标、控制规模、寻找缩减办法。他的结论很克制："规模本身不是坏事，但不必要的规模是不可取的。"

第二部分"规模控制"是 OS/360 的痛苦教训，三条都很具体。

第一条：**只给核心程序设规模目标是不够的，必须把所有方面的规模编入预算**。OS/360 是磁盘驻留系统，开发者对自由廉价的磁盘访问感到欣喜，却没有同时设定**访问预算**。后果是：当程序员发现自己的核心模块超标时，会把单元拆成覆盖模块——这个动作本身就增大了程序整体规模、降低了运行速度。更要命的是"我们的管理控制系统既没有度量，也没有捕获这些问题"：每个人汇报的内核大小都在目标之内，所以没人发现规模出了事。直到性能模拟程序跑起来，暴露出 Fortran H 每分钟只能模拟编译 5 条语句、控制程序模块大量磁盘切换，问题才现形。他提炼出的道理是对称的——**像制定驻留空间预算一样制定总体规模预算，像制定规模预算一样制定后台存储访问的预算**。

第二条：**在指明模块有多大的同时，要确切定义模块的功能**。因为他们的做法是先分配空间预算、后分配功能，于是任何规模超标的程序员都会检查"哪部分能扔给别人"——结果是控制程序管理的缓冲区变成了用户空间的一部分，而所有控制模块都这么做，彻底影响了系统的稳定与安全性。

第三条更深刻，也更像人话：项目规模太大、缺乏管理和沟通，以至于**每个团队成员都以为自己是"争取小红花的学生"，而不是构建系统软件产品的人员**。为了满足各自的目标，人人都在局部优化自己的程序，很少有人停下来考虑对客户的整体影响。作者断言，对大型项目来说，这种导向加上缺乏沟通是最大的危险。对策有两层：系统结构师必须在整个实现过程中保持持续警觉，确保连贯的系统完整性；而**培养开发人员"从系统整体出发、面向用户"的态度，是软件编程管理人员最重要的职能**。

第三部分"空间技能"是方法层面。既然预算和控制本身不会让程序变小，就需要创造性和技能。他给了三个抓手：

- **用功能交换尺寸**：为程序保留多少可选项目，是个策略问题。要么提供很多各自占少量空间的功能，要么设计成若干选项分组、按组裁剪程序——任何一组特殊选项被合并成整体计价时，需要的空间都更少。他用汽车打比方：把照明灯、点烟器和时钟作为整套配件标价，比单独提供便宜。所以设计者必须决定**用户可选项目的粗细程度**。
- **警惕小模块的隐性代价**：当内存被限制在一定范围内时，即使最细密的功能模块，其适用范围也难以推广。在最小规模的系统里，大部分模块被覆盖，相当一部分常驻空间要拿去做交换页面或暂存区，于是**那个尺寸决定了所有模块的尺寸**。把功能分解成很小的模块本身就要耗费空间、降低性能；等大型系统（有 20 倍暂存空间）来复用这些模块时，省下的只是访问次数，模块过小造成的空间与速度损失依旧存在——这实际上限制了"用小系统的模块搭出高效大系统"的可能。
- **空间—时间折中**：对给定功能，空间越多速度越快，这条在很大范围内成立——正是它让空间预算成为可能。

此外他补充了两条组织性的做法：确保编程技能得到**培训**而不是只依赖个人天赋与经验（换新语言或新机器时尤其重要）；以及**开发公共库**——队列、搜索、散列、排序的例程或宏库，每项功能至少准备两个实现，一个跑得快、一个短小精炼，而这项工作可以与系统设计并行。

最后一部分"数据的表现形式是编程的根本"是全书最有洞察力的段落之一。作者说，精湛的技艺出自创造，而技艺改进的结果**往往是战略突破，而不只是技巧提高**。这种突破有时来自新算法（他举 Cooley-Tukey 快速傅立叶变换，或把比较算法复杂度从 n² 降到 n log n），但更普遍的是来自**数据或表的重新表达**：

> 如果提供了程序流程图，而没有表数据，我仍然会很迷惑。而给我看表数据，往往就不再需要流程图，程序结构是非常清晰的。

他举了两个例子：一位年轻人负责 IBM 650 的精密控制台解释器，发现用户交互频率低、速度慢，但空间极其昂贵，于是写了一个**解释器的解释器**，最终把程序空间压缩到不可思议的程度；Digitek 那个小而优雅的 Fortran 编译器用极为密集、专业化的代码表达自己，以至于不再需要外部存储——解码要损失时间，却因为避免了输入输出反而得到 10 倍补偿。他的结论是：被空间逼到墙角的程序员，常常能通过**从代码里挣脱出来、回头审视程序的数据**，得到极好的结果。

## 我的判断 {#reading}

- 这一章表面上讲内存，实质讲的是**把一个被当成"技术内部问题"的东西还原成用户成本**。这与第 4 章的"易用性优先"是同一条思路：凡是用户最终要付钱或付出代价的东西，就必须进入设计目标，而不是留给实现者凭品味裁量。
- "波音 747"那段反驳我认为今天依然稀缺。资源占用高本身不是罪，问题是**它换来了什么**。反过来，一个占用极小但不好用的系统同样是失败的设计——作者没有站在任何一边，而是要求算总账。
- 三条 OS/360 教训里，第一条最技术、第三条最人性，但第二条我读着最心惊：**先给空间、后给功能，会诱导每个人把复杂度推给邻居**。缓冲区从控制程序挪进用户空间，单看每个模块都合规，合起来就危及稳定与安全。这是典型的"局部合规、全局失守"，任何指标体系都可能诱发它。
- "每个人都以为自己是争取小红花的学生"这句话，是我在本书里见过最生动的组织诊断。它解释了为什么一群聪明人会把系统做坏——不是能力问题，而是**激励方向与产品方向不一致**。
- 我对"空间—时间折中在很大范围内成立"这条持保留：在今天的分布式与缓存场景里，空间换时间的适用区间被架构局部性扭曲得很厉害，多占内存未必更快。但它作为**预算机制**（给每个功能一个空间额度，逼你在设计期做取舍）依然有效，这与第 5 章末尾"给每个功能分配 m 字节、n 微秒"是同一个手法。
- "数据的表现形式是编程的根本"这句我完全认同，并且认为它是全章的落点：前文的预算、控制和技能都是防守手段，只有重新表达数据才是真正的进攻手段。程序的复杂度往往不在代码里，而在数据结构是否选对。

## 和我手上的工作有什么关系 {#relevance}

- 我写的是嵌入式与 MCU 上的代码，这一章的账算得格外直白：RAM 和 Flash 就是钱。以前我按"能不能塞进去"判断实现，现在会多问一句每千字节换来了什么功能、这个功能用户是否真的用得到——这正好对应作者说的"用功能交换尺寸"。
- 三条 OS/360 教训里，第三条几乎逐字命中我的工作方式：一个人做项目时，很容易退化成"把每个模块写好看"的局部优化，而系统整体的一致性和用户的真实感受没人负责。作者给出的对策——**结构师保持持续警觉 + 培养面向整体的态度**——在单人项目里就是要求我阶段性地从代码里退出来，检查系统而不是模块。
- 公共库那一条我打算补上：给这个站点和手上的嵌入式项目分别建一个可复用的函数与片段库（队列、环形缓冲、排序、状态机模板），每个功能至少两种实现——一个求速度、一个求尺寸。作者说这项工作可以与系统设计并行，恰好适合交给 Agent 批量完成。
- "数据的表现形式是编程的根本"是我这章收获最大的一句。回看自己写过的驱动代码，最难的 bug 几乎都出在数据结构选错（缓冲区与索引的所有权、状态的存放位置）而不是算法写错。以后遇到复杂度异常的地方，先审数据，不先审逻辑。
- 空间预算这个动作也值得移植到内容工作上：这个站点每加一个栏目、一门语言、一个主题，都有维护成本。给站点也设一个"规模预算"，比事后清理要省力得多。

## 一句话记住 {#takeaway}

规模不是罪，不必要的规模才是；要像硬件工程师管零件那样为软件设规模与访问预算，但真正把程序变小、变清楚的，是重新表达它的数据。
