-
-
Save jwieringa/7812094 to your computer and use it in GitHub Desktop.
| ;Multiples of 3 and 5 | |
| ; If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. | |
| ; Find the sum of all the multiples of 3 or 5 below 1000. | |
| ; Solve using seqs | |
| ; My personal favorite | |
| ; I like it because I think it is using lazy seqs which, if I understand things, is a great | |
| ; way to solve this problem in clojure. It does bother me that there are numbers being generated | |
| ; that are being dropped. i.e. is there a way to process this so that the union can be dropped? | |
| (defn find-and-sum-multiples [m1 m2 limit] | |
| "Returns the sum of multiples of m1 and m2 upto limit" | |
| (reduce + (clojure.set/union (set (range 0 limit m1)) | |
| (set (range 0 limit m2))))) | |
| (find-and-sum-multiples 3 5 1000) | |
| ; Solve Recursively | |
| (defn multiple-3-or-5 [n] | |
| (cond (= 0 (rem n 5)) n | |
| (= 0 (rem n 3)) n | |
| :else 0)) | |
| (defn recur-multiples-of-3-5 [limit] | |
| (loop [limit limit | |
| acc 0] | |
| (if (= limit 0) | |
| acc | |
| (recur (dec limit) (+ acc (multiple-3-or-5 limit)))))) | |
| (recur-multiples-of-3-5 1000) |
Benchmarks
Summary: My assumption was that find-and-sum-multiples was going to be the most efficient because it utilizes lazy sequencing. I think that these benchmarks verify that assumption, but I think there may be some optimizations in find-and-sum-multiples that the others are not able to take advantage of (i.e. once the function is run once do the solutions stick around?).
While benchmarking perhaps isn't the best way to discover idiomatic Clojure, it seem to provide some insight into how the individual bricks are behaving.
Benchmarking with: (use 'criterium.core)
Using seqs:
user=> (defn find-and-sum-multiples [m1 m2 limit]
#_=> "Returns the sum of multiples of m1 and m2 upto limit"
#_=> (reduce + (clojure.set/union (set (range 0 limit m1))
#_=> (set (range 0 limit m2)))))
#'user/find-and-sum-multiples
user=> (bench (find-and-sum-multiples 100 3 5))
WARNING: Final GC required 2.1605409926290777 % of runtime
Evaluation count : 49463460 in 60 samples of 824391 calls.
Execution time mean : 1.215210 µs
Execution time std-deviation : 98.359408 ns
Execution time lower quantile : 1.128250 µs ( 2.5%)
Execution time upper quantile : 1.429801 µs (97.5%)
Overhead used : 2.709801 ns
Found 3 outliers in 60 samples (5.0000 %)
low-severe 2 (3.3333 %)
low-mild 1 (1.6667 %)
Variance from outliers : 60.1551 % Variance is severely inflated by outliers
nilUsing Recursion:
user=> (defn multiple-3-or-5? [n]
#_=> (cond (= 0 (rem n 5)) n
#_=> (= 0 (rem n 3)) n
#_=> :else 0))
#'user/multiple-3-or-5?
user=> (defn recur-multiples-of-3-5 [limit]
#_=> (loop [limit limit
#_=> acc 0]
#_=> (if (= limit 0)
#_=> acc
#_=> (recur (dec limit) (+ acc (multiple-3-or-5? limit))))))
#'user/recur-multiples-of-3-5
user=> (bench (recur-multiples-of-3-5 100))
Evaluation count : 7547640 in 60 samples of 125794 calls.
Execution time mean : 7.914505 µs
Execution time std-deviation : 321.042089 ns
Execution time lower quantile : 7.612677 µs ( 2.5%)
Execution time upper quantile : 8.621702 µs (97.5%)
Overhead used : 2.709801 ns
Found 5 outliers in 60 samples (8.3333 %)
low-severe 3 (5.0000 %)
low-mild 2 (3.3333 %)
Variance from outliers : 27.0628 % Variance is moderately inflated by outliers
nilUsing Macro:
This result surprised me. I suspect that there is a way to build this macro such that it behaves more efficiently.
user=> (bench (->> [3 5]
#_=> (map (partial range 0 100))
#_=> flatten
#_=> set
#_=> (reduce +))
#_=> )
Evaluation count : 1150500 in 60 samples of 19175 calls.
Execution time mean : 53.613235 µs
Execution time std-deviation : 828.676554 ns
Execution time lower quantile : 52.091475 µs ( 2.5%)
Execution time upper quantile : 54.968066 µs (97.5%)
Overhead used : 2.709801 ns
nilAdding another one to the mix after reading about a patter in The Joy of Clojure.
(defn multiple? [n]
(or (zero? (rem n 5))
(zero? (rem n 3))))
(reduce +
(for [x (range 1 100) :when (multiple? x)]
x))Benchmarked:
user=> (defn multiple? [n]
#_=> (or (zero? (rem n 5))
#_=> (zero? (rem n 3))))
#'user/multiple?
user=> (bench (reduce + (for [x (range 1 100) :when (multiple? x)] x)))
WARNING: Final GC required 2.136619650272382 % of runtime
Evaluation count : 6217140 in 60 samples of 103619 calls.
Execution time mean : 10.131764 µs
Execution time std-deviation : 349.525571 ns
Execution time lower quantile : 9.619484 µs ( 2.5%)
Execution time upper quantile : 10.970652 µs (97.5%)
Overhead used : 2.747116 ns
Found 2 outliers in 60 samples (3.3333 %)
low-severe 2 (3.3333 %)
Variance from outliers : 20.6394 % Variance is moderately inflated by outliers
nil
And then I find this:
Note: While this demonstrates the coolness of macros, I'm not sure it is a good use of one. Current reasoning, if a macro isn't necessary, then avoid using it. In this case, a macro isn't necessary and I'm not sure it brings any clarity to what is going on. I do wonder if it has a better benchmark.