【问题标题】:Avoiding Run-time Parameter Tests避免运行时参数测试
【发布时间】:2021-02-26 15:08:30
【问题描述】:

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

(defparameter *param1* 0)

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

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

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

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

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

【问题讨论】:

    标签: macros global-variables runtime common-lisp


    【解决方案1】:

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

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

    我肯定会在大量使用宏进行二次猜测编译器之前尝试这样做。

    当然,另一件事是引发测试(这必须有一个名字,但我不确定它是什么:我总是称它为“引发”)。转码喜欢

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

    进入

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

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

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


    作为上述的替代方案,这里有一个非常可怕的 hack,它允许您使用参数,但会连接它们的 compile-time 值,从而导致编译器将它们视为常量。注意:当我说“可怕”时,我的意思也是“几乎没有经过测试”。

    您可以这样做:

    (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. 如果是,并且如果它被绑定,那么它将扩展到它的 compile-time 值(为安全起见)。

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

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

    在编译时等价于

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

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

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

    【讨论】:

    • 这将如何与 defconstant 一起工作? “常量”可能会在后续运行中设置为不同的值。在运行之间重新编译所有内容都可以,但这不会为 defconstant 生成编译器错误吗? (ps:使用SBCL)
    • @davypough:是的,不允许重新定义常量,SBCL 对此很挑剔。简单的方法是在重新编译之前冷启动 Lisp,如果你重新定义它们。如果你不能做到这一点,那么将定义的东西吹走。
    • @davypough:我添加了一个附录,其中包含您可能想查看的相当恶心的 hack。 告诫购买者
    • 优雅更新!这肯定会出现在我的工具箱中,尽管对于我当前的参数来说可能有点多。在一些评论中,在运行时检查众多参数的额外时间似乎是微不足道的(但不确定如何验证这一点,因为它们是如此普遍)。把它们都放在一个单独的包里的好主意,也是。感谢您的深入回答。
    猜你喜欢
    • 1970-01-01
    • 2017-11-02
    • 2020-06-01
    • 1970-01-01
    • 2021-07-03
    • 1970-01-01
    • 2020-01-30
    • 1970-01-01
    相关资源
    最近更新 更多