【问题标题】:Clojure - more effective 'min-by' function and performance analysisClojure - 更有效的“min-by”函数和性能分析
【发布时间】:2017-06-09 09:55:31
【问题描述】:

我是 Clojure 的新手,我在 2 个月前开始学习这门语言。我正在阅读“clojure 的乐趣”一书,并在函数式编程主题中找到了一个 min-by 函数。我在想,我已经完成了我的 min-by 函数,这在至少 10.000 个项目上似乎至少提高了 50% 的性能。下面是函数

; the test vector with random data
(def my-rand-vec (vec (take 10000 (repeatedly #(rand-int 10000)))))

; the joy of clojure min-by
(defn min-by-reduce [f coll]
  (when (seq coll)
        (reduce (fn [min other]
                    (if (> (f min) (f other))
                        other
                        min))
                      coll)))

(time (min-by-reduce eval  my-rand-vec))

; my poor min-by 
(defn min-by-sort [f coll]
  (first (sort (map f coll))))

(time (min-by-sort eval my-rand-vec))

终端输出是

"Elapsed time: 91.657505 msecs"
"Elapsed time: 62.441513 msecs"

我的解决方案是否存在性能或资源缺陷?我真的很好奇 clojure Gurus 为这个功能提供了更优雅的 clojure 解决方案。

编辑

带有标准的更清晰的测试代码。

(ns min-by.core
  (:gen-class))

(use 'criterium.core)

(defn min-by-reduce [f coll]
  (when (seq coll)
    (reduce (fn [min other]
                (if (> (f min) (f other))
                    other
                    min))
                  coll)))


(defn min-by-sort [f coll]
  (first (sort-by f coll)))

(defn my-rand-map [length]
  (map #(hash-map :resource %1 :priority %2) 
      (take length (repeatedly #(rand-int 200)))
      (take length (repeatedly #(rand-int 10)))))


(defn -main
  [& args]
  (let [rand-map (my-rand-map 100000)]
  (println "min-by-reduce-----------")
  (quick-bench (min-by-reduce :resource rand-map))
  (println "min-by-sort-------------")
  (quick-bench (min-by-sort :resource rand-map))
  (println "min-by-min-key----------")
  (quick-bench (apply min-key :resource rand-map)))
 )

终端输出是:

min-by-reduce-----------
            Evaluation count : 60 in 6 samples of 10 calls.
         Execution time mean : 11,366539 ms
Execution time std-deviation : 2,045752 ms
Execution time lower quantile : 9,690590 ms ( 2,5%)
Execution time upper quantile : 14,763746 ms (97,5%)
               Overhead used : 3,292762 ns

Found 1 outliers in 6 samples (16,6667 %)
    low-severe       1 (16,6667 %)
Variance from outliers : 47,9902 % Variance is moderately inflated by outliers

min-by-sort-------------
             Evaluation count : 6 in 6 samples of 1 calls.
          Execution time mean : 174,747463 ms
 Execution time std-deviation : 18,431608 ms
Execution time lower quantile : 158,138543 ms ( 2,5%)
Execution time upper quantile : 203,420044 ms (97,5%)
                Overhead used : 3,292762 ns

Found 1 outliers in 6 samples (16,6667 %)
    low-severe       1 (16,6667 %)
Variance from outliers : 30,7324 % Variance is moderately inflated by outliers

min-by-min-key----------
             Evaluation count : 36 in 6 samples of 6 calls.
          Execution time mean : 17,405529 ms
 Execution time std-deviation : 1,661902 ms
Execution time lower quantile : 15,962259 ms ( 2,5%)
Execution time upper quantile : 19,366893 ms (97,5%)
                Overhead used : 3,292762 ns

【问题讨论】:

  • 如果您想尝试一下,eval 是一个糟糕的选择。您的整个运行时都由它主导。你变慢的原因是你在你的 reduce 版本中调用了 eval 2*N 而在你的 sort 版本中只调用了 N 次。
  • 谢谢!我已将eval 更改为identity,结果完全改变了。 100.000 个项目的终端输出是 "Elapsed time: 8.234689 msecs" "Elapsed time: 131.30328 msecs"

标签: clojure


【解决方案1】:

首先,您的版本返回(f min) 而不是min,从理论上讲,找到最小值是线性O(n) 操作,而排序和取第一个是准线性O(n log n)。对于小向量,可能很难获得准确的时序结果,因此,时间复杂度并不能保证准线性运算总是比线性运算慢!

尝试使用 1,000,000 或更多的样本量,并使用更复杂的键功能。例如,生成示例字符串并使用length 或类似的方式进行排序。这样您就可以获得更接近真实世界的结果。

提示:您可以使用identity 代替eval 来“跳过”提供用于测试目的的功能。可能不会对基准测试产生太大影响,但只是让您了解该功能。

正如用户 ClojureMostly 指出的那样,eval 是一个很大的瓶颈,并且使基准测试偏向了错误的结论。

【讨论】:

  • 谢谢!好地方!我错过了这个返回值错误。 :)
【解决方案2】:

我相信 JoC 试图说明 reduce 的用法,仅此而已。

我也将 JoC 作为我的第一本书阅读,但我希望在我先阅读更多介绍性书籍之前保存它。那里有很多不错的。您甚至可以在线阅读(大部分)Clojure for the Brave and Truehttp://www.braveclojure.com/clojure-for-the-brave-and-true/ 我还建议购买完整的硬拷贝版本。

您还应该查看 Clojure Cookbook:https://github.com/clojure-cookbook/clojure-cookbook 和以前一样,我也建议购买完整的硬拷贝版本。

【讨论】:

  • 谢谢!我同意。我买了,我首先阅读了 Brave Clojure。这是一本为初学者介绍 FP 和 Clojure 的非常好的书。 JoC 高一到两级。
【解决方案3】:

我更改了min-by-sort 函数。整个画面都变了。

(defn min-by-sort [f coll]
  (first (sort-by f coll)))

10.000 个项目的 2 个函数的终端输出是

"Elapsed time: 0.863016 msecs"
"Elapsed time: 11.44852 msecs"

那么,问题来了,有没有更好或者更优雅的min-by-xxx函数,根据一个函数找到集合中的最小值并返回原值?

最后用地图和关键字之类的函数进行测试。

; find (f min) by reduce
(defn min-by-reduce [f coll]
  (when (seq coll)
    (reduce (fn [min other]
                (if (> (f min) (f other))
                    other
                    min))
                  coll)))

; find (f min) by sort-by
(defn min-by-sort [f coll]
  (first (sort-by f coll)))

;a  helper function to build a sequence of {:resource x, :priority y} maps
(defn my-rand-map [length]
  (map #(hash-map :resource %1 :priority %2) 
      (take length (repeatedly #(rand-int 200)))
      (take length (repeatedly #(rand-int 10)))))

; test with 100 items in the seq
(let [rand-map (my-rand-map 100)]
 (time (min-by-reduce :resource rand-map))
 (time (min-by-sort :resource rand-map)))

测试 100 个项目
"Elapsed time: 0.245403 msecs" "Elapsed time: 0.18094 msecs"

测试 1000 个项目
"Elapsed time: 2.653952 msecs" "Elapsed time: 3.214373 msecs"

测试 10.000 个项目
"Elapsed time: 14.275679 msecs" "Elapsed time: 38.064996 msecs"

我认为,区别当然是排序项目,但减少只是遍历项目并累积实际最小值。是真的吗?

【讨论】:

  • 谢谢! (apply min-key :resource rand-map) 赢了!对于 10.000 个项目,它是 “经过的时间:6.270974 毫秒”
【解决方案4】:

标准的min-key 只需要一点点调整:

(defn min-by [f coll]
  (when (seq coll)
    (apply min-key f coll)))

如果您查看 the source code for min-key,这与 JoC 的 min-by-reduce 基本相同。

【讨论】:

  • 是的。 min-key 似乎非常接近,但似乎比 JoC min-by-reduce 快 4-5 倍。为什么?
  • @tkircsi 天真的 Clojure 时间是出了名的不可靠。尝试Criterium 以获得可靠的结果。让我知道你发现了什么:)。
  • 谢谢!我会试一试,然后返回结果。学习的好课。 :)
猜你喜欢
  • 2011-11-15
  • 2012-11-12
  • 2017-12-07
  • 1970-01-01
  • 1970-01-01
  • 2013-04-06
  • 2012-11-04
  • 2014-10-27
  • 2012-05-28
相关资源
最近更新 更多