首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >为什么这个看似基本的clojure函数速度这么慢?

为什么这个看似基本的clojure函数速度这么慢?
EN

Stack Overflow用户
提问于 2022-04-08 01:10:33
回答 4查看 488关注 0票数 2

我对clojure还不熟悉,而且作为快速的练习,我编写了一个函数,它应该经过斐波纳契序列,直到它超过999999999 10亿次(做一些额外的计算,但并不是很重要)。我用Java编写了一些同样的东西,虽然我理解Clojure的速度比Java慢,但java程序需要35秒才能完成,Clojure程序则需要27分钟,这让我感到非常惊讶(考虑到nodejs能够在大约8分钟内完成它)。我用repl编译了这个类,并使用这个java -cp `clj -Spath` fib命令运行它。真的不确定为什么这么慢。

代码语言:javascript
运行
复制
(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方法供参考

代码语言:javascript
运行
复制
    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);
    }
EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2022-04-08 05:29:38

您的代码看起来像是一个“基本函数”,但是有两个主要问题:

  • 你用了atomAtom并不是可变的,但它是管理同步状态的构造,没有竞争条件。因此,reset!swap!是原子操作,而且速度很慢。请看下面的示例:
代码语言:javascript
运行
复制
(let [counter (atom 0)]
  (dotimes [x 1000]
    (-> (Thread. (fn [] (swap! counter inc)))
        .start))
  (Thread/sleep 2000)
  @counter)
=> 1000

1000个线程启动,计数器值增加1000倍,结果是1000,不足为奇。但是,将其与volatile!进行比较,它并不是线程安全的:

代码语言:javascript
运行
复制
(let [counter (volatile! 0)]
  (dotimes [x 1000]
    (-> (Thread. (fn [] (vswap! counter inc)))
        .start))
  (Thread/sleep 2000)
  @counter)
=> 989

另见关于Atoms的闭包引用

  • 除非您真的需要atomsvolatiles,否则不应该使用它们。也不鼓励使用loop,因为通常有一些更好的功能,它完全可以满足您的需要。您确实尝试将Java函数重写为Clojure。Clojure需要不同的解决问题的方法,而且您的代码肯定不是惯用的。我建议您不要逐行重写Java代码,但是要找到一些简单的问题,并学习如何在不使用atomvolatile!loop的情况下以Clojure方式解决这些问题。

顺便说一句,还有memoize,它在像您这样的例子中很有用。

票数 2
EN

Stack Overflow用户

发布于 2022-04-10 01:25:34

有一件事是很多Clojure的新手没有意识到的,那就是Clojure默认是一种更高级的语言。这意味着它将迫使您在实现中处理算术溢出,将数字视为可以扩展的对象,防止您更改任何变量,强制您使用线程安全代码,并将您推向依赖递归循环的函数式解决方案。

它也不强制您在默认情况下键入所有内容,这也方便您不必考虑所有类型的所有类型,并确保所有类型是兼容的,例如,向量只包含整数--例如,Clojure不关心,允许您在其中放置整数和空头。

所有这些东西对于快速编写-足够正确、可进化和可维护的应用程序来说是很棒的,但是对于高性能的算法来说并不是很好。

这意味着,默认情况下,Clojure是为实现应用程序而不是为实现高性能算法而优化的。

不幸的是,似乎大多数人“尝试”了一种新的语言,因此Clojure的新成员将倾向于首先使用这种语言来尝试并实现高性能的算法。这是一个明显的不匹配的Clojure默认是擅长的,许多新来的人立即面临额外的摩擦Clojure在这里造成。Clojure假设您将实现一个应用程序,而不是一些高性能的10亿N大小的斐波纳契算法。

但是不要失去希望,Clojure也可以用来实现高性能的算法,但它不是默认的,所以您通常需要更有经验的Clojure开发人员知道如何这样做,因为它不太明显。

以下是Clojure中的算法,它的执行速度与Java实现一样快,它是对精确Java代码的递归重写:

代码语言:javascript
运行
复制
(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:

代码语言:javascript
运行
复制
:deps {generateme/fastmath {:mvn/version "2.1.8"}}

正如你所看到的,在我的笔记本电脑上,它在大约47秒内完成。我还在我的笔记本电脑上运行了你的Java版本来比较我的硬件,而我得到的是: 46947.343671毫秒。

所以在我的笔记本电脑上,你可以看到Clojure和Java基本上都一样快,都是在47秒左右运行。

区别在于,在Java中,编程风格总是有助于实现高性能的算法。您可以直接使用原语类型和基本算法,没有装箱,没有溢出检查,没有同步或原子性或波动性保护的可变变量,等等。

因此,在Clojure中几乎不需要获得类似的性能:

  1. 使用原语类型
  2. 使用原始数学
  3. 避免使用更高级别的托管可变容器(如atom )。

显然,我们也需要运行相同的算法,所以类似的实现。我并不是想比较是否存在另一种算法可以更快地解决相同的问题,而是如何在Clojure中实现相同的algo,使其运行得同样快。

为了在Clojure中执行原语类型,您必须知道只允许您使用letloop在本地上下文中这样做,并且所有函数调用都将撤消原语类型,除非它们也被类型为原语long或double (可以跨越Clojure中函数边界的唯一受支持的原语类型)。

这是我当时做的第一件事,只需使用Clojure的loop/recur重写相同的循环,并声明与您相同的变量,而是使用let隐藏,因此我们不需要托管可变容器。

最后,我使用了快速数学,它是一个库,它提供了许多基本的算术函数版本,这样我们就可以进行基本的数学计算。Clojure的核心有它自己的一些,但它没有国防部,例如,所以我需要引入快速数学。

就这样。

通常,这是您需要知道的,保持原语类型,保持原始数学(使用快速数学),键入提示以避免反射,利用让阴影,保持原语数组,就可以得到Clojure高性能的实现。

这里有一套很好的信息:interop#primitives

最后一件事,Clojure的哲学是,它的目的是快速实现--足够正确、可进化和可维护的应用程序可以扩展。这就是为什么语言是这样的。正如我已经展示的那样,虽然您可以实现高性能的algos,但是Clojure的理念也不是为Java已经擅长的东西重新发明语法。Clojure可以使用Java,因此对于那些需要非常命令式、可变、原始逻辑的算法,它会期望您回到Java,将其作为一个静态方法来编写,然后从Clojure中使用它。或者它认为你甚至会委托给比Java更有表现力的东西,并且使用BLAS,或者GPU来执行超快的矩阵运算,或者类似的东西。这就是为什么它不愿意提供自己的命令式构造,或者原始内存访问等等,因为它认为自己没有比运行过的主机做得更好的事情。

票数 16
EN

Stack Overflow用户

发布于 2022-04-08 07:28:16

如果您是编程初学者,我建议您在假定语言/lib/framework/platform错误之前,始终假定代码是错误的。

看看Fibonacci序列在Java中的各种实现,您可能会学到一些东西。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71790601

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档