初步说明:对于 Knuth 风格的文学编程(即在阅读 WEB 或 CWEB 程序时),Knuth 设想的“真实”程序既不是“源”<code>.w 文件,也不是生成的(纠结的).c
文件,但排版(编织)输出。最好将源.w
文件视为生成它的一种手段(当然还有.c
提供给编译器的源文件)。(如果你手边没有 cweave 和 TeX;我已经在这里排版了其中的一些程序;这个程序DLX1 在这里。)
所以在这种情况下,我将代码中的位置描述为 DLX1 的模块 25,或子程序“cover”:

无论如何,回到实际问题:请注意,这(DLX1)是为计算机编程艺术编写的程序之一. 因为每年报告一个程序“秒”或“分钟”所花费的时间变得毫无意义,他报告了一个程序花费了多少“mems”加上“oops”,这是由“mems”主导的,即对 64 位字的内存访问次数(通常)。所以这本书包含诸如“这个程序在 3.5 吉游戏的运行时间内找到这个问题的答案”这样的陈述。此外,这些陈述基本上是关于程序/算法本身的,而不是由特定版本的编译器为特定硬件生成的特定代码。(理想情况下,当细节非常重要时,他会在 MMIX 或 MMIXAL 中编写程序并分析其在 MMIX 硬件上的操作,但这种情况很少见。)计算 mems(如上报告)是插入o
和oo
程序中的指令。请注意,对于多次执行的“内循环”指令(例如本例中的子程序cover
中的所有内容),正确处理这一点更为重要。
这在第 1.3.1 节(分册 1的一部分)中有详细说明:
定时。[…] 程序的运行时间不仅取决于时钟频率,还取决于可以同时活动的功能单元的数量以及它们的流水线化程度;它取决于用于在执行指令之前预取指令的技术;它取决于用于产生 2 64 个虚拟字节错觉的随机存取存储器的大小;它取决于缓存和其他缓冲区等的大小和分配策略等。
出于实际目的,MMIX
通常可以通过为每个操作分配固定成本来令人满意地估计程序的运行时间,该成本基于在具有大量主内存的高性能机器上获得的近似运行时间;所以这就是我们要做的。假设每个操作采用整数 υ,其中 υ(发音为“oops”)是表示流水线实现中的时钟周期时间的单位。尽管 υ 的值随着技术的进步而降低,但我们始终跟上最新的进步,因为我们以 υ 为单位测量时间,而不是以纳秒为单位。我们估计的运行时间也将假定取决于程序使用的内存引用或内存的数量;这是加载和存储指令的数量。例如,我们假设每个LDO
(load octa) 指令成本 µ + υ,其中 µ 是内存引用的平均成本。程序的总运行时间可能会报告为 35µ+ 1000υ,意思是“35 mems 加上 1000 oops”。μ/υ比值多年来一直在稳步增加;没有人确切知道这种趋势是否会持续下去,但经验表明 µ 和 υ 值得独立考虑。
他当然明白与现实的区别:
尽管我们经常使用表 1 的假设来估计运行时间,但我们必须记住,实际运行时间可能对指令的顺序非常敏感。例如,如果我们在发出命令和需要结果之间找到 60 件其他事情要做,那么整数除法可能只需要一个周期。如果多个 LDB(加载字节)指令引用相同的八字节,则它们可能只需要引用一次内存。然而,加载命令的结果通常不能在紧随其后的指令中使用。经验表明,有些算法可以很好地处理高速缓存,而另一些则不行;因此 µ 并不是真正的常数。甚至指令在内存中的位置也会对性能产生重大影响,因为有些指令可以与其他指令一起获取。[…] 只有元模拟器可以提供有关程序在实践中的实际行为的可靠信息;但这样的结果可能难以解释,因为可能有无限多的配置。这就是为什么我们经常求助于表 1 中更简单的估计。
最后,我们可以使用 Godbolt 的Compiler Explorer查看典型编译器为此代码生成的代码。(理想情况下,我们会查看 MMIX 指令,但由于我们无法做到这一点,所以让我们满足于那里的默认设置,这似乎是 x68-64 gcc 8.2。)我删除了所有的o
s 和oo
s。
对于代码版本:
/*o*/ t = nd[cc].len - 1;
/*o*/ nd[cc].len = t;
第一行生成的代码是:
movsx rax, r13d
sal rax, 4
add rax, OFFSET FLAT:nd+8
mov eax, DWORD PTR [rax]
lea r14d, [rax-1]
第二行是:
movsx rax, r13d
sal rax, 4
add rax, OFFSET FLAT:nd+8
mov DWORD PTR [rax], r14d
对于代码版本:
/*o ?*/ nd[cc].len --;
生成的代码是:
movsx rax, r13d
sal rax, 4
add rax, OFFSET FLAT:nd+8
mov eax, DWORD PTR [rax]
lea edx, [rax-1]
movsx rax, r13d
sal rax, 4
add rax, OFFSET FLAT:nd+8
mov DWORD PTR [rax], edx
正如您所看到的(即使对 x86-64 程序集了解不多)只是前一种情况下生成的代码的串联(除了使用 registeredx
而不是r14d
),因此在一行中写入减量并没有保存你任何mems。特别是,将其视为单个是不正确的,尤其是cover
在这种算法中被称为大量次数的情况(为精确覆盖而跳舞的链接)。
所以 Knuth 写的版本是正确的,因为它的目标是计算 mem 的数量。正如您所观察到的,他也可以写oo,nd[cc].len--;
(数两个 mem),但在这种情况下,乍一看它可能看起来像一个错误。(顺便说一句,在您的示例中,oo,nd[k].len--,nd[k].aux=i-1;
两个 mem 的问题来自 load 和 store in --
;而不是两个商店。)