这个Clojure代码可以优化吗?

时间:2013-12-08 00:34:07

标签: performance clojure boxing

我为我正在制作的游戏编写了以下代码。但似乎有点慢。如果您尚未检查代码,则它是A *搜索/路径查找算法。对于100x100网格,它需要大约100-600毫秒,具体取决于所使用的启发式(以及随后访问的磁贴数量)。

没有反射警告。但是,我怀疑拳击可能是一个问题。但我不知道在这种情况下如何摆脱拳击,因为计算分为几个函数。另外,我将tile / coordinates保存为两个数字的向量,如下所示:[x y]。但是这些数字会被装箱,对吧?如果您不想全部阅读,典型的代码是:(def add-pos (partial mapv + pos))其中pos是上述类型的双数字向量。有一些地方的数字以类似于上面add-pos的方式被操纵,然后放回到矢量中。有没有办法优化这样的代码?任何其他提示也是受欢迎的,与性能相关或其他。

编辑:想想更多关于它的内容,我想出了一些后续问题:Clojure函数能否返回原语? Clojure函数可以采用原语(没有任何装箱)吗?我可以在没有拳击的情况下将原语放在类型/记录中吗?

(ns game.server.pathfinding
  (:use game.utils)
  (:require [clojure.math.numeric-tower :as math]
            [game.math :as gmath]
            [clojure.data.priority-map :as pm]))

(defn walkable? [x]
  (and x (= 1 x)))

(defn point->tile
  ([p] (apply point->tile p))
  ([x y] [(int x) (int y)]))

(defn get-tile [m v]
  "Gets the type of the tile at the point v in
   the grid m. v is a point in R^2, not grid indices."
  (get-in m (point->tile v)))

(defn integer-points
  "Given an equation: x = start + t * step, returns a list of the
   values for t that make x an integer between start and stop,
   or nil if there is no such value for t."
  [start stop step]
  (if-not (zero? step)
    (let [first-t (-> start ((if (neg? step) math/floor math/ceil))
                      (- start) (/ step))
          t-step (/ 1 (math/abs step))]
      (take-while #((if (neg? step) > <) (+ start (* step %)) stop)
                  (iterate (partial + t-step) first-t)))))

(defn crossed-tiles [[x y :as p] p2 m]
  (let [[dx dy :as diff-vec] (map - p2 p)
        ipf (fn [getter]
              (integer-points (getter p) (getter p2) (getter diff-vec)))
        x-int-ps (ipf first)
        y-int-ps (ipf second)
        get-tiles (fn [[x-indent y-indent] t]
                    (->> [(+ x-indent x (* t dx)) (+ y-indent y (* t dy))]
                         (get-tile m)))]
    (concat (map (partial get-tiles [0.5 0]) x-int-ps)
            (map (partial get-tiles [0 0.5]) y-int-ps))))

(defn clear-line?
  "Returns true if the line between p and p2 passes over only
   walkable? tiles in m, otherwise false."
  [p p2 m]
  (every? walkable? (crossed-tiles p p2 m)))

(defn clear-path?
  "Returns true if a circular object with radius r can move
   between p and p2, passing over only walkable? tiles in m,
   otherwise false.
   Note: Does not currently work for objects with a radius >= 0.5."
  [p p2 r m]
  (let [diff-vec (map (partial * r) (gmath/normalize (map - p2 p)))
        ortho1 ((fn [[x y]] (list (- y) x)) diff-vec)
        ortho2 ((fn [[x y]] (list y (- x))) diff-vec)]
    (and (clear-line? (map + ortho1 p) (map + ortho1 p2) m)
         (clear-line? (map + ortho2 p) (map + ortho2 p2) m))))

(defn straighten-path
  "Given a path in the map m, remove unnecessary nodes of
   the path. A node is removed if one can pass freely
   between the previous and the next node."
  ([m path]
   (if (> (count path) 2) (straighten-path m path nil) path))
  ([m [from mid to & tail] acc]
   (if to
     (if (clear-path? from to 0.49 m)
       (recur m (list* from to tail) acc)
       (recur m (list* mid to tail) (conj acc from)))
     (reverse (conj acc from mid)))))

(defn to-mid-points [path]
  (map (partial map (partial + 0.5)) path))

(defn to-tiles [path]
  (map (partial map int) path))

(defn a*
  "A* search for a grid of squares, mat. Tries to find a
   path from start to goal using only walkable? tiles.
   start and goal are vectors of indices into the grid,
   not points in R^2."
  [mat start goal factor]
  (let [width (count mat)
        height (count (first mat))]
    (letfn [(h [{pos :pos}] (* factor (gmath/distance pos goal)))
            (g [{:keys [pos parent]}]
              (if parent
                (+ (:g parent) (gmath/distance pos (parent :pos)))
                0))
            (make-node [parent pos]
              (let [node {:pos pos :parent parent}
                    g (g node) h (h node)
                    f (+ g h)]
                (assoc node :f f :g g :h h)))
            (get-path
              ([node] (get-path node ()))
              ([{:keys [pos parent]} path]
               (if parent
                 (recur parent (conj path pos))
                 (conj path pos))))
            (free-tile? [tile]
              (let [type (get-in mat (vec tile))]
                (and type (walkable? type))))
            (expand [closed pos]
              (let [adj [[1 0] [0 1] [-1 0] [0 -1]]
                    add-pos (partial mapv + pos)]
                (->> (take 4 (partition 2 1 (cycle adj)))
                     (map (fn [[t t2]]
                            (list* (map + t t2) (map add-pos [t t2]))))
                     (map (fn [[d t t2]]
                            (if (every? free-tile? [t t2]) d nil)))
                     (remove nil?)
                     (concat adj)
                     (map add-pos)
                     (remove (fn [[x y :as tile]]
                               (or (closed tile) (neg? x) (neg? y)
                                   (>= x width) (>= y height)
                                   (not (walkable? (get-in mat tile)))))))))
            (add-to-open [open tile->node [{:keys [pos f] :as node} & more]]
              (if node
                (if (or (not (contains? open pos))
                        (< f (open pos)))
                  (recur (assoc open pos f)
                         (assoc tile->node pos node)
                         more)
                  (recur open tile->node more))
                {:open open :tile->node tile->node}))]
      (let [start-node (make-node nil start)]
        (loop [closed #{}
               open (pm/priority-map start (:f start-node))
               tile->node {start start-node}]
          (let [[curr _] (peek open) curr-node (tile->node curr)]
            (when curr
              (if (= curr goal)
                (get-path curr-node)
                (let [exp-tiles (expand closed curr)
                      exp-nodes (map (partial make-node curr-node) exp-tiles)
                      {:keys [open tile->node]}
                      (add-to-open (pop open) tile->node exp-nodes)]
                  (recur (conj closed curr) open tile->node))))))))))

(defn find-path [mat start goal]
  (let [start-tile (point->tile start)
        goal-tile (point->tile goal)
        path (a* mat start-tile goal-tile)
        point-path (to-mid-points path)
        full-path (concat [start] point-path [goal])
        final-path (rest (straighten-path mat full-path))]
    final-path))

1 个答案:

答案 0 :(得分:3)

我建议使用Clojure High Performance Programming一本书来解决像你这样的问题。

有解包原语的功能(byteshortintlongfloatdouble)。

反射警告不适用于数字类型反射/失败以优化数字代码。有一个lib强制警告数字反射 - primitive-math

您可以声明函数参数和函数返回值(defn ^Integer foo [^Integer x ^Integer y] (+ x y))的类型。

如果你想要表现,请避免申请。

如果您想要表现,请避免使用varargs(需要申请的常见原因)。 Varargs函数在每次调用时都会创建垃圾(为了构造args映射,通常不在函数体外使用)。 partial总是构造一个varargs函数。考虑用#(* x%)替换varargs(partial * x),后者可以更积极地进行优化。

使用原始的jvm单一类型数组有一个权衡(它们很长并且固定长度,这会导致更复杂和更脆弱的代码),但它们的性能会比标准的clojure顺序类型更好,并且可用如果所有其他方法都无法获得所需的性能。

此外,使用criterium来比较代码的各种实现,它有一堆技巧来帮助排除影响执行时间的随机事物,这样你就可以看到在紧密循环中真正表现最佳的东西。 / p>

此外,关于您将某个点表示为[xy] - 您可以减少用它们保存的集合的空间和查找开销(defrecord point [xy])(只要您知道它们将仅保留两个元素,你不介意改变你的代码来要求(:x点)或(:y点))。你可以通过制作或使用一个简单的两个数字的java类来进一步优化(通过权衡失去免疫力)。