在clojure中动态生成高性能函数

mik*_*era 7 java compiler-construction code-generation clojure dynamic

我正在尝试使用Clojure动态生成可应用于大量数据的函数 - 即要求是将函数编译为字节码以便快速执行,但是直到运行时才能知道它们的规范.

例如,假设我使用简单的DSL指定函数,例如:

(def my-spec [:add [:multiply 2 :param0] 3])
Run Code Online (Sandbox Code Playgroud)

我想创建一个函数compile-spec,使得:

(compile-spec my-spec)
Run Code Online (Sandbox Code Playgroud)

将返回一个返回2x + 3的参数x的编译函数.

在Clojure中最好的方法是什么?

Mic*_*zyk 11

Hamza Yerlikaya已经提出了最重要的观点,即Clojure代码总是被编译.我只是添加一个插图和一些关于优化工作的一些悬而未决的成果的信息.

首先,关于Clojure的代码上面的点总是被编译包括通过调用创建高阶函数和函数返回关闭evalfn/ fn*形式和确实什么都可以作为Clojure的功能作用.因此,您不需要单独的DSL来描述函数,只需使用更高阶函数(可能还有宏):

(defn make-affine-function [a b]
  (fn [x] (+ (* a x) b)))

((make-affine-function 31 47) 5)
; => 202
Run Code Online (Sandbox Code Playgroud)

如果您的规范包含有关参数类型的信息,那么事情会更有趣,因为您可能有兴趣编写宏来使用这些类型提示生成代码.我能想到的最简单的例子就是上面的变体:

(defmacro make-primitive-affine-function [t a b]
  (let [cast #(list (symbol (name t)) %)
        x (gensym "x")]
    `(fn [~x] (+ (* ~(cast a) ~(cast x)) ~(cast b)))))

((make-primitive-affine-function :int 31 47) 5)
; => 202
Run Code Online (Sandbox Code Playgroud)

使用:int,:long,:float:double(或相应名称的非名称空间限定的符号),以利用拆箱原始算术适合您的参数类型的第一个参数.根据您的功能,这可能会给您带来非常显着的性能提升.

其他类型的提示通常提供#^Foo bar语法(^Foo bar在1.2中做同样的事情); 如果要将它们添加到宏生成的代码中,请调查该with-meta函数(您需要合并'{:tag Foo}到表示函数的正式参数的符号的元数据中,或者let您希望将类型提示放在其上的引入的本地符号).


哦,如果你还想知道如何实现你原来的想法......

您始终可以构造Clojure表达式来定义您的函数(list 'fn ['x] (a-magic-function-to-generate-some-code some-args ...))- 并调用eval结果.这将使您能够执行以下操作(要求规范包含参数列表会更简单,但这里是一个假定参数将从规范中删除的版本,都被调用paramFOO并且将按字典顺序排序) :

(require '[clojure.walk :as walk])

(defn compile-spec [spec]
  (let [params (atom #{})]
    (walk/prewalk
     (fn [item]
       (if (and (symbol? item) (.startsWith (name item) "param"))
         (do (swap! params conj item)
             item)
         item))
     spec)
    (eval `(fn [~@(sort @params)] ~@spec))))

(def my-spec '[(+ (* 31 param0) 47)])

((compile-spec my-spec) 5)
; => 202
Run Code Online (Sandbox Code Playgroud)

绝大多数时候,没有充分的理由以这种方式做事,应该避免; 使用高阶函数和宏代替.但是,如果你正在做类似于进化编程的事情,那么它就在那里,提供最大的灵活性 - 结果仍然是编译功能.


Ham*_*aya 6

即使您没有AOT编译代码,只要您定义一个函数,它就会被动态编译为字节码.