ゴミ箱の中のメモ帳

まだ見ぬ息子たちへ綴る手記

各プログラミング言語のベンチマークとその無意味さ

世間ではよく各プログラミング言語の優位性が語られているようだが、私はそのような会話はアホの副産物だと感じている。

Rubyだろうが、PHPだろうが、Perlだろうが、Pythonだろうが、それらプログラミング言語の速度差を比較するなんてほとんど無意味。確かに言語間でそこそこの速度差があるのは事実だが、結局ある程度の規模のコードを書くのであればそのコードの書き方で速度は決まる。

例えば、標準出力を5000億回繰り返すようなプログラムであれば速度差は重要になるので早い言語を選んだほうがいいが、そんなコードでなければ、各々が書きやすいと思える言語を使うのが最速になる。

例えばC言語ならスクリプト言語に比べれば比べ物にならないほど高速なコードを書くことができるが、C言語は書き方における速度差は顕著であるし、慣れないプログラマが書くと高次元でなく、確保した以上の配列に書き込むような低次元なミスなどが多発し使い物にならないコードが出来てしまう。なので、C言語で書くよりも、自分が慣れている言語で書くことが確実で高速なメンテナンスの良いコードが書ける。


最近の言語であればライブラリなんかもほぼ同等に十分に揃っているし、言語間における開発環境の優位性など殆ど無い。それらが顕著にあり、言語間で速度差や開発効率が大きく変わるのであれば、世間にはこれほど多種多様な環境があるわけがない。


先の記事に「gccとclangとicc」として、コンパイラの性能について書いてみたが、どうせならと各スクリプト言語でも実験してみた。今回も各種言語のサンプルコードはフィボナッチで各種言語をベンチマークからお借りしている。


実行した各サンプルコードはこの通り。

Python 2.7.6

def fib(n):
    if n < 2: return n
    return fib(n - 2) + fib(n - 1)

print fib(38)

Python 3.4.0

def fib(n):
    if n < 2: return n
    return fib(n - 2) + fib(n - 1)

print(fib(38))

Ruby 1.9.3p484

def fib(n)
    return n if (n < 2)
    return fib(n - 2) + fib(n - 1)
end

puts fib(38)

Ruby 2.0.0p384

def fib(n)
    return n if (n < 2)
    return fib(n - 2) + fib(n - 1)
end

puts fib(38)

Perl 5.18.2

sub fib($) {
    return $_[0] if ($_[0] < 2);
    return fib($_[0] - 2) + fib($_[0] - 1);
}

print fib(38), "\n";

PHP 5.5.9

<?php
function fib($n) {
    if ($n < 2) return $n;
    return fib($n - 2) + fib($n - 1);
}

print fib(38);
print "\n";

C (GCC 4.8.2 -Ofast)

#include <stdio.h>

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 2) + fib(n - 1);
}

int main(int argc, char *argv[]) {
    printf("%d\n", fib(38));
    return 0;
}

Java 1.8.0

public class fib {
    static int fib(int n) {
        if (n < 2) return n;
        return fib(n - 2) + fib(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(fib(38));
    }
}

Gauche 0.9.3.3

(define (fib n)
  (cond ((= n 1) 1)
     ((= n 2) 1)
     (else (+ (fib (- n 1)) (fib (- n 2))))))

(print (fib 38))

実行結果

言語 3回の平均(秒)
Python 2.7.6 11.13
Python 3.4.0 14.30
Ruby 1.9.3p484 7.20
Ruby 2.0.0p484 5.76
Perl 5.18.2 25.71
PHP 5.5.9 15.06
GCC 4.8.2 0.13
Java 1.8.0 0.21
Gauche 0.9.3.3 3.38

f:id:mon0:20140704053156p:plain


これはRubyユーザが発狂してPythonPHPPerlが遅くて使い物にならないと騒いで回りそうだが、まさにこれは先に書いた通りの現象になる。今回のコードはGauche以外は先のページから取得してきたものをそのまま利用したが、そのページは平均的に再起を使い同じ処理を書こうとしているに過ぎない。

再起が早い言語もあれば、再起がオーバーヘッドになる言語(環境)もある。例えば、この結果だけを見るとC言語Perlでは約200倍の速度差があるし、速度だけを考えるとC言語JavaGaucheを使えとなってしまう。スクリプト言語ユーザはそれを納得しないであろうし、そこから、プログラミング言語は言語の速度だけではないということは理解しているかと思う。

言語によって早い書き方と、遅い書き方がある。それを理解している言語を使うことが最も大事だ。例えばPythonではこのサンプルコードのようなコードを書いてしまうと非常に遅い。


以下でPythonについて少し言及してみる。

例えばこのPython2.7のコードを再起を使ったまま展開してみると以下のようなものだ。

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

print fib(38)

これを実行してみると平均で13.58秒となった。上のサンプルコードよりも遅い。理由は単純で、条件分岐が先のコードよりも多く、さらに、条件分岐の果てに目的のコードが存在しているから毎回条件分岐を通り遅くなってしまう。

こんな単純なことでも2秒以上の差がでてしまう。コードの読みやすさではどちらもほとんど変わらないが、単純な条件分岐でも呼び出される回数によってこの差が出る。


次に、再起を使わずにフィボナッチ数を求めるコードを考えてみる。世間では再起が無差別に早いと思われがちだがそんなことはない。

def fib(n):
    return reduce(lambda x,y: x+[x[-1]+x[-2],], xrange(n-1), [0,1])[n]

print fib(38)

これを実行すると0.01秒だ。timeコマンドの分解能がそれ以下は無いため実際はそれ以下かと思う。先の結果からC言語でも0.13秒かかっているため、このコードはC言語よりも高速な結果となっている。書き方次第で、C言語よりも高速に書ける(C言語も書き方次第だが)。

これによって不慣れな言語を使うよりも、自分が慣れている言語を使うほうが高速に書けることがわかった。

このコードを見て「アホかよ」と思う方もいるかとおもうが、別にわかりづらい書き方ではない。だがシンプルに書いてもいい。

def fib(n):
    nums = []
    a, b = 1, 1
    while True:
        a, b = b, a+b
        nums.append(a)

        if len(nums) == n-1:
            break

    return nums[-1]

print fib(38)

これを実行しても0.01秒だ。先の再起を使うよりも(私には)わかりやすく、高速に動作する。



このように、実際にプログラミング言語ベンチマークなどほとんどあてになるものではなく、それぞれの言語で、それぞれの言語にあった書き方をすればメンテナンス性もよく、高速に動作することがわかってもらえたかと思う。そして何より、「この言語のほうが早い」という勘違いをせずに、自分で慣れた環境を使ってコードを書けることは幸せで、さらにその幸せから苦を感じずにプログラミングを行える。

これが大切なことかと思っている。

ベンチマーク比較テスト

ベンチマーク比較テスト

ベンチマーク 新聞レンズ

ベンチマーク 新聞レンズ