Nick
Nick

Reputation: 9051

Rust vs. Clojure speed comparasion, any improvement for the Clojure code?

I translated a piece of Rust code example to Clojure.

Rust (imperative and functional): Note: Both imperative and functional code here are together for clarity. In the test, I run them separately.

// 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 (imperative) time:

~/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 (Functional) time:

~/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 time:

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

Question:

  1. What's the difference of (reduce +) and (apply +) in Clojure?
  2. Is this Clojure code the idiomatic way?
  3. Can I draw conclusion that Speed: Clojure > Rust imperative > Rust functional ? Clojure really surprised me here for performance.

Upvotes: 4

Views: 4012

Answers (2)

turingcomplete
turingcomplete

Reputation: 2188

The Clojure code looks idiomatic in my opinion but it's doing a lot of unnecessary work. Here is an alternative way.

(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

You can't really conclude that one is faster than the other based on this test though. Benchmarking is a tricky game. You need to test your programs in production-like environments with heavy inputs to get any meaningful results.

What's the difference of (reduce +) and (apply +) in Clojure?

apply is a higher order function with variable arity. Its first argument is a function of variable arity, takes a bunch of intervening args, and then the last arg must be a list of args. It works by first consing the intervening args to the list of args, then passes the args to the function.

Example:

(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

As for reduce, well I think the docs say it clearly

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

There are situations were you could use either apply f coll or reduce f coll, but I normally use apply when f has variable arity, and reduce when f is a 2-ary function.

Upvotes: 2

noisesmith
noisesmith

Reputation: 20194

If you look at the source of +, you will see that (reduce +) and (apply +) are identical for higher argument counts. (apply +) is optimized for the 1 or 2 argument versions though.

(range) is going to be much faster than (iterate inc 0) for most cases.

partial is slower than a simple anonymous function, and should be reserved for cases where you don't know how many more args will be supplied.

Showing the results of benchmarking with criterium, we can see that applying those changes give a 36% drop in execution time:

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

Upvotes: 4

Related Questions