如何以更好的性能评估AST? 目前我们创建AST作为树,其中叶节点(终端)是一个参数的函数 - 关键字及其值的映射。终端用关键字表示,而功能(非终端)可以是用户(或clojure)定义的功能。完全增长方法从非终端和终端创建树:
(defn full-growth
"Creates individual by full growth method: root and intermediate nodes are
randomly selected from non-terminals Ns,
leaves at depth depth are randomly selected from terminals Ts"
[Ns Ts arity-fn depth]
(if (<= depth 0)
(rand-nth Ts)
(let [n (rand-nth Ns)]
(cons n (repeatedly (arity-fn n) #(full-growth Ns Ts arity-fn(dec depth)))))))
生成AST的示例:
=> (def ast (full-growth [+ *] [:x] {+ 2, * 2} 3))
#'gpr.symb-reg/ast
=> ast
(#object[clojure.core$_STAR_ 0x6fc90beb "clojure.core$_STAR_@6fc90beb"]
(#object[clojure.core$_STAR_ 0x6fc90beb "clojure.core$_STAR_@6fc90beb"]
(#object[clojure.core$_STAR_ 0x6fc90beb "clojure.core$_STAR_@6fc90beb"]
:x
:x)
(#object[clojure.core$_PLUS_ 0x1b00ba1a "clojure.core$_PLUS_@1b00ba1a"]
:x
:x))
(#object[clojure.core$_PLUS_ 0x1b00ba1a "clojure.core$_PLUS_@1b00ba1a"]
(#object[clojure.core$_PLUS_ 0x1b00ba1a "clojure.core$_PLUS_@1b00ba1a"]
:x
:x)
(#object[clojure.core$_PLUS_ 0x1b00ba1a "clojure.core$_PLUS_@1b00ba1a"]
:x
:x)))
,相当于
`(~* (~* (~* ~:x ~:x) (~+ ~:x ~:x)) (~+ (~+ ~:x ~:x) (~+ ~:x ~:x)))
(def ast `(~* (~* (~* ~:x ~:x) (~+ ~:x ~:x)) (~+ (~+ ~:x ~:x) (~+ ~:x ~:x))))
我们可以编写直接评估此AST的fn:
(defn ast-fn
[{x :x}]
(* (* (* x x) (+ x x)) (+ (+ x x) (+ x x))))
=> (ast-fn {:x 3})
648
我们有两种基于AST创建函数的方法,一种是在apply和map的帮助下,另一种是在comp和juxt的帮助下:
(defn tree-apply
"((+ :x :x) in) => (apply + [(:x in) (:x in))]"
([tree] (fn [in] (tree-apply tree in)))
([tree in]
(if (sequential? tree)
(apply (first tree) (map #(tree-apply % in) (rest tree)))
(tree in))))
#'gpr.symb-reg/tree-apply
=> (defn tree-comp
"(+ :x :x) => (comp (partial apply +) (juxt :x :x))"
[tree]
(if (sequential? tree)
(comp (partial apply (first tree)) (apply juxt (map tree-comp (rest tree))))
tree))
#'gpr.symb-reg/tree-comp
=> ((tree-apply ast) {:x 3})
648
=> ((tree-comp ast) {:x 3})
648
使用时间fn,我们测量在测试用例上执行函数的时间:
=> (defn timing
[f interval]
(let [values (into [] (map (fn[x] {:x x})) interval)]
(time (into [] (map f) values)))
true)
=> (timing ast-fn (range -10 10 0.0001))
"Elapsed time: 37.184583 msecs"
true
=> (timing (tree-comp ast) (range -10 10 0.0001))
"Elapsed time: 328.961435 msecs"
true
=> (timing (tree-apply ast) (range -10 10 0.0001))
"Elapsed time: 829.483138 msecs"
true
正如您所看到的,直接函数(ast-fn),树补偿生成函数和树生成函数之间的性能存在巨大差异。
有更好的方法吗?
编辑: madstap的回答看起来非常有希望。我对他的解决方案进行了一些修改(终端也可以是其他一些函数,不仅仅是关键字,就像常量函数一样,不管输入如何都会不断返回值):
(defn c [v] (fn [_] v))
(def c1 (c 1))
(defmacro full-growth-macro
"Creates individual by full growth method: root and intermediate nodes are
randomly selected from non-terminals Ns,
leaves at depth depth are randomly selected from terminals Ts"
[Ns Ts arity-fn depth]
(let [tree (full-growth Ns Ts arity-fn depth)
val-map (gensym)
ast2f (fn ast2f [ast] (if (sequential? ast)
(list* (first ast) (map #(ast2f %1) (rest ast)))
(list ast val-map)))
new-tree (ast2f tree)]
`{:ast '~tree
:fn (fn [~val-map] ~new-tree)}))
现在,创建ast-m(使用常量c1作为终端)和相关的ast-m-fn:
=> (def ast-m (full-growth-macro [+ *] [:x c1] {+ 2 * 2} 3))
#'gpr.symb-reg/ast-m
=> ast-m
{:fn
#object[gpr.symb_reg$fn__20851 0x31802c12 "gpr.symb_reg$fn__20851@31802c12"],
:ast
(+
(* (+ :x :x) (+ :x c1))
(* (* c1 c1) (* :x c1)))}
=> (defn ast-m-fn
[{x :x}]
(+
(* (+ x x) (+ x 1))
(* (* 1 1) (* x 1))))
#'gpr.symb-reg/ast-m-fn
时间看起来非常相似:
=> (timing (:fn ast-m) (range -10 10 0.0001))
"Elapsed time: 58.478611 msecs"
true
=> (timing (:fn ast-m) (range -10 10 0.0001))
"Elapsed time: 53.495922 msecs"
true
=> (timing ast-m-fn (range -10 10 0.0001))
"Elapsed time: 74.412357 msecs"
true
=> (timing ast-m-fn (range -10 10 0.0001))
"Elapsed time: 59.556227 msecs"
true
答案 0 :(得分:1)
您正在以更低效的方式重新实现编译器所做的相当大的一部分,在运行时使用hashmaps按名称进行变量查找。通常,编译器可以将locals预先解析到堆栈上的已知位置,并使用单个字节码指令查找它们,但是您强制它调用许多函数以找出用于x
的变量。同样,您需要通过几个级别的动态分派来查找要调用*
的内容,而通常编译器可以在源代码中看到文字*
并发出对{的简单调用{1}}。
通过将所有这些东西推迟到运行时,你会对自己施加不可避免的惩罚。我认为你已经尽可能地加快了速度。
答案 1 :(得分:1)
使用宏来编写等效的ast-fn
。
(ns foo.core
(:require
[clojure.walk :as walk]))
(defmacro ast-macro [tree]
(let [val-map (gensym)
new-tree (walk/postwalk (fn [x]
(if (keyword? x)
(list val-map x)
x))
(eval tree))]
`(fn [~val-map] ~new-tree)))
在我的机器上,这接近于ast-fn
的性能。 45毫秒至50毫秒。它进行了更多的查找,但可以通过一些额外的修补来修复。
编辑:
我想到了更多关于这一点。在宏扩展时eval
参数将限制你如何使用它(参数不能是本地的)。使full-growth
宏可以更好地工作。就像amalloy说的那样,它完全取决于你想要在运行时和宏扩展时做什么。
(defmacro full-growth-macro
"Creates individual by full growth method: root and intermediate nodes are
randomly selected from non-terminals Ns,
leaves at depth depth are randomly selected from terminals Ts"
[Ns Ts arity-fn depth]
(let [tree (full-growth Ns Ts arity-fn depth)
val-map (gensym)
new-tree (walk/postwalk (fn [x]
(if (keyword? x)
(list val-map x)
x))
tree)]
`{:ast '~tree
:fn (fn [~val-map] ~new-tree)}))