python的reduce实现会产生什么开销?



我有一些 python 脚本花费的时间比我预期的要长,所以我开始调查并发现 python 的性能有一些惊喜。大多数情况下,它似乎围绕着reduce,但我不明白为什么。

为了进行实验,我编写了以下两个模块:

py.py

from functools import reduce
def mysum(n):
    return reduce(lambda acc, x: acc + x, range(n + 1))
n = int(1e8)
print(mysum(n))

clj.clj

(defn mysum [n]
  (reduce + (range (inc n))))
(println (mysum 1e8))

我用time比较了他们的表现:

➜  ~ time python py.py
5000000050000000
python py.py  21.90s user 0.41s system 95% cpu 23.344 total
➜  ~ time lumo clj.clj
5000000050000000
lumo clj.clj  2.44s user 0.13s system 102% cpu 2.519 total

看起来python的性能比clojure实现慢10倍以上。但这与我的预期相反。

即使使用 JVM 运行 clj 文件,这会产生巨大的启动成本,python 也被击败了一英里:

➜  ~ time clj clj.clj
5000000050000000
clj clj.clj  6.01s user 0.72s system 153% cpu 4.394 total

为什么python的reduce在这里这么慢?我做错了什么吗?

你的python代码正在被解释,而Clojure代码是由HotSpot编译器在JVM上编译的。 这是在JVM上的一大优势,也是Python和Ruby分别在JVM上移植Jython和JRuby的原因。

在原生 Java 中,简单求和甚至更快:以下是一些快速比较:

class Calc {
  public static long cumsum( long limit ) {
    long result = 0;
    for (long i=0; i<limit; i++) {
      result += i;
    }
    return result;
  }
(let [limit 1e8]
  (newline) (println :result-clj)
  (crit/quick-bench (reduce + (range limit)))
(newline) (println :result-java-cumsum)
(crit/quick-bench (Calc/cumsum limit)))
:result-clj             Execution time mean : 1777.600 ms
:result-java-cumsum     Execution time mean :   26.920399 ms

是的,这是 66 倍的加速。 尝试将计数减少到 1e6

:result-clj             Execution time mean : 17572.885 µs
:result-java-cumsum     Execution time mean :   257.092 µs

另一个技巧是热点编译器通常可以识别 1..N 中的和,并在代数公式中代入

sum(1..N) => N*(N+1)/2

根本没有循环!

我记得,range会生成一个列表,而xrange会产生可迭代的。尽管如此,我还是试图替换它,但它没有帮助。所以是的,这里的关键问题似乎与口译员的性质有关。顺便说一句,这个工作得很快——总和(xrange(100000001((

最新更新