C++ プログラミングガイド

第 8 章 パフォーマンス

C++ 関数のパフォーマンスを高めるには、コンパイラが C++ 関数を最適化しやすいように関数を記述することが必要です。言語一般、特に C++ のソフトウェアパフォーマンスについて関連する書籍は多数あります (『C++ プログラム書法』Tom Cargill 著、ソフトバンク、1994 年発行、『Writting Efficient Programs』Jon Louis Bentley 著、Prentice-Hall 社、1982 年発行、『Effective C++』Scott Meyers 著、Addison-Wesley 社、1992 年発行などを参照)。この章では、これらの書籍にある内容を繰り返すのではなく、Sun C++ コンパイラにとって特に有効なパフォーマンス向上の手法について説明します。

一時オブジェクトの回避

C++ 関数は、暗黙的に一時オブジェクトを多数生成することがよくあります。これらのオブジェクトは、生成後破壊する必要があります。この生成と破壊のために、少なからぬクラスに手間をかけなければなりません。Sun C++ コンパイラは一時オブジェクトの一部を削除しますが、すべてを削除できるとはかぎりません。

プログラムの明瞭さを保ちつつ、一時オブジェクトの数が最小になるように関数を記述してください。このための手法としては、暗黙の一時オブジェクトに代わって明示的な変数を使用すること、値パラメータに代わって参照パラメータを使用することなどがあります。また、+= だけを実装して使用するのではなく、+= のような演算を実装および使用することもよい手法です。たとえば、次の例の最初の行は、a + b の結果に一時オブジェクトを使用していますが、2 行目は一時オブジェクトを使用していません。


T x = a + b;
T x( a ); x += b;

インライン関数の使用

小さくて実行速度の速い関数を呼び出す場合は、通常どおりに呼び出すよりもインライン展開する方が効率が上がります。逆に言えば、大きいか実行速度の遅い関数を呼び出す場合は、分岐するよりもインライン展開する方が効率が悪くなります。また、インライン関数の呼び出しはすべて、関数定義が変更されるたびに再コンパイルする必要があります。このため、インライン関数を使用するかどうかは十分な検討が必要です。

関数定義を変更する可能性があり、呼び出し元をすべて再コンパイルするには手間がかかると予測される場合は、インライン関数は使用しないでください。そうでない場合は、関数をインライン展開するコードが関数を呼び出すコードよりも小さいか、あるいはアプリケーションの動作がインライン関数によって大幅に高速化される場合にのみ使用してください。

コンパイラは、すべての関数呼び出しをインライン展開できるわけではありません。そのため、関数のインライン展開の効率を最高にするにはソースを変更しなければならない場合があります。どのような場合に関数がインライン展開されないかを知るには、+w オプションを使用してください。次のような状況では、コンパイラは関数をインライン展開しません。

デフォルト演算子の使用

クラス定義がパラメータのないコンストラクタ、コピーコンストラクタ、コピー代入演算子、またはデストラクタを宣言しない場合、コンパイラがそれらを暗黙的に宣言します。こうして宣言されたものはデフォルト演算子と呼ばれます。C のような構造体は、デフォルト演算子を持っています。デフォルト演算子は、優れたコードを生成するためにどのような作業が必要かを把握しています。この結果作成されるコードは、ユーザーが作成したコードよりもはるかに高速です。これは、プログラマが通常使用できないアセンブリレベルの機能をコンパイラが利用できるためです。そのため、デフォルト演算子が必要な作業をこなしてくれる場合は、プログラムでこれらの演算子をユーザー定義によって宣言する必要はありません。

デフォルト演算子はインライン関数であるため、インライン関数が適切でない場合にはデフォルト演算子を使用しないでください (前の節を参照)。デフォルト演算子は、仮想 (virtual) にはなり得ません。デフォルト演算子は、次のような場合に適切です。

C++ のプログラミングを紹介する書籍の中には、コードを読んだ際に、コードの作成者がデフォルト演算子の効果を考慮に入れていることがわかるように、常にすべての演算子を定義することを勧めているものもあります。しかし、そうすることは明らかに上記で述べた最適化と相入れないものです。デフォルト演算子の使用について明示するには、クラスがデフォルト演算子を使用していることを説明したコメントをコードに入れることをお勧めします。

値クラスの使用

構造体や共用体などの C++ クラスは、値によって渡され、値によって返されます。POD (Plain-Old-Data) クラスの場合、C++ コンパイラは構造体を C コンパイラと同様に渡す必要があります。これらのクラスのオブジェクトは、直接渡されます。ユーザー定義のコピーコンストラクタを持つクラスのオブジェクトの場合、コンパイラは実際にオブジェクトのコピーを構築し、コピーにポインタを渡し、ポインタが戻った後にコピーを破壊する必要があります。これらのクラスのオブジェクトは、間接的に渡されます。この 2 つの条件の中間に位置するクラスの場合は、コンパイラによってどちらの扱いにするかが選択されます。しかし、そうすることでバイナリ互換性に影響が発生するため、コンパイラは各クラスに矛盾が出ないように選択する必要があります。

ほとんどのコンパイラでは、オブジェクトを直接渡すと実行速度が上がります。特に、複素数や確率値のような小さな値クラスの場合に、実行速度が大幅に上がります。そのためプログラムの効率は、間接的ではなく直接渡される可能性が高いクラスを設計することによって向上する場合があります。

互換モード (-compat=4) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。

これらの要素が含まれない場合は、クラスは直接渡されます。

標準モード (-compat=5) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。

これらの要素が含まれない場合は、クラスは直接渡されます。

クラスを直接渡す

クラスが直接渡される可能性を最大にするには、次のことを実施します。

各種のプロセッサでクラスを直接渡す

C++ コンパイラによって直接渡されるクラス (と共用体) は、C コンパイラが構造体 (または共用体) を渡す場合とまったく同じように渡されます。しかし、C++ の構造体と共用体の渡し方は、アーキテクチャによって異なります。

SPARC V7 および V8 では構造体と共用体は、呼び出し元で記憶領域を割り当て、ポインタをその記憶領域に渡すことによって渡されます (つまり、構造体と共用体はすべて参照により渡されます)。

SPARC V9 では、16 バイト (32 バイト) 以下の構造体は、レジスタ中で渡され (返され) ます。共用体と他のすべての構造体は、呼び出し元で記憶領域を割り当て、ポインタをその記憶領域に渡すことによって渡され (返され) ます (つまり、小さな構造体はレジスタ中で渡され、共用体と大きな構造体は参照により渡されます)。この結果、小さな値のクラスは基本の型と同じ効率で渡されることになります。

x86 では、構造体と共用体を渡すには、スタックで領域を割り当て、引数をそのスタックにコピーします。構造体と共用体を返すには、呼び出し元のフレームに一時オブジェクトを割り当て、一時オブジェクトのアドレスを暗黙の最初のパラメータとして渡します。

メンバー変数のキャッシュ

C++ メンバー関数では、メンバー変数へのアクセスが頻繁に行われます。

そのため、コンパイラは、this ポインタを介してメモリーからメンバー変数を読み込まなければならないことがよくあります。値はポインタを介して読み込まれているため、次の読み込みをいつ行うべきか、あるいは先に読み込まれている値がまだ有効であるかどうかをコンパイラが決定できないことがあります。このような場合、コンパイラは安全な (しかし遅い) 手法を選択し、アクセスのたびにメンバー変数を再読み込みする必要があります。

不要なメモリー再読み込みが行われないようにするには、次のようにメンバー変数の値を局所変数に明示的にキャッシュしてください。

この最適化は、基本の型の場合と同様に、値をレジスタに置くことができる場合に最も効果的です。また、別名の使用が減ることによりコンパイラの最適化が行われやすくなるため、記憶領域を使用する値にも効果があります。

この最適化は、メンバー変数が明示的に、あるいは暗黙的にたびたび参照渡しされる場合には逆効果になる場合があります。

現在のオブジェクトとメンバー関数の引数の 1 つの間に別名が存在する可能性がある場合などには、クラスの意味を望ましいものにするために、メンバー変数を明示的にキャッシュしなければならないことがあります。次に例を示します。


complex& operator*= (complex& left, complex& right)
{
  left.real = left.real * right.real + left.imag * right.imag;
  left.imag = left.real * right.imag + left.image * right.real;
}

上のコードが次の指令で呼び出されると、意図しない結果になります。


x*=x;