如果一道题有一个清楚的公式,我们很自然地期待:把公式翻译成代码,计算机照着做就好了。公式更短,似乎也应该更省事。
但当你打开一些高效程序,常会看见另一种景象:数字被重新排列,数据被拆成块,一件事被分成好几轮,有时还先做一份看起来多余的准备。我们为什么把简单的数学写得这么复杂?
预计用时:30 分钟。只需看懂加法、乘法和一个数字表。矩阵与硬件知识将在需要时展开。
打开 “看见计算”可视化实验台,先用边长 4,选择“逐格完成”。A、B 是输入,C 是正在形成的结果。C 的一个格子来自 A 的一行与 B 的一列:对应数字相乘,再把乘积相加。
例如两张小表:
A = [1 2] B = [5 6] C 的左上格 = 1×5 + 2×7 = 19
[3 4] [7 8] C 的右上格 = 1×6 + 2×8 = 22
其余两个格子同样计算,得到 43 和 50。这就是本章使用的矩阵乘法。先不需要给它添加更多抽象意义。
点击“走一步”。现在你看到的每一步是一次模型中的数组访问。数要被取到合适的位置,结果才有机会形成。选择“分块复用”,看看访问路线变了什么。
先选一句你更相信的话,再找一个能区分它们的操作:
固定矩阵、块大小和模型缓存,只切换顺序,观察乘法数与装入次数。再把缓存容量调大,观察刚才的差别是否还在。让第二个变化来检验自己的解释,不能只截取第一次符合预期的画面。
模型能帮我们看清一个机制。随后在页面底部测真实程序,结果可能更复杂:浏览器还会优化循环,机器还有多层缓存,后台任务也在运行。动画的绿色和橙色不曾读过本机 CPU 的缓存计数器。
数学上,“求和”可以写成一个符号。程序里,它需要确定加哪些数、以什么顺序加、把中间结果放在哪里。硬件上,还要面对数只能用有限位表示、数据不总在计算单元身边、部分工作必须等待之前的结果。
于是,“便宜”要重新说明:是在数加乘次数,还是在数访问次数?比较的是单个请求等多久,还是一秒能处理多少批数据?要求每一位都相同,还是允许指定误差?
这些选择仍然可以用数学描述。我们会逐渐形成更适合当前机器的成本模型,而不是放弃数学判断。
| 现场 | 动一下什么 | 追问 |
|---|---|---|
| 数的缝隙 | 数的尺度、加法顺序 | 怎样表示一个数,怎样算得可信 |
| 数据的旅程 | 循环顺序、块与缓存容量 | 哪些工作消耗在搬运上 |
| 等待的形状 | 分工数量、串行工作、协调成本 | 多个计算者什么时候有用 |
| 空白的代价 | 非零比例 | 改一种表示是否真的划算 |
| 何时足够 | 目标容差 | 近似到什么程度可以停止 |
前面三个单元之后有一次代码解析,随后继续讨论规模、预处理与近似。章末把一个具体任务整理成独立算法课堂可以接手的输入、方案和证据。本章的目的,是让你比较算法时能说清它面对哪台机器、哪种数据和什么要求。
先进入 一个数为什么加不上去。在比较谁算得快之前,我们先看它究竟算出了什么。