跳转到主要内容

第 9 章 · 削足适履

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

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

这一章讲了什么

第一部分把"程序空间"还原成一笔钱。他用 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 倍补偿。他的结论是:被空间逼到墙角的程序员,常常能通过从代码里挣脱出来、回头审视程序的数据,得到极好的结果。

我的判断

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

和我手上的工作有什么关系

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

一句话记住

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