【问题标题】:Clojure Performance For Expensive Algorithms昂贵算法的 Clojure 性能
【发布时间】:2013-02-19 04:10:24
【问题描述】:

我已经实现了一个算法来计算最长的连续公共子序列(不要与最长公共子序列混淆,尽管对于这个问题并不重要)。我需要从中获得最大的性能,因为我会经常调用它。为了比较性能,我在 Clojure 和 Java 中实现了相同的算法。 Java 版本的运行速度明显更快。 我的问题是我可以对 Clojure 版本做些什么来加快它达到 Java 的水平。

这是 Java 代码:

public static int lcs(String[] a1, String[] a2) {
    if (a1 == null || a2 == null) {
        return 0;
    }

    int matchLen = 0;
    int maxLen = 0;

    int a1Len = a1.length;
    int a2Len = a2.length;
    int[] prev = new int[a2Len + 1]; // holds data from previous iteration of inner for loop
    int[] curr = new int[a2Len + 1]; // used for the 'current' iteration of inner for loop

    for (int i = 0; i < a1Len; ++i) {
        for (int j = 0; j < a2Len; ++j) {
            if (a1[i].equals(a2[j])) {
                matchLen = prev[j] + 1; // curr and prev are padded by 1 to allow for this assignment when j=0
            }
            else {
                matchLen = 0;
            }
            curr[j+1] = matchLen;

            if (matchLen > maxLen) {
                maxLen = matchLen;
            }
        }

        int[] swap = prev;
        prev = curr;
        curr = swap;
    }

    return maxLen;
}

这是相同的 Clojure 版本:

(defn lcs
  [#^"[Ljava.lang.String;" a1 #^"[Ljava.lang.String;" a2]
  (let [a1-len (alength a1)
        a2-len (alength a2)
        prev (int-array (inc a2-len))
        curr (int-array (inc a2-len))]
    (loop [i 0 max-len 0 prev prev curr curr]
      (if (< i a1-len)
        (recur (inc i)
               (loop [j 0 max-len max-len]
                 (if (< j a2-len)
                   (if (= (aget a1 i) (aget a2 j))
                     (let [match-len (inc (aget prev j))]
                       (do
                         (aset-int curr (inc j) match-len)
                         (recur (inc j) (max max-len match-len))))
                     (do
                       (aset-int curr (inc j) 0)
                       (recur (inc j) max-len)))
                   max-len))
               curr
               prev)
        max-len))))

现在让我们在我的机器上测试一下:

(def pool "ABC")
(defn get-random-id [n] (apply str (repeatedly n #(rand-nth pool))))
(def a1 (into-array (take 10000 (repeatedly #(get-random-id 5)))))
(def a2 (into-array (take 10000 (repeatedly #(get-random-id 5)))))

Java:

(time (Ratcliff/lcs a1 a2))
"Elapsed time: 1521.455 msecs"

Clojure:

(time (lcs a1 a2))
"Elapsed time: 19863.633 msecs"

Clojure 速度很快,但仍比 Java 慢一个数量级。我能做些什么来缩小这个差距吗?或者我是否将其最大化,一个数量级是“最小的 Clojure 开销”。

如您所见,我已经在使用循环的“低级”构造,我使用的是原生 Java 数组,并且我已经对参数进行了类型提示以避免反射。

可能有一些算法优化,但我现在不想去那里。我很好奇我能获得多接近 Java 的性能。如果我不能缩小差距,我只会使用 Java 代码。该项目的其余部分在 Clojure 中,但有时为了提高性能可能需要使用 Java。

【问题讨论】:

  • 首先,将 (set! *warn-on-reflection* true) 放在 Clojure 实现命名空间的顶部并重新加载,注意任何警告,然后解决它们。其次,理想情况下,使用github.com/hugoduncan/criterium 进行基准测试。接下来,在探查器中检查它...
  • ...我们开始,“自动装箱循环参数:max-len”...
  • 谢谢,Hendekagon!所有好的提示。我摆脱了自动装箱警告(通过使用 (int) 包装对内循环的调用),但这并没有提高性能。然后我将所有对 inc 的调用都转为 unchecked-inc。这也没有任何影响。
  • 我认为应该是 (long ) 而不是 int...
  • 这并不能回答你的问题,但是像this 这样的天真的惯用 Clojure 实现可能会更慢,但它更容易推理和适用于任何类型,并且它可以找到跨越重叠的序列结束和开始(它比 Java 更容易编写十亿倍)。如果有办法让这段代码运行得像 Java 一样快!

标签: java performance clojure


【解决方案1】:

编辑:在第一个下面添加了一个更快更丑的版本。

这是我的看法:

(defn my-lcs [^objects a1 ^objects a2]
  (first
    (let [n (inc (alength a1))]
      (areduce a1 i 
        [max-len ^ints prev ^ints curr] [0 (int-array n) (int-array n)]
        [(areduce a2 j max-len (unchecked-long max-len)
           (let [match-len 
                 (if (.equals (aget a1 i) (aget a2 j))
                   (unchecked-inc (aget prev j))
                   0)]
             (aset curr (unchecked-inc j) match-len)
             (if (> match-len max-len)
               match-len
               max-len)))
         curr prev]))))

与你的主要区别:a[gs]et vs a[gs]et-int,使用unchecked- ops(隐式通过areduce),使用向量作为返回值(和“交换”机制)和max-len在内部循环之前强制为原始值(原始值循环是有问题的,自 1.5RC2 以来稍微少一些,但支持还不完美,但是 *warn-on-reflection* 不是沉默的)。

我切换到.equals 而不是= 以避免 Clojure 等效项中的逻辑。

编辑:让我们变得丑陋并恢复数组交换技巧:

(deftype F [^:unsynchronized-mutable ^ints curr
            ^:unsynchronized-mutable ^ints prev]
  clojure.lang.IFn
  (invoke [_ a1 a2]
    (let [^objects a1 a1
          ^objects a2 a2]
      (areduce a1 i max-len 0
        (let [m (areduce a2 j max-len (unchecked-long max-len)
                  (let [match-len 
                        (if (.equals (aget a1 i) (aget a2 j))
                          (unchecked-inc (aget prev j))
                          0)]
                    (aset curr (unchecked-inc j) (unchecked-int match-len))
                    (if (> match-len max-len)
                      match-len
                      max-len)))
              bak curr]
          (set! curr prev)
          (set! prev bak)
          m)))))

(defn my-lcs2 [^objects a1 a2]
  (let [n (inc (alength a1))
        f (F. (int-array n) (int-array n))]
    (f a1 a2)))

在我的盒子上,它快了 30%。

【讨论】:

  • 谢谢。我分别测试了每个修改。如上所述,aset 确实比 aset-int 快 3 倍。因此,它的运行时间是 Java 的 4 倍。未经检查的数学并没有产生任何明显的差异。但是使用 .equals 而不是 = 又提高了 3-4 倍!到目前为止,通过所有优化,Java 大约需要 1.5 秒,Clojure 大约需要 1.9 秒。一点也不差!
  • 您的版本运行非常接近 java ~1.9 秒 vs. ~1.5 秒,但正如我上面所说,我相信性能提升来自切换到 aset 和 .equals。通过切换到 aset 和 .equals 并且没有其他修改,我获得了与您的版本相同的性能。
  • 您可能只使用== 而不是.equals
  • 使用*unchecked-math* true 我认为你不需要unchecked-* 函数。
  • @cgrand - 感谢您提供使用 areduce 的替代实现。感觉比循环更像 Clojurely,虽然两者都还不错。
【解决方案2】:

以下是一些改进:

  1. 花哨的类型提示没有优势,只需使用 ^objects
  2. 我相信,aset-int 已被弃用——只是普通的旧版aget 更快,总体上似乎快了大约 3 倍

除此之外(以及上面提到的关于 recur 的长类型提示),我看不到任何明显的进一步改进方法。

(defn lcs
  [^objects a1 ^objects a2]
  (let [a1-len (alength a1)
        a2-len (alength a2)
        prev (int-array (inc a2-len))
        curr (int-array (inc a2-len))]
    (loop [i 0 max-len 0 prev prev curr curr]
      (if (< i a1-len)
        (recur (inc i)
               (long (loop [j 0 max-len max-len]
                 (if (< j a2-len)
                   (if (= (aget a1 i) (aget a2 j))
                     (let [match-len (inc (aget prev j))]
                       (do
                         (aset curr (inc j) match-len)
                         (recur (inc j) (max max-len match-len))))
                     (do
                       (aset curr (inc j) 0)
                       (recur (inc j) max-len)))
                   max-len)))
               curr
               prev)
        max-len))))
#'user/lcs
user> (time (lcs a1 a2))
"Elapsed time: 3862.211 msecs"

【讨论】:

  • aset 确实快了 3 倍!谢谢你。在我的机器上,Java 约为 1.5 毫秒,Clojure 约为 6 毫秒。不错。我也试过 (long ) 而不是 (int ) 但没有任何区别。
  • 还将参数类型提示更改为 ^objects 而不是 #^"[Ljava.lang.String;"对性能没有影响。这是令人惊讶的。你用 Clojure 还不知道的 ^objects 暗示了什么?
  • 用 Math/max 代替 max 似乎并没有产生显着的改进。同样用 (if (> match-len max-len) match-len max-len) 替换它似乎也没有任何影响。
猜你喜欢
  • 1970-01-01
  • 2019-04-09
  • 2020-10-27
  • 1970-01-01
  • 2019-12-10
  • 2011-11-01
  • 1970-01-01
  • 2017-02-13
  • 1970-01-01
相关资源
最近更新 更多