方案:为什么内部定义比外部定义快?

时间:2018-10-07 13:26:17

标签: performance scheme racket

我尝试在下面运行程序

(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上运行,结果是相似的。为什么会这样?

3 个答案:

答案 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)并从该表中获取evenodd-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时,您只是从环境中获取它(即使没有很酷的技巧,它也只需花费一次内存取消引用即可。)

一个有趣的实验是将oddeven都定义为绑定变量,因此所有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] letlambda应用程序的语法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.rktexternal.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对此发表评论将很有趣。