Rust vs. Clojure 速度对比,Clojure 代码是否有改进空间?

4
我已经将一段 Rust 代码示例翻译成 Clojure。 Rust(命令式和函数式)注意:这里将命令式和函数式代码放在一起以增加清晰度。在测试中,我会分别运行它们。
// The `AdditiveIterator` trait adds the `sum` method to iterators
use std::iter::AdditiveIterator;
use std::iter;  
    fn main() {
    println!("Find the sum of all the squared odd numbers under 1000");
    let upper = 1000u;

    // Imperative approach
    // Declare accumulator variable
    let mut acc = 0;
    // Iterate: 0, 1, 2, ... to infinity
    for n in iter::count(0u, 1) {
        // Square the number
        let n_squared = n * n;

        if n_squared >= upper {
            // Break loop if exceeded the upper limit
            break;
        } else if is_odd(n_squared) {
            // Accumulate value, if it's odd
            acc += n_squared;
        }
    }
    println!("imperative style: {}", acc);

    // Functional approach
    let sum_of_squared_odd_numbers =
        // All natural numbers
        iter::count(0u, 1).
        // Squared
        map(|n| n * n).
        // Below upper limit
        take_while(|&n| n < upper).
        // That are odd
        filter(|n| is_odd(*n)).
        // Sum them
        sum();
    println!("functional style: {}", sum_of_squared_odd_numbers);
}

fn is_odd(n: uint) -> bool {
    n % 2 == 1
}  

Rust(命令式)时间:

~/projects/rust_proj $> time ./hof_imperative 
Find the sum of all the squared odd numbers under 1000
imperative style: 5456

real    0m0.006s
user    0m0.001s
sys 0m0.004s

~/projects/rust_proj $> time ./hof_imperative 
Find the sum of all the squared odd numbers under 1000
imperative style: 5456

real    0m0.004s
user    0m0.000s
sys 0m0.004s

~/projects/rust_proj $> time ./hof_imperative 
Find the sum of all the squared odd numbers under 1000
imperative style: 5456

real    0m0.005s
user    0m0.004s
sys 0m0.001s

Rust(函数式)时间:

~/projects/rust_proj $> time ./hof 
Find the sum of all the squared odd numbers under 1000
functional style: 5456

real    0m0.007s
user    0m0.001s
sys 0m0.004s

~/projects/rust_proj $> time ./hof 
Find the sum of all the squared odd numbers under 1000
functional style: 5456

real    0m0.007s
user    0m0.007s
sys 0m0.000s

~/projects/rust_proj $> time ./hof 
Find the sum of all the squared odd numbers under 1000
functional style: 5456

real    0m0.007s
user    0m0.004s
sys 0m0.003s

Clojure:

(defn sum-square-less-1000 []
  "Find the sum of all the squared odd numbers under 1000
"
  (->> (iterate inc 0)
       (map (fn [n] (* n n)))
       (take-while (partial > 1000))
       (filter odd?)
       (reduce +)))

Clojure时间:

user> (time (sum-square-less-1000))
"Elapsed time: 0.443562 msecs"
5456
user> (time (sum-square-less-1000))
"Elapsed time: 0.201981 msecs"
5456
user> (time (sum-square-less-1000))
"Elapsed time: 0.4752 msecs"
5456

问题:

  1. (reduce +)(apply +)在Clojure中有什么区别?
  2. 这段Clojure代码是否符合惯用方式?
  3. 我能得出结论:速度:Clojure > Rust命令式 > Rust函数式吗?Clojure在性能方面真的让我感到惊讶。

10
time shell 工具包含整个程序的执行过程,不仅仅是直接计算(与 Clojure 的 time 不同),因此不适用于比较运行时间非常短的程序,例如 time true(什么也不做)在我的电脑上的运行时间范围从 0.002 秒到 0.006 秒。 - huon
1
这个问题应该发布在https://codereview.stackexchange.com/。 - Chris Morgan
1
看看使用transducers后Clojure会快多少会很有趣。 - Thumbnail
这个问题似乎不适合讨论,因为它涉及性能比较和基准测试,而不是关于如何编写程序的内容。 - amalloy
2个回答

4
如果查看+的源代码,您会发现对于更高的参数计数,(reduce +)(apply +)是相同的。但是(apply +)针对于1或2个参数版本进行了优化。
在大多数情况下,(range)要比(iterate inc 0)快得多。 partial比简单的匿名函数慢,应该保留用于不知道会提供多少个参数的情况。
通过使用criterium进行基准测试的结果,我们可以看到这些更改导致执行时间降低了36%:
user> (crit/bench (->> (iterate inc 0)
                       (map (fn [n] (* n n)))
                       (take-while (partial > 1000))
                       (filter odd?)
                       (reduce +)))
WARNING: Final GC required 2.679748643529675 % of runtime
Evaluation count : 3522840 in 60 samples of 58714 calls.
             Execution time mean : 16.954649 µs
    Execution time std-deviation : 140.180401 ns
   Execution time lower quantile : 16.720122 µs ( 2.5%)
   Execution time upper quantile : 17.261693 µs (97.5%)
                   Overhead used : 2.208566 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
nil
user> (crit/bench (->> (range)
                       (map (fn [n] (* n n)))
                       (take-while #(> 1000 %))
                       (filter odd?)
                       (reduce +)))
Evaluation count : 5521440 in 60 samples of 92024 calls.
             Execution time mean : 10.993332 µs
    Execution time std-deviation : 118.100723 ns
   Execution time lower quantile : 10.855536 µs ( 2.5%)
   Execution time upper quantile : 11.238964 µs (97.5%)
                   Overhead used : 2.208566 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   1 (1.6667 %)
    low-mild     1 (1.6667 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
nil

2

在我看来,Clojure代码看起来很符合惯用语,但它正在执行许多不必要的工作。这里是一种替代方式。

(reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2))


user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.180778 msecs"
5456
user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.255972 msecs"
5456
user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.346192 msecs"
5456
user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.162615 msecs"
5456
user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.257901 msecs"
5456
user=> (time (reduce #(+ %1 (* %2 %2)) 0 (range 1 32 2)))
"Elapsed time: 0.175507 msecs"
5456

根据这个测试,你不能真正得出哪一个比另一个更快的结论。基准测试是一个棘手的游戏。你需要在类似于生产环境的环境中进行测试,并使用大量的输入来获得任何有意义的结果。

Clojure 中 (reduce +) 和 (apply +) 有什么区别?

apply 是一个具有可变性的高阶函数。它的第一个参数是一个可变性函数,接受一堆介入参数,然后最后一个参数必须是参数列表。它通过首先将介入参数与参数列表合并,然后将参数传递给函数来工作。

例如:

(apply + 0 1 2 3 '(4 5 6 7))
=> (apply + '(0 1 2 3 4 5 6 7))
=> (+ 0 1 2 3 4 5 6 7)
=> result

关于reduce,我认为文档已经说得很清楚了。

user=> (doc reduce)
-------------------------
clojure.core/reduce
([f coll] [f val coll])
  f should be a function of 2 arguments. If val is not supplied,
  returns the result of applying f to the first 2 items in coll, then
  applying f to that result and the 3rd item, etc. If coll contains no
  items, f must accept no arguments as well, and reduce returns the
  result of calling f with no arguments.  If coll has only 1 item, it
  is returned and f is not called.  If val is supplied, returns the
  result of applying f to val and the first item in coll, then
  applying f to that result and the 2nd item, etc. If coll contains no
  items, returns val and f is not called.
nil

有些情况下,你既可以使用apply f coll,也可以使用reduce f coll,但通常当f具有可变元数时,我会使用apply,而当f是一个二元函数时,则会使用reduce


1
  • 是可变元数,并且对于更高的参数计数是通过 reduce 实现的。
- noisesmith
1
对于比较目的,criterium 给出了执行时间平均值为 1.1 微秒(相对于原始版本的 17 和我改进版本的 11)。干得好。 - noisesmith
我认为将unchecked-math设置为true可以使其更快。虽然我还没有测试过。 - turingcomplete
(binding [*unchecked-math* true] (crit/bench (reduce #(+ ^long %1 (* ^long %2 ^long %2)) 0 (range 1 32 2)))) 将其降至922纳秒。 - noisesmith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接