为什么Clojure的频率并不比Python的集合快.Counter?

时间:2015-05-10 16:43:52

标签: python performance clojure

注意 - 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不使用类型提示?
  • 如何进一步优化性能?我可以为整数的哈希映射值添加类型提示吗?

2 个答案:

答案 0 :(得分:4)

对JVM上的任何内容进行基准测试是一项棘手的工作。 JVM会在运行时优化代码,但要预测何时发生或控制代码并不容易。要获得除两个函数(两个Clojure)之间最常见的性能提示之外的任何内容,您需要使用专用的基准测试库。 Criterium是Clojure社区中最常用的库。

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

Clojure版本没有类型提示,原因有几个。

  1. 频率是一种通用功能,因此可以处理任何类型的值。
  2. 类型提示仅与Java 的互操作性能有关。来自Clojure Programming,第367页

      

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

  3. 如果您在函数中专门使用Java 基元,则可以使用类型声明来优化它。同样来自Clojure Programming,第438页

      

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

         

    所有参数和返回值都是(未修饰的)函数边界的对象。这些调用方法都接受参数并返回根类型java.lang.Object的值。这使得Clojure的动态类型默认值(即,您的函数的实现确定了可接受的参数类型的范围,而不是由语言强制执行的静态类型声明),但是具有强制JVM将任何作为参数传递的基元封装的副作用。或作为这些功能的结果返回。因此,如果我们使用原始参数调用Clojure函数 - 例如long - 该参数将被装箱到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。

答案 1 :(得分:1)

使用

(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"