メモリ割り当て
C++プログラマがよく直面する議論の1つにメモリ管理問題があります。Javaはこの問題を引き受けてくれますが、C++のメモリ管理はコンストラクタとデストラクタ、および取り扱いの難しいnew/deleteペアに依存しています。
メモリ割り当てのパフォーマンスを比較するため、まず単純なタスクとして、1,000バイトの配列を繰り返し割り当て、割り当てと割り当て解除に要する時間を測定することにします。これはJavaでは注意を要するタスクです。なぜなら、メモリを解放するための確実な方法がないからです(System.gcは「必要なら一部のメモリを解放してもいいよ」とJVMに伝える提案にすぎず、実際に解放される保証はありません)。それでも、比較の際にはこの測定値を使用するしかないので、Javaでの測定値にはメモリ解放時間が含まれない可能性があることを踏まえたうえで使用してください(結局のところ、前にも述べたとおり、C++プログラマとJavaプログラマが対立するときの主な論点の1つは、Javaではメモリ管理のことを考えなくて済むのに対し、C++ではメモリ管理に気を配る必要があるということです)。
ここでは、整数ではなくバイトの配列を扱うことに注意してください。なぜなら、Javaの整数とC++の整数はサイズが異なるので、種類の違う配列を割り当てるのでは公正でないからです。
そのため、Javaで割り当てを行う関数は次のようになります(IntAlloc.java)。
private void javaAlloc()
{
System.gc();
elements = new byte[N_ELEMS];
elements[0] = 0;
}
ガーベージコレクションを提案してからメモリを割り当てていることに注意してください。実際にアクセスされるまでメモリを割り当てないという「賢い」処置をコンパイラにとらせないために、配列の最初の要素を設定することで強制的にメモリ割り当てを行っています。
このテストでは時間測定をナノ秒単位で行います。ミリ秒単位では十分でない可能性があるからです。Javaでは、これにSystem.nanoTime関数を使用します。なお、言うまでもないことでしょうが、1ミリ秒=1,000,000ナノ秒です。
上記のコードを実行した平均時間は次のようになります。
Java memory allocation took 11,755,693
つまり、10,000バイトを割り当てるのに約11ミリ秒を要したことになります。上記のコードでガーベージコレクションの提案を行わないと(javaAlloc関数内のSystem.gc行をコメント化すると)どうなるか試してみましょう。
Java memory allocation took 12,994
以上の結果は、System.gc()でガーベージコレクションを提案したために、ガーベージコレクションが作動して、使用されていたメモリを再収集したことを示しています。また、ガーベージコレクションのことを気にしなければ、10,000バイトを割り当てるのに1ミリ秒もかからないことを示しています(もっと正確に言えば、約13,000ナノ秒で、1バイトあたりの平均が約1.3ナノ秒)。
次にC++で試してみましょう(ダウンロードファイル内のIntAllocプロジェクトに入っています)。
C allocation took 3661.273463
この結果に驚く人もいるかもしれませんが、実はC++でのメモリ割り当てはJavaのものに匹敵するのです。これらの測定値から、どちらが速いのか判断するのは難しいでしょう。いずれも1ミリ秒未満だからです。ただし、それほど大きな違いはありません。なお、繰り返しになりますが、ここに示した結果も最適化されたC++コードのものです。
System.gcを普段使っているかどうかをJavaプログラマに尋ねてみれば(リアルタイムシステムのプログラミングをしている人は別として)、実際にSystem.gcを使っているプログラマは非常に少ないことが分かるでしょう。大多数のJavaプログラマはメモリ管理をJVMに任せているのです。というのも、JVMのメモリ管理はうまく実装されていて、自ら作動すべきタイミングを心得ているので、システムパフォーマンスにそれほど大きな影響を与えないからです。
なお、本稿のコードをじっくり読み込み、少し変更を加えてみようと考える読者のためにあらかじめ言っておくと、割り当てをintに変更しても、JavaでもC++でもそれほどパフォーマンスが落ちることはありません。
ここまで基本データ型に関して両方の言語の動作を見てきました。一歩進んで、オブジェクトに関してはどんな結果になるか見てみましょう。そのため、複素数データ型をマップするクラスを使って調べることにします。複素数データ型は、実数部と虚数部という2つの部分からなるデータ型で、実数部も虚数部も浮動小数点数です。このクラスを両方の言語で実装し、それを何回かインスタンス化して、何が分かるか調べてみましょう。
Javaでは、データを格納し、それをゲッターとセッターによって公開する2つのプライベートメンバーを使用します。また、デフォルトコンストラクタの上に、2つの値を受け取って、これらのメンバーを初期化するコンストラクタを記述します(Complex.java)。
public class Complex
{
private double real;
private double imaginary;
public Complex()
{
this( 0.0, 0.0 );
}
public Complex( double real, double imaginary )
{
this.real = real;
this.imaginary = imaginary;
}
public double getReal()
{
return real;
}
public void setReal(double real)
{
this.real = real;
}
public double getImaginary()
{
return imaginary;
}
public void setImaginary(double imaginary)
{
this.imaginary = imaginary;
}
}
同様に、C++では次のように実装します。
class Complex
{
public:
Complex(double real = 0, double imaginary = 0)
{
this->real = real;
this->imaginary = imaginary;
}
double getReal()
{
return real;
}
void setReal( double real )
{
this->real = real;
}
double getImaginary()
{
return imaginary;
}
void setImaginary( double imaginary )
{
this->imaginary = imaginary;
}
private:
double real;
double imaginary;
};
ここで使用しているコードは非常に単純です(このコードはダウンロードファイル内のComplexCreateプロジェクトおよびComplexCreate.javaに入っています)。ただし、大きな違いがあります。Javaのインスタンス化プロセスは、次のような2つのステップになっています。
- 実際の配列のメモリを割り当てる
- 各オブジェクトを作成する
それに対し、C++ではこれを1つのステップで行えます。
arr = new Complex[N_GENERATED];
new演算子を使用すると、実は1つのステップでアイテムが作成されて初期化されます。C++コードとJavaコードを実行すると、次の結果が得られます。
(Java) Java create took 710788 (C++) C instantiation took 29348.262052
この結果もナノ秒単位です。およそ710,000(Java)対29,000(C++)という比較です。この意味に関してはあえて触れません。
条件付きでC++の勝ち
プログラミング言語で扱うのは、もちろんメモリ割り当て、ループ処理、浮動小数点演算だけではありませんが、これらをまったく使用しないプログラムを見つけるのは難しいでしょう。これらの処理を行うなら、どちらが好きかという話は別にして、最適化されたC++コンパイラの方がJavaよりも速いコードを生成できます。もっとも、Javaコンパイル自体に(筆者は特別なフラグなしで標準JDKコンパイラを使用しました)、本稿で述べてきたようなケースで実行速度を向上させる最適化機能がもっと必要なのかという疑問もあります。
