避免 运行 时间参数测试

Avoiding Run-time Parameter Tests

我的原型程序最初定义了一些全局参数,这些参数随后会影响分析过程。但程序主体最终包含了对这些参数的大量测试,以确定如何进行详细分析。例如:

(defparameter *param1* 0)

(ecase *param1*
  (0 (foo))
  (1 (bar))
  (2 (baz)))

但是在 运行 时间执行所有这些不同的测试是低效的。这些测试能否有效地转移到编译时(或以其他方式处理),因为所有参数在分析开始之前都是已知的?

我的第一个想法是为测试构建宏,因此在 运行 时只有相关代码可用:

(defmacro param1-macro ()
  (ecase *param1*
    (0 `(foo))
    (1 `(bar))
    (2 `(baz))))

但是像 (param1-macro) 这样的调用分散在各处使得代码在调试期间难以阅读和分析。每个宏调用的唯一决策过程是非局部的。有没有更透明的方法来提高 运行-时间效率?

如果这些参数是编译时常量,则将它们设为常量(即用 defconstant 定义它们)。这应该让编译器有机会在编译时对它们的值做出假设并将条件转换为无条件执行。

编译器能做到这一点当然取决于编译器:在我有限的测试中,至少有一些 CL 编译器进行了这种优化。

我肯定会在大量事后猜测编译器与宏之前尝试这样做。

当然,另一件事是提高测试(这必须有一个名称,但我不确定它是什么:我总是称它为 'raising')。转码如

(dotimes (i big-number)
  (case *parameter*
    ((1) ...)
    ...))

进入

(case *parameter*
  ((1) (dotimes (i big-number)
         ...))
  ...)

这将测试数量减少了 big-number 倍。我怀疑这也是好的编译器可以自己做的优化。

最后,也许也是最重要的一点:衡量它有多慢。测试真的需要很长时间吗?几乎可以肯定,除非你已经衡量了他们的成本,否则你不会知道这一点:当然,这就是我在做出这样的假设时发现的结果!


作为上面的替代方案,这里有一个非常可怕的 hack,它允许您使用参数,但会连接它们的 compile-time 值,从而导致编译器处理他们喜欢常数。注意:当我说 'horrible' 时,我的意思也是 'mostly untested'.

以下是您可以执行此操作的方法:

(in-package :cl-user)

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar *wiring* t))

(declaim (inline wiring))

(defun wiring (x)
  x)

(define-compiler-macro wiring (&whole form x)
  (if (and *wiring*
           (symbolp x)
           (boundp x))
      `(quote ,(symbol-value x))
    form))

现在,如果 *wiring* 在编译时为假,(wiring ...) 只是一个恒等函数,所以你可以说 (wiring *param*) 表示 *param*。它被声明为内联,因此它应该具有零成本(而且,实际上,零目的)。

但是如果 *wiring* 在编译时是 true,那么编译器宏会做一件可怕的事情:(wiring *param*) 将:

  1. 检查*param*是否是符号;
  2. 如果是,并且如果它被绑定,那么它将扩展到它的 编译时 值(为了安全而引用)。

这意味着,如果在编译时,*param* 绑定到 2,那么

(case (wiring *param*)
  ((2) (+ x 0))
  (otherwise (+ x 1)))

在编译时等价于

(case 2
  ((2) (+ x 0))
  (otherwise (+ x 1)))

然后编译器可以将其(在 SBCL 的情况下带有注释)转换为 (+ x 0)

这很可怕,因为……好吧,它有很多种可怕的方式。它肯定违反了很多关于代码应该如何表现的假设。但这也有点可爱:您可以在 Lisp 语言中做到这一点,我认为这很了不起。