方案的块结构效率

Scheme's block structure efficiency

The book 在第 1 章中定义了 块结构 ,允许您在过程定义中 'package' defines。

例如考虑这个 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.

我的问题是,我调用mean-square 通过解释程序的程序?如果是这样,那不是很低效吗?如果不是,为什么?

没问题。编译 mean-square 过程时,也会编译所有嵌套过程。每次调用 mean-square 过程时不需要重新编译它们。

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

因此归结为编译器可以优化多少。例如,它可以注意到这两个函数实际上都没有引用周围的词法环境。 (这些函数中的 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)))

想象一下这段代码:

(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 中,启用调试和更好的跟踪和错误消息的语言选项将 运行 变慢。

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

考虑这样的定义:

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

好吧,本地 search 过程绝对不能在这里编译掉,因为它是 return 从 make-searcher 编辑而来的。甚至更糟:(make-searcher 1)(make-searcher 2) 需要 return 不同的 程序,因为 ((make-searcher 1) '(1 2 3))(1 2 3)((make-searcher 2) '(1 2 3))(2 3).

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

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