# 第 9 章 削足适履(Ten Pounds in a Five-Pound Sack)

他应该瞪大眼睛盯着诺亚,..好好学习,看他们是怎样把那么多东西装到一个小小的方舟上的。

——西德尼·史密斯,爱丁堡评论

the author should gaze at Noah, and ... learn, as they did in the Ark, to crowd a great deal of matter into a very small compass.

——SYDENY SMITH. EDINBURGH REVIEW

# 作为成本的程序空间

程序有多大?除了运行时间以外,它所占据的空间也是主要开销。这同样适用于专用开发的程序,用户支付给开发者一笔费用,作为必要分担的开发成本。考虑一下 IBM APL 交互式软件系统,它的租金为每月 400 美金,在使用时,它至少占用 160K 字节的内存。在 Model 165 上,内存租金大约是 12 美金/每月每千字节。如果程序在全部时间内都可用,他需要支付 400 美元的软件使用费和 1920 美金的内存租用费。如果某个人每天使用 APL 系统 4 小时,他每月需要支出 400 美元的软件租金和 320 美元的内存租用费。

常常听到的一个"可怕的"谈论是在 2M 内存的机器上,操作系统就需要占用 400K 内存。这种言论就好像批评波音 747 飞机,仅仅因为它耗资两千七百万美元一样无知。我们首先必须问的是"它能干什么?"。对于所耗费的资金,获得的易用性和性能是什么?投资在内存上的每月 4800 美元的租金能否比用在其他硬件、编程人员、应用程序上更加有效?

当系统设计者认为对用户而言,常驻程序内存的形式比加法器、磁盘等更加有用时,他会将硬件实现中的一部分移到内存上。相反的,其他的做法是非常不负责任的。所以,应该从整体上来进行评价。没有人可以在自始至终提倡更紧密的软硬件设计集成的同时,又仅仅就规模本身对软件系统提出批评。

由于规模是软件系统产品用户成本中如此大的一个组成部分,开发人员必须设置规模的目标,控制规模,考虑减小规模的方法,就像硬件开发人员会设立元器件数量目标,控制元器件的数量,想出一些减少零件的方法。同任何开销一样,规模本身不是坏事,但不必要的规模是不可取的。

# 规模控制

对项目经理而言,规模控制既是技术工作的一部分,也是管理工作的一部分。他必须研究用户和他们的应用,以设置将开发系统的规模。接着,把这些系统划分成若干部分,并设定每个部分的规模目标。由于规模-速度权衡方案的结果在很大的范围内变化,规模目标的设置是一件颇具技巧的事情,需要对每个可用方案有深刻的了解。聪明的项目经理还会给自己预留一些空间,在工作推行时分配。

在 OS/360 项目中,即使所有的工作都完成得相当仔细,我们依然能从中得到一些痛苦的教训。

首先,仅对核心程序设定规模目标是不够的,必须把所有的方面都编入预算。在先前的大多数操作系统中,系统驻留在磁带上,长时间的磁带搜索意味着它无法自如地运用在程序片段上。OS/360 和它的前任产品 Stretch 操作系统和 1410-7010 磁盘操作系统一样,是驻留在磁盘上的。它的开发者对自由、廉价的磁盘访问感到欣喜。而如果使用磁带,会给性能带来灾难性的后果。

在为每个单元设立核心规模的同时,我们没有同时设置访问的目标。正如大家能想到的一样,当程序员发现自己的单元核心未能达到要求时,他会把它分解成链接库。这个过程本身增加了程序整体的规模,并降低了运行速度。最重要的是,我们的管理控制系统既没有度量,也没有捕获这些问题。每个人都汇报了核心的大小,都在目标范围之内,所以没有人发现规模上的问题。

幸运的是,OS/360 性能仿真程序投入使用的时间较早。第一次运行的结果反映出很大的麻烦。Fortran H,在带磁鼓的 Modal 65 上,每分钟模拟编译 5 条语句!嵌入的例程显示控制程序模块进行了很多次磁盘访问。甚至使用频繁的监控模块也犯了很多同样的错误,结果很类似于页面的切换。

第一个道理很清楚:和制订驻留空间预算一样,应该制订总体规模的预算;和制订规模预算一样,应该制订后台存储访问的预算。

下一个教训十分类似。在每个模块分配功能之前,已编制了空间的预算。其结果是,任何在规模上碰到问题的程序员,会检查自己的代码,看是否能将其中一部分扔给其他人。因此,控制程序所管理的缓冲区成为了用户空间的一部分。更严重的是,所有的控制模块都有相同的问题,彻底影响了系统的稳定和安全性。

所以,第二个道理也很清晰:在指明模块有多大的同时,确切定义模块的功能。

第三个更深刻的教训体现在以上的经验中。项目规模本身很大,缺乏管理和沟通,以至于每个团队成员认为自己是争取小红花的学生,而不是构建系统软件产品的人员。为了满足目标,每个人都在局部优化自己的程序,很少会有人停下来,考虑一下对客户的整体影响。对大型项目而言,这种导向和缺乏沟通是最大的危险。在整个实现的过程期间,系统结构师必须保持持续的警觉,确保连贯的系统完整性。在这种监督机制之外,是实现人员自身的态度问题。培养开发人员从系统整体出发、面向用户的态度是软件编程管理人员最重要的职能。

# 空间技能

空间预算的多少和控制并不能使程序规模减小,为实现这一目标,它还需要一些创造性和技能。

显然,在速度保持不变的情况下,更多的功能意味着需要更多的空间。所以,其中的一个技巧是用功能交换尺寸。这是一个较早的、影响较深远的策略问题:为用户保留多少选择?程序可以有很多的选择功能,每个功能仅占用少量的空间。也可以设计成拥有若干选项分组,根据选项组来剪裁程序。任何一系列特殊选项被合并在一起进行分组时,程序需要的空间较少。这很像小汽车。如果把照明灯、点烟器和时钟作为整个配件来标明价格,则成本会比单独提供这些选择所需要的成本低。所以,设计人员必须决定用户可选项目的粗细程度。

在内存大小一定的情况下进行系统设计时,会出现另外一个基本问题。内存受限的后果是即使最小的功能模块,它的适用范围也难以得到推广。在最小规模的系统中,大多数模块被覆盖(overlaid),系统的主干占用的空间,会被用作其他部分的交换页面。它的尺寸决定了所有模块的尺寸。而且将功能分解到很小的模块会耗费空间和降低性能。所以,当可以提供 20 倍临时性空间的大型系统使用这些模块时,节省的也仅仅是访问次数,仍然会因为模块的规模引起空间和速度上的损失。这样后果其实是——很难用小型系统的模块构造出非常高效的系统。

第二个技能是考虑空间-时间的折衷。对于给定的功能,空间越多,速度越快。这一点在很大的范围内都适用。也正是这一点使空间预算成为可能。

项目经理可以做两件事来帮助他的团队取得良好的空间-时间折衷。一是确保他们在编程技能上得到培训,而不仅仅是依赖他们自己掌握的知识和先前的经验。特别是使用新语言或者新机器时,培训显得尤其重要。熟练使用往往需要快速的学习和经验的广泛共享,也许它应该伴随特别的新技术奖励或者表扬。

另外一种方法是认识到编程需要技术积累,需要开发很多公共单元构件。每个项目要有能用于队列、搜索和排序的例程或者宏库。对于每项功能,库至少应该有两个程序实现:运行速度较快和短小精炼的。上述的公共库开发是一件重要的实现工作,它可以与系统设计工作并行进行。

# 数据的表现形式是编程的根本

创造出自精湛的技艺,精炼、充分和快速的程序也是如此。技艺改进的结果往往是战略上的突破,而不仅仅是技巧上的提高。这种战略上突破有时是一种新的算法,如快速傅立叶变换,或者是将比较算法的复杂度从 n2 降低到 n log n。

更普遍的是,战略上突破常来自数据或表的重新表达——这是程序的核心所在。如果提供了程序流程图,而没有表数据,我仍然会很迷惑。而给我看表数据,往往就不再需要流程图,程序结构是非常清晰的。

很容易就能找到重新表达所带来好处的例子。我记得有一个年轻人承担了为 IBM650 开发精细的控制台解释器的任务。他发现用户交互得很慢,并且空间很昂贵。于是,他编写了一个解释器的解释器,使得最后程序所占的空间减少到不可思议的程度。Digitek 小而优雅的 Fortran 编译器使用了非常密集的、专业化的代码来表达自己,以至于不再需要外部存储。

对这种表达方式解码会损失一些时间,但由于避免了输入-输出,反而得到了十倍的补偿。(Brooks 和 Iverson 第六章结尾的练习以及 Knuth 的练习 2——自动数据处理 1 包含了许多类似的例子。)

由于缺乏空间而绞尽脑汁的编程人员,常常能通过从自己的代码中挣脱出来,回顾、分析实际情况,仔细思考程序的数据,最终获得非常好的结果。实际上,数据的表现形式是编程的根本。