Rust vs. Clojure speed comparison, any improvement for Clojure code?

I translated the Rust code example into Clojure.

Rust (imperative and functional) : Note. Both imperative and functional codes are here 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 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 Time (imperative):

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

 ~/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] (* nn))) (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:

  • What is the difference between (reduce +) and (apply +) in Clojure?
  • Is this Clojure code an idiomatic way?
  • Can I conclude that speed: Clojure> Mandatory Rust> Functionality Rust? Clojure really surprised me here for performance.
+5
source share
2 answers

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

(range) will 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 do not know how many more arguments will be provided.

Showing the benchmarking results with the criterion, we see that the application of these changes leads to a 36% reduction in lead time:

 user> (crit/bench (->> (iterate inc 0) (map (fn [n] (* nn))) (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] (* nn))) (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 
+4
source

Clojure's code seems idiomatic in my opinion, but it does 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 really cannot conclude that one is faster than the other based on this test. Benchmarking is a difficult game. You need to test your programs in production environments at high cost in order to get any meaningful results.

What is the difference (decrease +) and (apply +) in Clojure?

apply is a higher order function with arity variable. Its first argument is a function of the arity variable, takes up a bunch of intermediate arguments, and then the last arg should be a list of args. It works by first inserting intermediate arguments into the argument list, then passing arguments 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 , I think the docs say it explicitly

 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 where you could use either apply f coll or reduce f coll , but usually I use apply when f has the arity variable and reduce when f is a 2-ary function.

+2
source

Source: https://habr.com/ru/post/1207284/


All Articles