用Clojure编写的阶乘函数的低性能

时间:2016-12-12 14:49:24

标签: ruby performance clojure

我是Clojure的新手。在试验它时,我编写了I函数来计算n!。我的Clojure代码如下:

(defn factorial
    [n]
    (reduce * (biginteger 1) (range 1 (inc n))))

然后我在repl中运行以下内容。

(time (factorial 100))

这就是结果:

"Elapsed time: 0.50832 msecs"
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000N

然后我在Ruby中创建了一个类似的解决方案:

def factorial(n)
  start = Time.now.to_f
  (2..n).inject(1) { |p, f| p * f }
  finish = Time.now.to_f
  time_taken = finish - start
  puts "It took: #{(time_taken * 1000)} msecs" 
end

with irb我跑了factorial(100) 导致:

It took: 0.06556510925292969 msecs
 => 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Ruby版本的性能似乎要大得多,尽管我已经看到大多数证据表明Clojure应该具有卓越的性能。有什么我误解的东西或我的Clojure解决方案的一些元素会减慢它的速度吗?

3 个答案:

答案 0 :(得分:6)

BigInteger来自java,而BigInt是在Clojure的核心中实现的。马上,这需要一些与interopability相关的费用。

此外,BigInt表示为a long or a BigInteger。只要有可能,the long is used。但是,如果任何操作使其overflow,则生成的新BigInt will use it's BigInteger。 Java的long映射到本机架构的实现,因此速度明显更快。这类似于Ruby在FixnumBignum之间的神奇转换。

由于您几乎只使用小数字(1到100以及大量中间产品),因此可以显着提升性能。

答案 1 :(得分:2)

@ndn's answer

之后

您可以通过类型提示参数n

来获得更快的速度
(defn factorial [^long n]
  (reduce * (bigint 1) (range 1 (inc n))))

答案 2 :(得分:2)

Miro-benchmarkinging经常会产生误导,一般而言,它很难做到正确。在clojure中获得合理接近的最简单方法(我发现的是标准库(感谢Hugo!)。如果我从简单循环计算阶乘的丑陋版本开始,我会得到大约3 ns。

user> (defn loopy-fact [x]
        (loop [y x
               answer-so-far 1]
          (if (pos? y)
            (recur (dec y) (*' answer-so-far y))
            answer-so-far)))
#'user/loopy-fact

user> (loopy-fact 100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000N

然后让它进行基准测试:

user> (criterium.core/bench #(loopy-fact 100))
WARNING: Final GC required 11.10521514596218 % of runtime
WARNING: Final GC required 1.069604210579865 % of runtime
Evaluation count : 12632130300 in 60 samples of 210535505 calls.
             Execution time mean : 2.978360 ns
    Execution time std-deviation : 0.116043 ns
   Execution time lower quantile : 2.874266 ns ( 2.5%)
   Execution time upper quantile : 3.243399 ns (97.5%)
                   Overhead used : 1.844334 ns

Found 4 outliers in 60 samples (6.6667 %)
    low-severe   2 (3.3333 %)
    low-mild     2 (3.3333 %)
 Variance from outliers : 25.4468 % Variance is moderately inflated by outliers

如果我们通过使用普通的Clojure样式,使用map和reduce,使代码看起来更好,并且没有努力使其快速。

user> (defn mapy-fact [x]
        (reduce *' (range 1 (inc x)))
#'user/mapy-fact

user> (mapy-fact 100)
933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000N

现在让我们找出相比的方法:

user> (criterium.core/bench #(mapy-fact 100))
Evaluation count : 8674569060 in 60 samples of 144576151 calls.
             Execution time mean : 5.208031 ns
    Execution time std-deviation : 0.265287 ns
   Execution time lower quantile : 5.032058 ns ( 2.5%)
   Execution time upper quantile : 5.833466 ns (97.5%)
                   Overhead used : 1.844334 ns

Found 4 outliers in 60 samples (6.6667 %)
    low-severe   1 (1.6667 %)
    low-mild     3 (5.0000 %)
 Variance from outliers : 36.8585 % Variance is moderately inflated by outliers

它有点慢,但只慢了2纳秒

这比你在测试中看起来要好得多,因为标准运行该函数的次数足以让JVM的Hotspot编译器绕过编译它并内联所有部分。这说明了微基准测试在JVM上可能会产生误导性的原因。你几乎肯定会对这种情况保持标准。

PS:*'是"自动推广"乘法运算符,它会根据需要将它的类型提升为大整数或大小数