我已经实现了一种算法来计算最长的 连续
公共子序列(不要与最长的公共子序列相混淆,尽管对这个问题并不重要)。我需要从中获得最大的性能,因为我会经常称呼它。为了比较性能,我在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来提高性能。
编辑: 在第一个下面添加了一个更快的较丑版本。
这是我的看法:
(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%。
问题内容: 让我们转换为: 此转换操作的费用是多少?是否执行复制?据我在Go规范中所看到的: 字符串的行为就像字节的切片,但是是不可变的 ,这至少应该涉及复制,以确保后续的切片操作不会修改我们的字符串。反向对话会怎样?对话是否涉及编码/解码,如utf8 <->符文? 问题答案: 该不是铸造而是转换。有些转换与强制转换一样,只是重新解释了 原位 。不幸的是,这不是字符串到字节片转换的情况。字节片是可
问题内容: 我是一个初学者,我一直读到重复代码很不好。但是,为了避免这样做,您通常必须进行额外的方法调用。假设我有以下课程 对于我来说,将我的clear()方法中的两行复制并粘贴到构造函数中而不是调用实际方法会更好吗?如果是这样,有什么不同?如果我的构造函数进行了10个方法调用,每个方法仅将实例变量设置为一个值,该怎么办?什么是最佳编程实践? 问题答案: 对于我来说,将我的clear()方法中的两
问题内容: 我想知道如何运作。我不太了解文件系统的工作原理,所以我应该首先开始阅读。 但是为了获得快速的预先信息: 如果在某个日志中注册了该路径和文件名,是否调用文件系统的单个操作?还是操作系统获取目录的内容,然后在目录中进行扫描以进行匹配? 我认为这将取决于文件系统,但是也许所有文件系统都使用快速方法? 我不是在谈论网络和磁带系统。让我们将其保留到ntfs,extX,zfs,jfs :-) 问题
有这样一些,例如算法,数据结构,数学,还有其他极客范的大多数程序员知道但很少使用的东西。实践中,这种奇妙的东西太复杂了,通常是不需要的。例如,当你花费大多数时间在低效的数据库调用上时,提高算法是没有什么用的。不幸的大量编程由让系统相互交流以及使用非常简单的数据结构去构建漂亮的用户界面组成。 高科技什么时候是合适的科技?你什么时候应当打开一本书去找一些东西而非一个毫秒级算法?做这些有时候是有用的,但
问题内容: 我对Java真的很陌生,我读到Java 非常昂贵。我只想知道什么是昂贵的,它又如何昂贵? 谢谢。 问题答案: 也许还没有你想的那么糟 它曾经是可怕的(这可能就是您读到它“非常昂贵”的原因)。这些模因可能需要很长时间才能消失 由于涉及缓存刷新和失效的规则,因此Java语言中的同步块通常比许多平台提供的关键部分功能更为昂贵,而这些平台通常使用原子的“测试并设置位”机器指令来实现。即使程序仅