我尝试在下面运行程序
(define (odd-internal x)
(define (even x)
(if (zero? x)
#t
(odd-internal (sub1 x))))
(if (zero? x)
#f
(even (sub1 x))))
(define (odd-external x)
(if (zero? x)
#f
(even (sub1 x))))
(define (even x)
(if (zero? x)
#t
(odd-external (sub1 x))))
(begin (display "Using internal definition\n")
(time (odd-internal 40000000)))
(begin (display "Using external definition\n")
(time (odd-external 40000000)))
这是球拍中的结果
Using internal definition
cpu time: 166 real time: 165 gc time: 0
#f
Using external definition
cpu time: 196 real time: 196 gc time: 0
#f
您可以看到使用内部定义要快得多。我尝试在Chez Scheme上运行,结果是相似的。为什么会这样?
答案 0 :(得分:1)
您的数字太小而无意义。绝对而言,166毫秒与196毫秒之间的差异很小。谁知道其他因素可能会影响这一点?虚拟机预热时间,内存分配差异或任何其他原因很容易导致该大小差异。可以肯定的是,您应该使数字更大。
在运行Racket v7.0的计算机上,我将参数从40000000
增加到1000000000
并运行了程序。内部定义案例的结果为2.361 s,外部定义案例的结果为2.212 s。考虑到上面列出的各种因素,这种差异太小而无意义。
基准测试非常困难,而在VM上运行并经过JIT编译的基准测试语言则更加困难。即使您考虑了热身和GC,运行大量迭代并取平均值,并且通常尝试做正确的事,但您获得的结果仍然几乎没有意义,正如2017年OOPSLA论文Virtual Machine Warmup Blows Hot and Cold所解释的那样:>
传统上,将具有实时(JIT)编译器的虚拟机(VM)分为两个阶段来执行程序:初始预热阶段确定程序的哪些部分将在JIT编译那些部分之前最受益于动态编译进入机器代码;随后,该程序被认为处于峰值性能的稳定状态。测量方法几乎总是丢弃在预热阶段收集的数据,因此报告的测量完全集中在峰值性能上。我们基于变更点分析引入了一种全自动统计方法,该方法使我们能够确定程序是否已达到稳定状态,如果达到稳定状态,则可以确定该程序是否代表了峰值性能。使用此工具,我们显示即使在最受控制的情况下运行,小型,确定性强,经过广泛研究的微基准测试通常也无法在各种常见VM上达到稳定的峰值性能状态。在3台不同的机器上重复我们的实验,我们发现最多43.5%的“ VM,基准”对始终达到峰值性能的稳定状态。
强调我的。确保正在测量自己认为要测量的东西。
答案 1 :(得分:1)
首先,这将取决于您的实现,因为嵌套定义可以以多种方式实现。在我的Chez Scheme 9.5设置中,当我使用奇数内部时,我的运行时间缩短了25%。
现在,为什么。发生这种情况是因为嵌套定义(即内部定义)与实际定义有很大不同。
在顶层使用define
时,您正在向自由变量表添加新记录。每当您尝试评估未绑定到任何lambda的变量时,都会在自由变量(哈希)表中查找该变量。这种搜索非常有效,但是比获取绑定变量要慢。因此,当您计算(odd-external 40000000)
并从该表中获取even
和odd-external
大约4000万次时-即使有缓存和其他有趣的东西,仍然可以完成。
相反,嵌套定义创建绑定变量。实现它们的一种方式是嵌套的lambda / let / letrec表达式。这样,odd-internal
函数将被转换为 [1] :
(define (odd-internal x)
(let ((even (lambda (x)
(if (zero? x)
#t
(odd-internal (sub1 x))))))
(if (zero? x)
#f
(even (sub1 x)))))
(这是Chez Scheme的简化)。
现在,每次您应用odd-internal
时,它仍然是一个自由变量,因此您可以对其进行散列并在自由变量表中找到它。但是,当您应用even
时,您只是从环境中获取它(即使没有很酷的技巧,它也只需花费一次内存取消引用即可。)
一个有趣的实验是将odd
和even
都定义为绑定变量,因此所有4000万变量获取都将受益于快速的绑定变量获取时间。与原来的25%相比,我看到了16%的改进。这是代码:
(define (odd-quick x)
(define (odd x) (if (zero? x) #f (even (sub1 x))))
(define (even x) (if (zero? x) #t (odd (sub1 x))))
(odd x))
[1] let
是lambda
应用程序的语法suger,因此您可以将代码读取为:
(define (odd-internal x)
((lambda (even)
(if (zero? x)
#f
(even (sub1 x))))
(lambda (x)
(if (zero? x)
#t
(odd-internal (sub1 x))))))
答案 2 :(得分:1)
我很惊讶这是一个区别,因此从Lexis的回答中,我将两个版本分别拆分为文件internal.rkt
和external.rkt
,并以这种方式对其进行编译和反编译:>
raco make external.rkt
raco decompile compiled/external_rkt.zo
这比在宏步进器中查看完全扩展的程序要走的更远。它看起来很难让人读懂,因此我将其最重要的部分进行了修饰:
(define (odd-external x1)
(if (zero? x1)
'#f
(let ((x2 (sub1 x1)))
(if (zero? x2)
'#t
(let ((x3 (sub1 x2)))
(if (zero? x3)
'#f
(let ((x4 (sub1 x3)))
(if (zero? x4)
'#t
(let ((x5 (sub1 x4)))
(if (zero? x5) '#f (even (sub1 x5))))))))))))
(define (even x1)
(if (zero? x1)
'#t
(let ((x2 (sub1 x1)))
(if (zero? x2)
'#f
(let ((x3 (sub1 x2)))
(if (zero? x3)
'#t
(let ((x4 (sub1 x3)))
(if (zero? x4)
'#f
(let ((x5 (sub1 x4)))
(if (zero? x5)
'#t
(let ((x6 (sub1 x5)))
(if (zero? x6)
'#f
(let ((x7 (sub1 x6)))
(if (zero? x7)
'#t
(odd-external (sub1 x7))))))))))))))))
这里没什么特别的。它将环展开一定的次数并保持恒定的折痕。请注意,我们仍然可以相互递归,展开是5倍和7倍。该常量甚至是常量折叠的,因此它用(even 399999995)
代替了我的调用,因此编译器也将代码运行了5转并放弃了。有趣的是内部版本:
(define (odd-internal x1)
(if (zero? x1)
'#f
(let ((x2 (sub1 x1)))
(if (zero? x2)
'#t
(let ((x3 (sub1 x2)))
(if (zero? x3)
'#f
(let ((x4 (sub1 x3)))
(if (zero? x4)
'#t
(let ((x5 (sub1 x4)))
(if (zero? x5)
'#f
(let ((x6 (sub1 x5)))
(if (zero? x6)
'#t
(let ((x7 (sub1 x6)))
(if (zero? x7)
'#f
(let ((x8 (sub1 x7)))
(if (zero? x8)
'#t
(odd-internal
(sub1 x8))))))))))))))))))
它不再相互递归,因为它在8次调用之后会自行调用。每轮进行8轮,而另一轮进行7轮,然后进行5。在两轮中,内部轮进行16轮,而另一轮进行12轮。内部轮的初始调用是(odd-internal '399999992)
,因此编译器放弃前8轮。
我想反编译器级别的功能旁边的代码是开放代码,并且每个步骤的代码都非常便宜,这使得调用数量增加了25%。毕竟,每次递归还需要再增加4次,这与计算时间的差异相吻合。这是基于观察的推测,因此Lexi对此发表评论将很有趣。