こちらも参考になります: https://gist.github.com/athos/8081180
これらの文章でGrenchmanというツールを知りました。ありがとうございます。
ただ、このベンチマークにはいくつか気になる点があったので、それらの点について述べます。すでに指摘されていたらごめんなさい。
(range 1000000)は[0, 1000000)、xrange(1, 1000000)は[1, 1000000)の範囲で整数を生成します。
最初の記事で、Clojureは5回、Pythonは1回だけコードを実行しています。しかし、Pythonもヒープ領域の拡張などの影響で初回と2回目以降で実行速度が異なることがありえます。また、ベンチマークで複数回測定することや言語間の比較で測定回数を揃えることは悪いことではありません。実行時間にばらつきが出るのは避けがたいことだからです。
そこで私は以下のようにループするスクリプトを書いて実行しました。1
import timeit
def f(x):
return reduce(lambda x, y: x + y, map(lambda x, y: x + y, xrange(x), xrange(x)))
for i in range(5):
r = timeit.timeit("f(1000000)", "from __main__ import f", number = 1)
print(r*1000)
私のマシンでは次のような結果が得られました。
$ python --version
Python 2.7.5
$ python test.py
381.520986557
351.56416893
347.455024719
347.482919693
347.192049026
これは初回とそれ以降の差がはっきりと出過ぎているとは思います。実際、何度か試行すると数回に一度は初回以外が一番長くかかった場合を見かけました。しかし、いずれにしても実行時間にばらつきが出ていました。
また、次の記事ではJVMの設定によって実行時間が変化することを示していますが、前回と違って一度だけ実行しています。そのため、先ほどと同様の理由で前回の結果との比較に少々難があります。
そこで、私は通常のLeiningenと以下のパッチを当てたLeiningenを使ってGrenchmanで測定しました。
--- /home/omasanori/bin/lein 2013-12-19 11:26:05.661993152 +0900
+++ /home/omasanori/bin/lein-server 2013-12-22 16:40:52.282233574 +0900
@@ -129,7 +129,7 @@
BIN_DIR="$(dirname "$SCRIPT")"
-export LEIN_JVM_OPTS="${LEIN_JVM_OPTS-"-XX:+TieredCompilation -XX:TieredStopAtLevel=1"}"
+export LEIN_JVM_OPTS
# This needs to be defined before we call HTTP_CLIENT below
if [ "$HTTP_CLIENT" = "" ]; then
@@ -312,7 +312,7 @@
exec sh -c "exec $(cat $TRAMPOLINE_FILE)"
else
export TRAMPOLINE_FILE
- "$LEIN_JAVA_CMD" -client \
+ "$LEIN_JAVA_CMD" -server \
"${BOOTCLASSPATH[@]}" \
$LEIN_JVM_OPTS \
-Dfile.encoding=UTF-8 \
結果は以下の通りです。
$ java -version
java version "1.7.0_45"
OpenJDK Runtime Environment (IcedTea 2.4.3) (Gentoo build 1.7.0_45-b31)
OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode)
# 別の端末で lein repl :headless :port 60000 する
$ grench eval '(clojure-version)'
"1.5.1"
$ GRENCH_PORT=60000 grench eval '(dotimes [_ 5] (time (reduce unchecked-add (map unchecked-add (range 1000000) (range 1000000)))))'
"Elapsed time: 659.042892 msecs"
"Elapsed time: 615.617369 msecs"
"Elapsed time: 610.588024 msecs"
"Elapsed time: 610.586214 msecs"
"Elapsed time: 611.568759 msecs"
# 先ほどのサーバを終了させ、今度は lein-server repl :headless :port 60000 する
$ grench eval '(clojure-version)'
"1.5.1"
$ GRENCH_PORT=60000 grench eval '(dotimes [_ 5] (time (reduce unchecked-add (map unchecked-add (range 1000000) (range 1000000)))))'
"Elapsed time: 289.130653 msecs"
"Elapsed time: 281.813446 msecs"
"Elapsed time: 244.454376 msecs"
"Elapsed time: 243.675864 msecs"
"Elapsed time: 240.42589 msecs"
私の記憶が正しければ、Linux amd64ではClient VMが廃止されています。そうであるならば、この差はHotSpotの設定が影響した結果でしょう。
結論は元の記事とあまり変わりませんが、測定条件が揃っていないのが気になりました。
最初の記事には「よく unchecked-xxx 使えみたいな話がありますが、使ってもなお遅いということを覚えておいた方がよさそうですね」と書かれています。しかし、無検査演算を使わなかった場合との比較はありません。今回のベンチマークで通常の演算と無検査演算の差は出るのでしょうか。私はその点を検証しました。nREPLサーバの設定は上で使ったlein-serverのものです。
$ GRENCH_PORT=60000 grench eval '(dotimes [_ 5] (time (reduce + (map + (range 1000000) (range 1000000)))))'
"Elapsed time: 279.154024 msecs"
"Elapsed time: 270.455675 msecs"
"Elapsed time: 238.784275 msecs"
"Elapsed time: 238.033093 msecs"
"Elapsed time: 238.294541 msecs"
先ほどの結果との差は小さく、今回のベンチマークで無検査演算を使うことで何かしらの効果があるかどうかは疑わしいと私は考えます。
ついでに、自動拡張付き演算子でも測定しました。
$ GRENCH_PORT=60000 grench eval "(dotimes [_ 5] (time (reduce +' (map +' (range 1000000) (range 1000000)))))"
"Elapsed time: 268.880845 msecs"
"Elapsed time: 241.360002 msecs"
"Elapsed time: 241.052481 msecs"
"Elapsed time: 240.855405 msecs"
"Elapsed time: 241.345931 msecs"
これも大きな差は出ませんでした。
結びとして、『プログラミングClojure 第2版』から無検査演算を提供する理由を説明している部分を引用します。
ではなぜClojureは無検査演算を使えるようにしているんだろうか。2つ理由がある。
- Javaのセマンティクスが必要になるときがある。無検査演算の主な用途は、このふるまいを期待しているライブラリとやりとりしなければならない場合だ。
- 実際にやってみないと、誰も無検査演算が大して速くないということが分からない(あるいは、信じない)。
- 比較対象と測定条件をそろえましょう。
- 「無検査演算は速い」が常に成り立つとは限りません。測定してから採用するかどうか決めましょう。
以下は余談です。今回のベンチマークに対するインチキの類が含まれます。重要な話題は既に述べたので読まなくても構いません。
処理の内容を展開し、loopで書き直すと以下のようになります。
(loop [acc 0, i 0]
(if (< i 1000000)
(recur (+ acc (+ i i)) (inc i))
acc))
結果を以下に示します。
$ GRENCH_PORT=60000 grench eval '(dotimes [_ 5] (time (loop [acc 0 i 0] (if (< i 1000000) (recur (+ acc (+ i i)) (inc i)) acc))))'
"Elapsed time: 7.548957 msecs"
"Elapsed time: 2.437493 msecs"
"Elapsed time: 2.240476 msecs"
"Elapsed time: 2.252866 msecs"
"Elapsed time: 2.250039 msecs"
上のコードではrangeによる遅延シーケンスの生成を回避しています。更に、上のコードではaccとiに型ヒントを付けなくともプリミティブ型のlongをboxingせずに使います。2
参考のため、無検査演算を使ったバージョンも計測します。
$ GRENCH_PORT=60000 grench eval '(dotimes [_ 5] (time (loop [acc 0 i 0] (if (< i 1000000) (recur (unchecked-add acc (unchecked-add i i)) (unchecked-inc i)) acc))))'
"Elapsed time: 2.990067 msecs"
"Elapsed time: 5.164559 msecs"
"Elapsed time: 1.086252 msecs"
"Elapsed time: 1.090639 msecs"
"Elapsed time: 1.085242 msecs"
確かに高速化していますが、プリミティブ型とloopを使うことで得られた向上分に比べるとわずかです。安全性と引き換えにする価値があるかどうかはそのコードの性能がどれほど全体にとって重要かどうかによりますが、そのような価値が認められる状況はあまり頻繁には起きないでしょう。
プリミティブ型の使用はともかく、遅延シーケンスの生成を回避するのは今回のベンチマークではインチキです。ただ、実際のコードでは必要な場面もあるかもしれませんから、参考のために示しました。3
『LET OVER LAMBDA』より引用。
ベンチマークがよく議論されるのは、特にプログラマにとって、これがとてつもなく楽しい、馬鹿馬鹿しくも底なしの道楽だからである。 あいにく、ほとんどのベンチマーク結果は使い物にならない。本書が提示するベンチマーク結果ですら、大いに眉につばを付けるべきだと忠告しておく。 とは言え、注意深く組み立てられ制御された環境において、同じマシン上の同じLispイメージの上で、微妙に違った版のコードを実行し測定すれば、性能上のボトルネックを理解して修正する役に立つことはあるかもしれない。
私からは以上です。