“能跑”以后,怎样知道它好不好
早期程序设计紧贴具体机器。字长、磁带、寄存器和指令表决定写法,一个聪明技巧可能只在某台设备上有效。与此同时,数学论文可以证明抽象过程,却离实际存储布局和指令成本很远。于是“程序 A 比程序 B 好”常靠资深者直觉:样例跑得快、代码较短、写法看起来漂亮。
Knuth 把问题推进一层:算法必须先被精确描述并证明做对了什么,然后才谈它用多少资源;成本还要随着输入规模 n 展开,而不是只报一次秒表数。这样既不能离机器太远,也不能被某款机器绑死。《The Art of Computer Programming》从 1968 年第一卷 *Fundamental Algorithms* 起,试图把基本概念、信息结构、程序实现与数学分析放进同一套长期参照系。
让算法同时走过证明纸与寄存器
- 基线:机器手册式编程按指令和设备组织知识,代码能运行便算完成;这是旧共识。
- 基线:技巧集记录聪明程序片段,却少有统一成本模型和适用条件。
- 基线:纯数学证明研究抽象过程,可能不追踪地址、存储与实际基本操作。
- 基线:一次性 benchmark 用某台机器的秒数排胜负,输入分布和规模一变便难外推。
借来的框是当时散落在论文、程序库和工程经验中的算法,外加组合数学、概率、递推关系与汇编机器模型。作者工序是把同一方法交给多重检验:先用文字或形式步骤规定输入、状态和终止;再给出可执行的低层实现;接着选定基本操作,求最好、平均或最坏情况下的次数;最后用练习、答案、文献史和交叉索引让读者复现与扩展。
MIX 是这道加工的重要支架。它不是宣称未来计算机都长成同一硬件,而是一台明确的假想机器:算法落到寄存器和内存后,隐去的成本会重新出现。若只用自然语言,“把元素插入适当位置”可能掩盖搜索与搬移;若只用真实商业机型,知识又会随产品淘汰。假想机器在抽象与实现之间提供一把稳定尺。
操作计数则把“快”拆成函数。对输入规模 n,比较、交换、访存或算术操作各发生多少次?精确式能解释小规模常数,渐近阶显示规模放大后的主导项。平均分析还迫使作者声明输入分布;最坏分析则给保障。它们不互相取代:一个方法可以渐近更好却常数更差,也可以平均漂亮但在敌对输入上崩坏。
许多算法和数学工具已有作者,Knuth 的伦理正是细查来源而非据为己有;但作者工序把零散知识变成一门可累计的“算法分析”。ACM 后来把其 Turing Award 直接授予算法分析、语言设计和这套书的组织性贡献。加工的强度体现在标准改变:程序之美必须同正确性、效率、清晰性和实现细节一起受审,不能只靠聪明一招。
习题系统也不是页末装饰。难度分级让读者知道任务是复述、推导还是研究问题;答案使自学形成闭环;勘误奖励把书保持为可被读者持续测试的对象。知识不是出版即封存的权威,而是带版本、来源和错误入口的工程。这种维护方式与书中算法观一致:规格清楚,失败可定位,改进可累积。
程序开始拥有随 n 变化的命运
戴上这副框,比较算法不再先问使用哪种语言。研究者先找问题规模、输入结构和基本操作,再看成本怎样增长。一次测得 10 毫秒只能说明某次实现;一个经推导的 n log n 或 n² 关系,才能解释输入扩大十倍后为何胜负翻转。机器会换,增长规律和证明依赖仍可迁移。
理论与实践也不再分居。证明忽略的常数、缓存和表示会在实现中反击;工程技巧若无法进入明确模型,就难判断是否普遍。Knuth 的“art”因此不是反科学的灵感崇拜,而是受约束的判断:知道何时值得优化,怎样选择表示,何时精确分析比大 O 更有信息,以及怎样把一个正确思想写成他人可检查的程序。
可反驳点是成本预测。冻结输入生成法、实现和计数单位后,分析应解释操作数随 n 的变化。若预测 n log n 的方法在扩大规模时稳定呈二次增长,证明或模型至少有一处错;若只在挑选过的样例上获胜,平均假设不成立。不能用硬件噪声替操作计数挡箭,因为书的框正要求把机器相关时间与算法结构分层。
从技巧柜换到算法成本函数
轴名:程序评价(左极:一次能跑|右极:跨规模可分析)
右极
│
● 纯数学过程 │ ★ Knuth
│
左极 ──● 机器技巧(旧共识)──● benchmark──────── 右极
│
左极
移动:换轴——由“这次跑多快”转向“成本函数怎样随 n 增长”
图注:横轴从一次实现的成败走向跨输入规模的可比较分析;纵向区分脱离机器的证明与落到明确机器模型的程序。Knuth 位于两者交会处,让理论和实现互相揭短。
作者盲点来自稳定机器尺的代价。MIX 能揭示低层成本,却无法预见缓存层级、并行、分布式通信、能耗与现代加速器;操作次数也不等于真实延迟。全书的百科雄心会让快速变化的领域永远未完,作者个人选择亦影响哪些算法进入正典。框可迁移,具体成本模型必须随计算环境更新。
标准库排序在六千七百万项时越界
新位置是 OpenJDK 标准库采用的 TimSort。它已经通过日常测试并广泛部署,但实现需要用一个有限数组保存待合并 run 的长度。按“规格—不变量—实现—最坏输入”这套审查,预测是:若 `mergeCollapse` 没有持续维持 run 栈的增长不变量,普通 benchmark 可以全绿,精心构造的长数组却会把栈顶推过预留边界;形式证明应在运行事故前卡在相应 proof obligation。
证伪条件是:实际 OpenJDK 实现能对所有合法数组维持该不变量并机械证明无越界,而同一版本仍可由合法输入稳定触发 `ArrayIndexOutOfBoundsException`;那会说明规格或证明没有约束到真实程序。现实对照来自 OpenJDK 官方 core-libs-dev 的 JDK-8072909 review thread:研究者指出旧修复只扩大 `runLen` 数组并不充分,给出的 67,108,864 项测试可重复触发索引 40 越界,问题影响 JDK 7u76、8 和 9,后来被标记为已修复。随后同行评审的 KeY 案例把固定版本写成方法契约,机械证明正常终止且无该异常;论文同时说明,发现错误的正是原实现无法完成的验证任务。结果:命中。它不证明所有形式化都划算,却具体支持了 Knuth 式标准:正确算法名、好平均性能和大量成功运行,不能替代实现不变量及最坏规模的审计。
从首卷到终身工程
- Knuth 的 Stanford 官方 TAOCP 页面确认系列、1968 年第一版宣传册、MIX/MMIX、卷册结构、勘误与读者反馈机制。
- 该站保存的 1968 年出版社 brochure 用于校准第一卷当时的定位,避免把后来 Volume 4 与 literate programming 倒灌进首版。
- ACM Turing Award 页面说明本书从编译器计划扩成组织计算机科学知识的系列,并明确其 quantitative approach 与算法比较目标。
- ACM 官方书目确认 Volume 1 为 1968 年 *Fundamental Algorithms*,涵盖 Basic Concepts 与 Information Structures,后续卷册年份另列。
- OpenJDK 官方 core-libs-dev review thread 与《Journal of Automated Reasoning》验证论文共同记录 TimSort 越界、复现输入和机械验证后的修复。
- 材料等级:初拆。作者站、首版材料与 ACM 档案足以拆出“实现+分析+文献”框;未逐章复核第一版全部算法与习题,故不标完整拆书。
资料校准
- https://cs.stanford.edu/~knuth/taocp.html
- https://cs.stanford.edu/~knuth/brochure.pdf
- https://amturing.acm.org/award_recipient/knuth_1013846.cfm
- https://amturing.acm.org/bib/knuth_1013846.cfm
- https://mail.openjdk.org/pipermail/core-libs-dev/2015-February/031405.html
- https://link.springer.com/article/10.1007/s10817-017-9426-4