我对clojure还不熟悉,而且作为快速的练习,我编写了一个函数,它应该经过斐波纳契序列,直到它超过999999999 10亿次(做一些额外的计算,但并不是很重要)。我用Java编写了一些同样的东西,虽然我理解Clojure的速度比Java慢,但java程序需要35秒才能完成,Clojure程序则需要27分钟,这让我感到非常惊讶(考虑到nodejs能够在大约8分钟内完成它)。我用repl编译了这个类,并使用这个java -cp `clj -Spath` fib命令运行它。真的不确定为什么这么慢。
(defn fib
[]
(def iter (atom (long 0)))
(def tester (atom (long 0)))
(dotimes [n 1000000000]
(loop [prev (long 0)
curr (long 1)]
(when (<= prev 999999999)
(swap! iter inc)
(if (even? @iter)
(swap! tester + prev)
(swap! tester - prev))
(recur curr (+ prev curr)))))
(println (str "Done in: " @iter " Test: " @tester))
)这里是我的Java方法供参考
public static void main(String[] args) {
long iteration = 0;
int test = 0;
for (int n = 0; n < 1000000000; n++) {
int x = 0, y = 1;
while (true) {
iteration += 1;
if (iteration % 2 == 0) {
test += x;
}
else {
test -=x;
}
int i = x + y;
x = y;
y = i;
if (x > 999999999) { break; }
}
}
System.out.println("iter: " + iteration + " " + test);
}发布于 2022-04-08 05:29:38
您的代码看起来像是一个“基本函数”,但是有两个主要问题:
(let [counter (atom 0)]
(dotimes [x 1000]
(-> (Thread. (fn [] (swap! counter inc)))
.start))
(Thread/sleep 2000)
@counter)
=> 10001000个线程启动,计数器值增加1000倍,结果是1000,不足为奇。但是,将其与volatile!进行比较,它并不是线程安全的:
(let [counter (volatile! 0)]
(dotimes [x 1000]
(-> (Thread. (fn [] (vswap! counter inc)))
.start))
(Thread/sleep 2000)
@counter)
=> 989另见关于Atoms的闭包引用。
atoms和volatiles,否则不应该使用它们。也不鼓励使用loop,因为通常有一些更好的功能,它完全可以满足您的需要。您确实尝试将Java函数重写为Clojure。Clojure需要不同的解决问题的方法,而且您的代码肯定不是惯用的。我建议您不要逐行重写Java代码,但是要找到一些简单的问题,并学习如何在不使用atom、volatile!和loop的情况下以Clojure方式解决这些问题。顺便说一句,还有memoize,它在像您这样的例子中很有用。
发布于 2022-04-10 01:25:34
有一件事是很多Clojure的新手没有意识到的,那就是Clojure默认是一种更高级的语言。这意味着它将迫使您在实现中处理算术溢出,将数字视为可以扩展的对象,防止您更改任何变量,强制您使用线程安全代码,并将您推向依赖递归循环的函数式解决方案。
它也不强制您在默认情况下键入所有内容,这也方便您不必考虑所有类型的所有类型,并确保所有类型是兼容的,例如,向量只包含整数--例如,Clojure不关心,允许您在其中放置整数和空头。
所有这些东西对于快速编写-足够正确、可进化和可维护的应用程序来说是很棒的,但是对于高性能的算法来说并不是很好。
这意味着,默认情况下,Clojure是为实现应用程序而不是为实现高性能算法而优化的。
不幸的是,似乎大多数人“尝试”了一种新的语言,因此Clojure的新成员将倾向于首先使用这种语言来尝试并实现高性能的算法。这是一个明显的不匹配的Clojure默认是擅长的,许多新来的人立即面临额外的摩擦Clojure在这里造成。Clojure假设您将实现一个应用程序,而不是一些高性能的10亿N大小的斐波纳契算法。
但是不要失去希望,Clojure也可以用来实现高性能的算法,但它不是默认的,所以您通常需要更有经验的Clojure开发人员知道如何这样做,因为它不太明显。
以下是Clojure中的算法,它的执行速度与Java实现一样快,它是对精确Java代码的递归重写:
(ns playground
(:gen-class)
(:require [fastmath.core :as fm]))
(defn -main []
(loop [n (long 0) iteration (long 0) test (long 0)]
(if (fm/< n 1000000000)
(let [^longs inner
(loop [x (long 0) y (long 1) iteration iteration test test]
(let [iteration (fm/inc iteration)
test (if (fm/== (fm/mod iteration 2) 0) (fm/+ test x) (fm/- test x))
i (fm/+ x y)
x y
y i]
(if (fm/> x 999999999)
(doto (long-array 2) (aset 0 iteration) (aset 1 test))
(recur x y iteration test))))]
(recur (fm/inc n) (aget inner 0) (aget inner 1)))
(str "iter: " iteration " " test))))
(println (time (-main)))
"Elapsed time: 47370.544514 msecs"
;;=> iter: 45000000000 0使用deps:
:deps {generateme/fastmath {:mvn/version "2.1.8"}}正如你所看到的,在我的笔记本电脑上,它在大约47秒内完成。我还在我的笔记本电脑上运行了你的Java版本来比较我的硬件,而我得到的是: 46947.343671毫秒。
所以在我的笔记本电脑上,你可以看到Clojure和Java基本上都一样快,都是在47秒左右运行。
区别在于,在Java中,编程风格总是有助于实现高性能的算法。您可以直接使用原语类型和基本算法,没有装箱,没有溢出检查,没有同步或原子性或波动性保护的可变变量,等等。
因此,在Clojure中几乎不需要获得类似的性能:
显然,我们也需要运行相同的算法,所以类似的实现。我并不是想比较是否存在另一种算法可以更快地解决相同的问题,而是如何在Clojure中实现相同的algo,使其运行得同样快。
为了在Clojure中执行原语类型,您必须知道只允许您使用let和loop在本地上下文中这样做,并且所有函数调用都将撤消原语类型,除非它们也被类型为原语long或double (可以跨越Clojure中函数边界的唯一受支持的原语类型)。
这是我当时做的第一件事,只需使用Clojure的loop/recur重写相同的循环,并声明与您相同的变量,而是使用let隐藏,因此我们不需要托管可变容器。
最后,我使用了快速数学,它是一个库,它提供了许多基本的算术函数版本,这样我们就可以进行基本的数学计算。Clojure的核心有它自己的一些,但它没有国防部,例如,所以我需要引入快速数学。
就这样。
通常,这是您需要知道的,保持原语类型,保持原始数学(使用快速数学),键入提示以避免反射,利用让阴影,保持原语数组,就可以得到Clojure高性能的实现。
这里有一套很好的信息:interop#primitives
最后一件事,Clojure的哲学是,它的目的是快速实现--足够正确、可进化和可维护的应用程序可以扩展。这就是为什么语言是这样的。正如我已经展示的那样,虽然您可以实现高性能的algos,但是Clojure的理念也不是为Java已经擅长的东西重新发明语法。Clojure可以使用Java,因此对于那些需要非常命令式、可变、原始逻辑的算法,它会期望您回到Java,将其作为一个静态方法来编写,然后从Clojure中使用它。或者它认为你甚至会委托给比Java更有表现力的东西,并且使用BLAS,或者GPU来执行超快的矩阵运算,或者类似的东西。这就是为什么它不愿意提供自己的命令式构造,或者原始内存访问等等,因为它认为自己没有比运行过的主机做得更好的事情。
发布于 2022-04-08 07:28:16
如果您是编程初学者,我建议您在假定语言/lib/framework/platform错误之前,始终假定代码是错误的。
看看Fibonacci序列在Java中的各种实现,您可能会学到一些东西。
https://stackoverflow.com/questions/71790601
复制相似问题