| Top Page | プログラミング |


Perl, R, Ruby, C++ で作成したプログラムの実行速度の比較

from 2013-01-24
updated on 2013-09-30

はじめに

コンピュータにやって欲しい仕事を指示するため、人はプログラムを書きます。 そのための言語にはさまざまなものがあります。目的に応じて書きやすいものを使えばよいのですが、 多量の計算が必要な場合、計算のスピードも気になります。 私自身、および私の周囲に使用者がいる言語4つ(Perl, R, Ruby, C++) で、 簡単な比較(ベンチマークテスト)をしてみました。

R ではデータの処理方法によって大きく計算時間が変わるといわれていますので、 データ構造やデータの処理法間の比較もしてみました。

なお、この結果は実行するプログラムの内容によって大きく変わりますし、 処理系によっても変わります。あくまで参考程度の情報です。

言語の選び方については、このページ末尾の補足もご覧ください。

要点

最初に結果の注目ポイントを示します。

言語間の全体的な比較

「キーと値のペア」の取り扱いの比較

R のデータ構造とアクセス方法による速度の違い


プログラムと実行結果

以下は、テストにつかったプログラムと、実行時間の測定結果です。 実行時間は、もっとも速い場合に対する相対値で示しています。 計算速度に応じて繰り返し回数やデータ数を調整して実行し、 所要時間を計算量で割って標準化してから言語間の比較をしました。

要素数1万個の配列の各要素に、何回も値を足す

実行時間(C++ に対する比)
言語 変数の値をそのまま足す 変数の値の対数を足す
C++ 1.0 1.0
Perl 136 8.3
Ruby 246 38.6
R (ベクトル処理) 3.9 1.2
R (要素逐次処理) 3270 155

// C++ 版

#include <iostream.h>
#include <time.h>
#include <math.h>

void main (void) 
{
     clock_t start = clock();

    int rep = 1000000; // 足し算の回数
    int n   = 10000;   // 変数の数(配列の要素数)

    double* x = new double[n];  // 一万個の変数(配列)の初期化
    for (int i = 0; i < n; ++i) {
    	x[i] = 0;
    }

    for (int j = 0; j < rep; ++j) {     // rep 回の繰り返し
        for (int i = 0; i < n; ++i) {   // 一万個の変数それぞれに値を加える
            x[i] += 1; // 数値計算ライブラリのテストでは、x[i] += log(i + 1)
        }	
    }

    delete [] x;              // 変数のメモリを解放
    cout << clock() - start;  // 経過時間を出力
}

# Perl 版

my $start = time;

my $rep = 100000;   # 足し算の回数
my $n   = 10000;    # 変数の数(配列の要素数)

my @x;              # 配列を用意

for (my $j = 0; $j < $rep; ++$j) {    # rep 回の繰り返し
    for (my $i = 0; $i < $n; ++$i) {  # 一万個の変数それぞれに値を加える
        $x[$i] += $i;  # 数値計算ライブラリのテストでは、$x[$i] += log($i + 1)
    }
}

print time -  $start;  # 経過時間を出力

# Ruby 版

t1 = Time.now

rep = 1000  # 足し算の回数
n   = 10000 # 変数の数(配列の要素数)

x = Array.new(n, 0)

for j in 1..rep         # rep 回の繰り返し
    for i in 0..(n-1){  # 一万個の変数それぞれに値を加える
        x[i] += i       # 数値計算ライブラリのテストでは、x[i] += log(i + 1)
    end
end

puts(Time.now - t1)     # 経過時間を出力

# R版 (ベクトル処理)

start <- proc.time()

rep <- 10000   # 足し算の回数
n   <- 10000   # 変数の数(配列の要素数)

x <- rep(0, n)  # 一万個の変数(配列)の初期化

for (j in (1:rep) ) {   # rep 回の繰り返し
    # 一万個の変数に、一度に値を加える
    x <- x + (1:n)   # 数値計算ライブラリのテストでは、x <- x + log(1:n)
}

print(proc.time() - start)

# R版 (要素逐次処理)

start <- proc.time()

rep <- 10000   # 足し算の回数
n   <- 10000   # 変数の数(配列の要素数)

x <- rep(0, n)  # 一万個の変数(配列)の初期化

for (j in (1:rep) ) {    # rep 回の繰り返し
    for (i in (1:n)) {   # 一万個の変数それぞれに値を加える
        x[i] <- x[i] + i   # 数値計算ライブラリのテストでは、x[i] <- x[i] + log(i + 1)
    }
}

print(proc.time() - start)     # 経過時間を出力

キーと値のペア1万個を記憶し、キーを指定して値を取り出し合計する

言語 C++ に対する 比
C++ 1.0
Perl 3.1
Ruby 3.5
R (データフレーム) 182
R (名前付きリスト) 1590
R (hash ライブラリ) 320

※以下のコードでは、計時部分は省略しています。

// C++ 版

#include <iostream.h>
#include <time.h>
#include <map.h>

typedef map<int, int> my_map;
typedef my_map::iterator my_itr;

void main (void) 
{
    int rep = 10000;  // 繰り返し回数
    int n   = 10000;  // 「キーと値」のペアの数

    for (int j = 0; j < rep; ++j) {  // 繰り返し
        my_map x;
        for (int i = 0; i < n; ++i) {  // マップにキーと値のペアをしまう
            x[i] = i * 10;
        }

        int sum = 0;
        for (my_itr itr = x.begin() ;itr != x.end(); ++itr) {  // マップの全要素にアクセスする
            sum += (*itr).second;
        }
    }
}

# Perl 版

my $rep = 10000;  # 繰り返し回数
my $n   = 10000;  # 「キーと値」のペアの数

for (my $j = 0; $j < $rep; ++$j) {  #  繰り返し
    my @hash = ();;
    for (my $i = 0; $i < $n; ++$i) {  # ハッシュにキーと値のペアをしまう
        $hash{$i} = $i * 10;
    }

    my $sum = 0;
    foreach my $key (keys %hash) {  # ハッシュの全要素にアクセスする。
        $sum += $hash{$key};
    }
}

# Ruby 版

rep = 10000  # 繰り返し回数
n   = 10000  # 「キーと値」のペアの数

for j in 1..rep  #  繰り返し
    x = Hash.new
    for i in 0..(n-1)  #  ハッシュにキーと値のペアをしまう
        x[i] = 10 * i
    end

    sum = 0
    x.each_key do |key|  # ハッシュの全要素にアクセスする。
        sum += x[key]
    end
end

# R, データフレーム版  

rep <- 100;      # 繰り返し回数
n   <- 10000;    # 「キーと値」のペアの数

for (j in (1:rep)) { #  繰り返し
    d <- data.frame();
    for (i in (1:n)) {  # データフレームに行を足していく
        d <- rbind(i, i * 10)
    }
    sum <- 0;
    for (key in d[[1]]) { # 一列目の値をキーにして、全データにアクセス
        dd <- d[d[[1]] == key, ]
        sum  <- sum +  dd[[2]]
    }
}

# R, 名前付きリスト版  

rep <- 10;       # 繰り返し回数
n   <- 10000;    # 「キーと値」のペアの数

for (j in (1:rep)) { #  繰り返し
    d <- list()
    for (i in (1:n)) {   #  リストに名前付きの要素を加えていく
        d[[as.character(i)]] <- i * 10
    }

    sum <- 0
    for (key in names(d)) { # 名前をキーとして全要素にアクセス
        sum <- sum + d[[key]]
    }
}

# R, hash ライブラリ版  

library(hash)

rep <- 10;       # 繰り返し回数
n   <- 10000;    # 「キーと値」のペアの数

for (j in (1:rep)) {
    d <- hash()
    for (i in (1:n)) { #  ハッシュにキーと値のペアを加えていく
        d[[as.character(i)]] <- i * 10
    }

    sum <- 0
    for (key in keys(d)) {  # ハッシュの全要素にアクセスする。
        sum <- sum + d[[key]]
    }
}

R で、大きな行列(1000行)の、列ごとの合計値を計算する

データの処理方法 行列+apply版 に対する 比
行列を apply で処理 1.0
データフレームを lapply で処理 1.0
行列を逐次要素アクセスで処理 26.5
データフレームを逐次要素アクセスで処理 904

※以下のコードでは、計時部分は省略しています。

# 行列 + apply 版

nc <- 10000;  # 列数
nr <- 10000;  # 行数

m <- matrix(1, nr, nc)  # 行列

s <- apply(m, 2, sum)  # 各列について、sum を実行

# 行列 + for ループ 版

nc <- 10000;  # 列数
nr <-  1000;  # 行数

m <- matrix(1, nr, nc)  # 行列

for (i in 1:nc) {        # 各列について
    s <- 0
    for (j in 1:nr) {    # 各行について
        s = s + m[j, i]  # 各要素にアクセス
    }
}

# データフレーム + lapply 版

nc <- 10000;  # 列数
nr <- 10000;  # 行数

m <- data.frame( matrix(1, nr, nc) )  # データフレーム

s <- lapply(m,  sum)  # 各列について、sum を実行

# データフレーム + for ループ 版

nc <- 10000;  # 列数
nr <- 10;     # 行数

m <- data.frame( matrix(1, nr, nc) )  # データフレーム

for (i in 1:nc) {        # 各列について
    s <- 0
    for (j in 1:nr) {    # 各行について
        s = s + m[j, i]  # 各要素にアクセス
    }
}


実験の環境・処理系


(やや長い)補足

言語を選ぶ基準は、速度だけではありません。 自分がやりたいことを助けてくれるライブラリ(人が書いてくれた部品)があるか、 自分がやりたいことを表現しやすい文法構造か、 開発がしやすいか(試行錯誤が簡単にできるか)、 前処理、後処理との連携はしやすいか などを総合的に考えて選びます。 たとえば、統計解析ライブラリや描画ライブラリが充実していることは、R の選択理由となります。

また、これ以上速くする必要がない状況で、いたずらに速度にこだわるのには意味がありません。 一日の実験データを処理する計算の所要時間を10秒から 0.1 秒に短縮できても、ほとんど得るものはありません。 また、1時間かかる計算でも、一回やれば終わりなら、一時間かけて計算すればよいだけのことです。 1時間を1分に短縮するためだけに、新しい言語を勉強する必要はないでしょう。

一方、パラメータ設定をいろいろ変えてシミュレーション計算をしたい場合、一回の設定で1時間かかるのか 1分で済むのかによって、どのぐらいの設定を試せるかが変わってきます。 また、コンピュータを対話形式で使うような場合(たとえば交通機関の予約システム)には 、反応時間が5秒か2秒かはユーザにとって大きな違いです。 速度アップの意味を十分に考えて、あまり得るのものない高速化のためにコストを投じないように注意します。

もうひとつ重要なポイントは、プログラムのコード自体の効率です。 同じ結果を出すのに、非効率的なアルゴリズムだと簡単に100倍も1,000倍の遅くなります。 結果を記憶しておいて使い回せばよいはずの計算を、何万回も繰り返してしまうようなコードはその典型です。 理詰めでプログラムの論理構造を練ることが大切です。 ただし、あとで見ても理解できないようなトリッキーな高速化をすると、 トータルではデメリットのほうが大きくなるので注意が必要です。

なお、複数の言語を勉強することには大きなメリットがあります。その理由は2つあります。 ひとつめは、言語それぞれに得意なこと、不得意なことがあることです。 複数の言語を知っていれば、目的によって適した言語を使い分けることができます。 たとえば、高速演算が必要なシミュレーションプログラムは C++ で作る、柔軟なデータ処理が必要な仕事では Perl を使う、 大規模なシミュレーションプログラムを、最初は Ruby で試行錯誤しながら作ってから C++ に移植する、統計解析やグラフ化は R に任せる、といった使い分けが考えられます。

もうひとつの理由は、複数の言語を勉強するとプログラミングの本質に近づけることです。 たとえば、上の2番めの例では、キーと値のペアという形でデータを管理するという共通の考え方を、 言語ごとに違ったかたちで表現しています。 同じアイデアの別の表現を知ることで、 個々の言語の文法と、その文法で表現されるデータ構造や処理の流れ・構造化の方法などを区別して理解を深めることができます。


| Top Page | プログラミング |