前回の記事(C++とSwiftの速度比較 〜ソートアルゴリズム編〜)ではC++とSwiftのソートアルゴリズムの実行速度を比較しました。今回はJavaも含めて比較するぞ!ということで取り掛かりました。
しかしいろいろあって、今回はC++とJavaのみでソートアルゴリズムの実効速度比較を行う運びとなりました。記事の最後には間接的にSwiftとJavaの比較についての考察も行っています。
概要
今回はC++とSwiftとJavaについて、比較や関数呼び出しなども含めた総合的なプログラムの実効速度を比較したいと考え、配列のソートを行うプログラムで速度の比較実験を行おうと思いました。
が、そこで大きな問題が。Javaの変数はオブジェクトへの参照値を持っています。つまりC/C++で言うポインタのようなものをデフォルトで使用しています。一方でSwiftは参照値ではない。
では比較のためにSwiftでポインタを扱おうと思ったが、これが少々面倒。そもそもSwiftでわざわざポインタを使うのもどうなの?という感じも。ではJavaで参照値ではなくオブジェクトのコピーをするように実装すればいいのでは?と思うが、やってみたらこれもなかなか面倒。
ということで、今回はC++でポインタをベースとした実装を行い、C++とJavaのみで比較を行いました。
実装した処理の流れは前回同様、
- 標準入力からテキストデータを読み込み、オブジェクトを生成し、順次配列の後ろへ追加
- オブジェクトのメンバを基準に配列の中身をソート
とし、その他の設定なども
- アルゴリズムはマージソート(関数呼び出しを増やしたいのと、実装の手間削減のため、再帰関数による実装とした)
- ソートの対象は自作のPersonクラスのオブジェクトで、id(整数型)とname(文字列型)というメンバ変数を持っており、ソートの基準はnameの昇順
- ソート対象データは全部で50万件、nameは3〜10文字のランダムなアルファベット
としました。
実験環境
以下、今回の実験環境です。全て同じハードウェア上で動かしています。
OS
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"
C++
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
(中略)
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)
コンパイルオプション
$ g++ -O2 -std=gnu++11
Java
$ java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
ソースコード
C++
ポインタの配列をソートするプログラムとしました。
#include <vector>
#include <string>
#include <iostream>
#include <ctime>
class Person {
private:
int id;
std::string name;
public:
Person( int id, const char* name ) {
this->id = id;
this->name = name;
}
Person() {
this->id = 0;
this->name = "";
}
int get_id() { return id; };
std::string get_name() { return name; };
bool operator< ( Person &p2 ) {
return this->name < p2.name;
};
Person &operator=( const Person &p ) {
this->id = p.id;
this->name = p.name;
return *this;
}
};
template<class T> class Sort {
T *buf;
std::vector<T> *array;
public:
Sort() {
};
private:
// ソート済みの2つの領域 (index_b〜index_c-1 と index_c〜index_e) をマージする関数
void merge( int index_b, int index_c, int index_e ) {
// 2つの領域が1つずつしかない場合は2つの大小関係だけ見る
if ( index_b + 1 == index_e ) {
if ( *(*array)[index_e] < *(*array)[index_b] ) {
auto tmp = (*array)[index_e];
(*array)[index_e] = (*array)[index_b];
(*array)[index_b] = tmp;
}
return;
}
// index_1とindex_2を動かしながら,小さい方を順次index_bufへ書き込む
int index_1 = index_b;
int index_2 = index_c + 1;
int index_buf = index_b;
while ( true ) {
if ( *(*array)[index_1] < *(*array)[index_2] ) {
buf[index_buf++] = (*array)[index_1];
// index_1が最後まで到達したらindex_2側を順番に見て終了
if ( index_1++ == index_c ) {
while ( index_2 <= index_e ) {
buf[index_buf++] = (*array)[index_2++];
}
break;
}
} else {
buf[index_buf++] = (*array)[index_2];
// index_2が最後まで到達したらindex_1側を順番に見て終了
if ( index_2++ == index_e ) {
while ( index_1 <= index_c ) {
buf[index_buf++] = (*array)[index_1++];
}
break;
}
}
}
// バッファから元配列へコピー
for ( int i = index_b; i <= index_e; i++ ) {
(*array)[i] = buf[i];
}
return;
};
// 再帰的に呼び出す関数
void merge_sort( int index_b, int index_e ) {
int index_c = ( index_b + index_e ) / 2;
// 左側をソート
if ( index_b != index_c )
merge_sort( index_b, index_c );
// 右側をソート
if ( (index_c + 1) != index_e )
merge_sort( index_c + 1, index_e );
// 左と右をマージ
merge( index_b, index_c, index_e );
return;
};
public:
// mainから呼ぶソート関数
// ポインタの配列を渡すことを前提としている
void merge_sort( std::vector<T> *vec ) {
// ソート対象の配列
this->array = vec;
// マージ中のソート結果を一時的に記憶するバッファ
this->buf = new T[ vec->size() ];
// マージソート
merge_sort( 0, vec->size() - 1 );
delete [] buf;
};
};
int main(int argc, char *argv[]) {
// 時間測定のための変数
struct timespec tp_start, tp_end;
// 標準入力からデータを読み込み
std::vector<Person*> vec_people;
int id;
std::string name;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp_start);
while ( std::cin >> id && std::cin >> name ) {
vec_people.push_back( new Person( id, name.c_str() ) );
}
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp_end);
std::cout << "reading : " << (double(tp_end.tv_sec - tp_start.tv_sec) + double(tp_end.tv_nsec - tp_start.tv_nsec) / 1000000000.0) << std::endl;
// 読み込んだデータをソート
Sort<Person*> sort;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp_start);
sort.merge_sort( &vec_people );
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp_end);
std::cout << "sorting : " << (double(tp_end.tv_sec - tp_start.tv_sec) + double(tp_end.tv_nsec - tp_start.tv_nsec) / 1000000000.0) << std::endl;
// ソート済み確認
for ( int i = 1; i < vec_people.size(); i++ ) {
if ( *(vec_people[i]) < *(vec_people[i-1]) ) {
std::cerr << "ERROR" << std::endl;
}
}
for ( auto p : vec_people ) {
delete p;
}
return 0;
}
Java
public class Person implements Comparable {
private int id;
private String name;
public Person( int id, String name ) {
this.id = id;
this.name = name;
}
public Person() {
this.id = 0;
this.name = "";
}
public int getId() { return id; };
public String getName() { return name; };
@Override
public int compareTo( Object obj ) {
return this.name.compareTo( ((Person)obj).name );
};
@Override
public boolean equals( Object obj ) {
if ( this.getId() == ((Person)obj).getId() &&
this.getName() == ((Person)obj).getName() ) {
return true;
} else {
return false;
}
}
};
結果
標準入力からの読み込みの部分とソーティングの部分のそれぞれの実行時間を計測しました。時間の計測には、C++ではC言語のライブラリからclock_gettime関数を、JavaではThreadMXBeanを用いました。以下の時間は5回実行したときの平均を表しています。
言語 | 読み込み(秒) | ソート(秒) |
---|---|---|
C++ | 0.370 | 0.244 |
Java | 0.663 | 0.293 |
JavaのソートはC++から見て20%弱遅いという結果となりました。配列アクセスの比較をした時とだいたい同じような傾向と言えるのではないでしょうか。
ここで、C++から見た遅さを前回の結果と合わせて比較し、間接的にJavaとSwiftを比較してみようと思います。あまり公正な比較ではないので、参考程度にお願いします。
言語 | 読み込み | ソート |
---|---|---|
Java | 179.14% | 119.96% |
Swift (-Ounchecked) | 311.08% | 1300.34% |
Swift | 527.80% | 1551.49% |
JavaとSwiftそれぞれのC++からの遅さを比べていますが、かなり違いが出ました。 言語の仕様には詳しくないため何とも言えませんが、配列アクセスの比較ではSwiftがあまりに遅いということは無かったため、Swiftは関数呼び出しやオブジェクトのコピー辺りが重いということでしょうか。 再帰ではなくループで実装したプログラムによる比較もしてみたいところです。
おわりに
今回はC++とJavaをソートアルゴリズムの観点から比較してみました。 Javaは思ったより速く、Swiftと比較したときほどの違いは出ませんでした。
また実装していて思ったのですが、Javaはすべて参照型となっているため、変数のコピーが大量に起こる場合でもオブジェクトのコピー頻度を意識したプログラミングをする必要が無いというところが非常に楽でした。 C++で同様のことをするためにはポインタを駆使する必要がありますからね。 一方でJavaはC++のように値とポインタを適宜使い分けるということがしにくいです。その辺りの動作を理解した上で使いこなすにはC++の方が自由度が高いため向いていると思います。
どちらにせよ、使う言語の特性をしっかりと把握した上でコーディングを行うことは必須と言えますね。
余談
プログラミングはその書き方によって効率も大きく変わってきます。以下はC++11/14の機能を使ってより効率的なコードを書くために良い書籍です。基本的な入門書ではなくC/C++をある程度理解していることが必須です。