【问题标题】:Scheme's block structure efficiencyScheme的块结构效率
【发布时间】:2020-03-08 16:16:08
【问题描述】:

mean-square 定义为例:

(define (mean-square x y) 
    (define (square x) (* x x))
    (define (average x y) (/ (+ x y) 2))
    (average (square x) (square y)))

当我运行 (mean-square 2 4) 时,我正确地得到了 10

我的问题是,内部定义(在这个玩具案例中是 squareaverage)是否运行每次我通过解释器调用 mean-square 过程?如果是这样,那不是效率低下吗?如果不是,为什么?

【问题讨论】:

  • 现代编译器可以轻松处理这个问题。这是一种可能的方式:en.wikipedia.org/wiki/Lambda_lifting
  • 对于这种特殊情况,许多编译器会完全内联本地函数。例如,Racket 就可以。

标签: scheme lisp racket sicp


【解决方案1】:

如果代码有些天真地编译,可能会有一些开销。原因是内部函数是在一个全新的词法环境中定义的,该词法环境在每次进入函数时都会被新实例化。在抽象语义中,每次调用函数时,都必须捕获新的词法闭包并将其连接到该环境框架中的正确位置。

因此归结为编译器可以优化多少。例如,它可以注意到两个函数实际上都没有引用周围的词法环境。 (这些函数中的xy 引用指向它们自己的参数,而不是指向周围mean-square 的参数)。这意味着它们都被移动到顶层而不改变语义:

(define (__anon1 x) (* x x))

(define (__anon2 x y) (/ (+ x y) 2))

(define (mean-square x y)
    (define square __anon1)
    (define average __anon2)
    (average (square x) (square y)))

因为现在squareaverage 实际上是简单的别名(由编译器生成的全局实体的别名,编译器知道它不会被任何超出其控制的东西操纵),它们表示的值可以通过以下方式传播:

(define (mean-square x y)
  (__anon2 (__anon1 x) (__anon1 y)))

【讨论】:

  • new lexical closures have to be captured and wired into the correct spots in that environment frame 这就是我所害怕的,说得比我以前都好!感谢您的细心解释!
【解决方案2】:

这不是问题。当mean-square 过程被编译时,所有的嵌套过程也被编译。每次调用mean-square 过程时都不需要重新编译它们。

【讨论】:

    【解决方案3】:

    我认为其他答案可能已经说服您,您给出的案例确实不需要任何开销:本地定义可以直接编译掉。但值得考虑的是,系统如何处理无法完成的情况。

    考虑这样的定义:

    (define (make-searcher thing)
      (define (search in)
        (cond [(null? in)
               #f]
              [(eqv? (first in) thing)
               in]
              [else (search (rest in))]))
      search)
    

    好吧,本地的search 过程肯定不能在这里编译掉,因为它是从make-searcher 返回的。更糟糕的是:(make-searcher 1)(make-searcher 2) 需要返回不同 过程,因为((make-searcher 1) '(1 2 3))(1 2 3)((make-searcher 2) '(1 2 3))(2 3)

    所以这听起来完全没有希望:本地search 过程不仅必须是一个过程(它不能被编译掉),而且每次都必须重新制作。

    但事实上事情并没有那么糟糕。词法范围意味着系统可以确切地知道哪些绑定对search 可见(在这种情况下,thing 的绑定及其参数)。例如,您可以做的是编译一些代码,在向量中查找这些绑定的值。然后,从make-search 返回的东西将search 的编译代码与绑定向量打包在一起。编译后的代码总是一样的,只是每次都需要创建和初始化向量。

    【讨论】:

    • 你的案例确实很有意思,谢谢指出。不过,在我完成了几章之后,我可能不得不重新阅读它! :)
    【解决方案4】:

    想象一下这段代码:

    (let ((a expr))
      (do-something-with a))
    

    同理:

    ((lambda (a)
       (do-something-with a))
     expr)
    

    在解释器中,它可能会在每次调用它之前创建 lambda,而其他 语言可能会将其变成(do-something-with expr)。该报告不想触及除保证尾递归之外的非功能性需求。在所有严肃的实现中,lambda 都很便宜。

    既然你提到球拍: 文件 test_com.rkt

    #lang racket
    (define (mean-square x y) 
        (define (square x) (* x x))
        (define (average x y) (/ (+ x y) 2))
        (average (square x) (square y)))
    
    (display (mean-square 2 4))
    

    终端命令:

    raco make test_com.rkt
    raco decompile compiled/test_com_rkt.zo
    

    结果输出:

    (module test_com ....
      (require (lib "racket/main.rkt"))
      (provide)
      (define-values
       (mean-square)
       (#%closed
        mean-square49
        (lambda (arg0-50 arg1-51)
          '#(mean-square #<path:/home/westerp/compiled/test_com.rkt> 2 0 14 136 #f)
          '(flags: preserves-marks single-result)
          (/ (+ (* arg0-50 arg0-50) (* arg1-51 arg1-51)) '2))))
      (#%apply-values print-values (display '10)) ; the only code that matters!
      (void)
      (module (test_com configure-runtime) ....
        (require '#%kernel (lib "racket/runtime-config.rkt"))
        (provide)
        (print-as-expression '#t)
        (void)))
    

    虽然mean-square 已经内联了他们的本地过程,但因为我给了它文字值,它永远不会调用它,所以它所做的只是(display '10) 然后退出。

    当然,如果您使用makeexe。从 DrRacket 中,启用调试和更好的跟踪和错误消息的语言选项将运行得更慢。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-29
      • 1970-01-01
      • 2014-02-26
      • 2012-04-18
      • 2014-04-05
      • 2012-04-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多