【问题标题】:Why Clojure's frequencies is not faster than Python's collections.Counter?为什么 Clojure 频率不比 Python 的 collections.Counter 快?
【发布时间】:2015-05-10 16:43:52
【问题描述】:

注意——这里是 Clojure 新手。

我预计发生次数计数器的 Clojure 实现会比 Python 快得多。但事实证明 Python 更快!对此有何解释?如何推断 Python 在哪里会更快,Clojure 在哪里会更快?

我将 CPython 2.7.8 和 Clojure 1.6.0 与 OpenJDK 64 位服务器 VM 1.7.0_75-b13 一起使用。

Python 代码:

from string import ascii_lowercase
import timeit

DATA = list(ascii_lowercase)*100000

def frequencies(items):
    counter = {}
    for item in items:
        counter[item] = counter.setdefault(item, 0) + 1

    return counter

print(timeit.timeit(lambda: frequencies(DATA), number=1))

输出:

0.528199911118

Clojure 代码:

(ns test
  (:gen-class))

(defn -main
  [& args]
  (let
    [data
     (doall (apply concat
                   (repeat 100000 (map char (range (int \a) (+ (int \z) 1))))))]
    (time (frequencies data))))

输出:

"Elapsed time: 861.668743 msecs"

更新 #1

我做了一些优化:

(ns test
  (:gen-class))

(defn frequencies2
  [coll]
  (into {} (reduce (fn [^java.util.HashMap counts x]
             (.put counts x
                   (inc (or (.get counts x) 0))) counts)
           (java.util.HashMap. {}) coll)))    

(defn -main
  [& args]
  (let
    [data
     (doall (apply concat
                   (repeat 10000 (map char (range (int \a) (inc (int \z)))))))]
    (time (dotimes [_ 15] (frequencies data)))
    (time (dotimes [_ 15] (frequencies2 data)))))

它输出:

"Elapsed time: 1524.498547 msecs"
"Elapsed time: 476.387626 msecs"

所以我补充两个问题:

  • 为什么clojure.core implementation 不使用类型提示?
  • 如何进一步优化性能?我可以为整数的哈希映射值添加类型提示吗?

【问题讨论】:

  • Clojure 代码在我的机器上更快,即使没有为 JIT 预热。
  • 如果我没记错的话,我认为 Clojure 版本更相当于这个 reduce(lambda x, y: x + Counter(y), a, Counter()) 而不是你发布的那个。
  • @AshwiniChaudhary frequencies 完全实现 like that
  • 我正在使用带有 core 2 duo 的旧 Macbook。 Python 版本始终以大约 768 毫秒的速度出现,而 Clojure 的平均时间大约为 575 毫秒(倾向于在 ±20 毫秒左右跳跃)。很多事情可能会影响时间。你运行的是哪个版本的 Clojure(或 Python),你运行的是哪个版本的 JVM,等等……
  • 您的更新版本在比旧版本小十倍的数据对象上调用频率。此外,您的data 计算可以缩短/显着加快到(let [ascii-lowercase (char-array "abcdefghijklmnopqrstuvwxyz")] (reduce into [] (repeat 100000 ascii-lowercase)))Don't use concat。 FWIW,使用 frequencies 的 100.000 版本在我使用 Oracle JDK 的机器上需要大约 554 毫秒,而 Python 版本需要 0.449 秒,所以它们在同一个球场上,这不是你所看到的。

标签: python performance clojure


【解决方案1】:

在 JVM 上对任何东西进行基准测试都是一项棘手的工作。 JVM 将在运行时优化您的代码,但很难预测何时会发生或控制它。要获得比两个函数(都是 Clojure)之间最一般的性能提示更多的东西,您需要使用专用的基准测试库。 Criterium 是 Clojure 社区中最常用的库。

关于性能的推理非常棘手,尤其是在两个非常不同的平台之间。我认为基准测试和测量大量代码将是在两种语言之间建立直觉的最佳方式。深入研究底层数据结构并了解它们的性能特征将对您有所帮助。正如您在frequencies2 中看到的那样,与使用 Clojure 的持久映射相比,使用可变 HashMap 可以获得更好的性能。然而,如果你走这条路,你就会失去所有不变的优点。

由于某些原因,Clojure 版本没有类型提示。

  1. Frequencies 是一个通用函数,因此它可以处理任何类型的值。

  2. hinting 类型仅对与 Java 互操作具有真正的性能价值。来自Clojure Programming,第 367 页

    函数参数或返回的类型提示不是签名声明:它们不影响函数可以接受或返回的类型。它们的唯一作用是允许 Clojure 调用 Java 方法并使用编译时生成的代码访问 Java 字段——而不是在运行时使用反射来搜索与所讨论的互操作形式匹配的方法或字段的慢得多的选项。 因此,如果提示没有通知互操作操作,它们实际上是无操作的。 [...] 这与 Clojure 确实提供的签名声明形成对比,但仅适用于原始参数和返回类型。

如果您在函数中专门使用 Java primitives,那么您可以使用类型 declarations 来优化它。再次来自Clojure Programming,第 438 页

当 Clojure 编译一个函数时,它会生成一个相应的类,该类实现了 Clojure 的 Java 接口之一 clojure.lang.IFn。 IFn 定义了许多调用方法;当您调用 Clojure 函数时,这些是在幕后调用的。

所有参数和返回值都是(未修饰的)函数边界处的对象。这些调用方法都接受参数并返回根类型 java.lang.Object 的值。这启用了 Clojure 的动态类型默认值(即,您的函数的实现确定了可接受的参数类型的范围,而不是语言强制执行的静态类型声明),但具有强制 JVM 将作为参数传递给的任何原语装箱的副作用。或作为这些函数的结果返回。因此,如果我们使用原始参数(例如 long)调用 Clojure 函数,该参数将被装箱到 Long 对象中,以符合 Clojure 函数底层调用方法的类型签名。类似地,如果一个函数的结果是一个原始值,则底层的 Object 返回类型确保在调用者收到结果之前对这些原始值进行装箱。 [...]

(defn round ^long [^double a] (Math/round a))
;= #'user/round
(seq (.getDeclaredMethods (round foo)))
;= (#<Method public java.lang.Object user$round.invoke(java.lang.Object)> 
#<Method public final long user$round.invokePrim(double)>)

如果您想进一步优化这一点并且专门处理 Java 原始整数,那么您可以将 ^int 类型声明用于您的参数或函数的返回值。但是我认为它对您当前的代码没有任何用处。另一种下降的方法是将计数并行化并在最后将它们组合起来。您还可以查看http://java-performance.info/implementing-world-fastest-java-int-to-int-hash-map/ 以获取更多想法,尽管此时您实际上是在用一种有趣的领域特定语法编写 Java。

【讨论】:

  • 但是,如果你走那条路,你将失去所有不变的优点。 请注意,我在 frequencies2 中使用 (into {} ... 将可变的 HashMap 转换为 Clojure @ 987654331@.
  • 我的意思是,您通常能够在使用可变对象的微基准测试中获得更好的性能。如果您没有将 HashMap 转换回 Clojure 映射,那么频率 2 会更快。
【解决方案2】:

使用

(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)    ;; clojure 1.7

获得编译器的警告。 您的更新版本足够快,但有两个警告:

Boxed math warning, /home/.../foo/src/foo/core.clj:68:28 - call: public static java.lang.Number clojure.lang.Numbers.unchecked_inc(java.lang.Object).
Boxed math warning, /home/.../foo/src/foo/core.clj:68:28 - call: public static java.lang.Number clojure.lang.Numbers.inc(java.lang.Object).

这是具有更多提示和标准输出的版本:

(defn frequencies2
  []
  (into {} (reduce (fn [^java.util.HashMap counts x]
                     (let [^int v (or (.get counts x) 0)]
                       (.put counts x
                             (inc v))) counts)
                   (HashMap.) data)))

标准:

> (bench (frequencies2))
Evaluation count : 720 in 60 samples of 12 calls.
             Execution time mean : 91.375085 ms
    Execution time std-deviation : 1.415710 ms
   Execution time lower quantile : 89.957446 ms ( 2.5%)
   Execution time upper quantile : 95.135782 ms (97.5%)
                   Overhead used : 2.313579 ns

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

请注意,原始frequencies 版本要慢得多: "Elapsed time: 525.264668 msecs"

【讨论】:

    猜你喜欢
    • 2012-12-20
    • 2017-10-12
    • 1970-01-01
    • 2017-05-26
    • 2016-06-30
    • 2014-06-19
    • 2011-12-25
    • 2017-01-11
    • 2016-02-11
    相关资源
    最近更新 更多