C++ プログラミングガイド |
第 8 章
パフォーマンス
C++ 関数のパフォーマンスを高めるには、コンパイラが C++ 関数を最適化しやすいように関数を記述することが必要です。言語一般、特に C++ のソフトウェアパフォーマンスについて関連する書籍は多数あります。たとえば、Tom Cargill 著、Addison-Wesley、1992 年発行、『C++ Programming Style』、Jon Louis Bentley 著、Prentice-Hall、1982 年発行、『Writing Efficient Programs』、Dov Bulka と David Mayhew 共著、Addison-Wesley、2000 年発行、『Efficient C++: Performance Programming Techniques』、Scott Meyers 著、Addison-Wesley、1998 年発行、『Effective C++ - 50 Ways to Improve Your Programs and Designs, Second Edition』などを参照してください。この章では、これらの書籍にある内容を繰り返すのではなく、Sun C++ コンパイラにとって特に有効なパフォーマンス向上の手法について説明します。
一時オブジェクトの回避
C++ 関数は、暗黙的に一時オブジェクトを多数生成することがよくあります。これらのオブジェクトは、生成後破棄する必要があります。しかし、そのようなクラスが多数ある場合は、この一時的なオブジェクトの作成と破棄が、処理時間とメモリー使用率という点でかなりの負担になります。Sun WorkShop C++ コンパイラは一時オブジェクトの一部を削除しますが、すべてを削除できるとは限りません。
プログラムの明瞭さを保ちつつ、一時オブジェクトの数が最小になるように関数を記述してください。このための手法としては、暗黙の一時オブジェクトに代わって明示的な変数を使用すること、値パラメータに代わって参照パラメータを使用することなどがあります。また、
+
と=
だけを実装して使用するのではなく、+=
のような演算を実装および使用することもよい手法です。たとえば、次の例の最初の行は、a + b
の結果に一時オブジェクトを使用していますが、2 行目は一時オブジェクトを使用していません。
T x = a + b;T x( a ); x += b;インライン関数の使用
小さくて実行速度の速い関数を呼び出す場合は、通常どおりに呼び出すよりもインライン展開する方が効率が上がります。逆に言えば、大きいか実行速度の遅い関数を呼び出す場合は、分岐するよりもインライン展開する方が効率が悪くなります。また、インライン関数の呼び出しはすべて、関数定義が変更されるたびに再コンパイルする必要があります。このため、インライン関数を使用するかどうかは十分な検討が必要です。
関数定義を変更する可能性があり、呼び出し元をすべて再コンパイルするには手間がかかると予測される場合は、インライン関数は使用しないでください。そうでない場合は、関数をインライン展開するコードが関数を呼び出すコードよりも小さいか、あるいはアプリケーションの動作がインライン関数によって大幅に高速化される場合にのみ使用してください。
コンパイラは、すべての関数呼び出しをインライン展開できるわけではありません。そのため、関数のインライン展開の効率を最高にするにはソースを変更しなければならない場合があります。どのような場合に関数がインライン展開されないかを知るには、
+w
オプションを使用してください。次のような状況では、コンパイラは関数をインライン展開しません。
- ループ、
switch
文、try
およびcatch
文のような難しい制御構造が関数に含まれる場合。実際には、これらの関数では、その難しい制御構造はごくまれにしか実行されません。このような関数をインライン展開するには、難しい制御構造が入った内側部分と、内側部分を呼び出すかどうかを決定する外側部分の 2 つに関数を分割します。コンパイラが関数全体をインライン展開できる場合でも、このようによく使用する部分とめったに使用しない部分を分けることで、パフォーマンスを高めることができます。- インライン関数本体のサイズが大きいか、あるいは複雑な場合。見たところ単純な関数本体は、本体内でほかのインライン関数を呼び出していたり、あるいはコンストラクタやデストラクタを暗黙に呼び出していたりするために複雑な場合があります (派生クラスのコンストラクタとデストラクタでこのような状況がよく起きる)。このような関数ではインライン展開でパフォーマンスが大幅に向上することはめったにないため、インライン展開しないことをお勧めします。
- インライン関数呼び出しの引数が大きいか、あるいは複雑な場合。インラインメンバー関数を呼び出すためのオブジェクトが、そのインライン関数呼び出しの結果である場合は、パフォーマンスが大幅に下がります。複雑な引数を持つ関数をインライン展開するには、その関数引数を局所変数を使用して関数に渡してください。
デフォルト演算子の使用
クラス定義がパラメータのないコンストラクタ、コピーコンストラクタ、コピー代入演算子、またはデストラクタを宣言しない場合、コンパイラがそれらを暗黙的に宣言します。こうして宣言されたものはデフォルト演算子と呼ばれます。C のような構造体は、デフォルト演算子を持っています。デフォルト演算子は、優れたコードを生成するためにどのような作業が必要かを把握しています。この結果作成されるコードは、ユーザーが作成したコードよりもはるかに高速です。これは、プログラマが通常使用できないアセンブリレベルの機能をコンパイラが利用できるためです。そのため、デフォルト演算子が必要な作業をこなしてくれる場合は、プログラムでこれらの演算子をユーザー定義によって宣言する必要はありません。
デフォルト演算子はインライン関数であるため、インライン関数が適切でない場合にはデフォルト演算子を使用しないでください (前の節を参照)。デフォルト演算子は、次のような場合に適切です。
- ユーザーが記述するパラメータのないコンストラクタが、その基底オブジェクトとメンバー変数に対してパラメータのないコンストラクタだけを呼び出す場合。基本の型は、「何も行わない」パラメータのないコンストラクタを効率よく受け入れます。
- ユーザーが記述するコピーコンストラクタが、すべての基底オブジェクトとメンバー変数をコピーする場合
- ユーザーが記述するコピー代入演算子が、すべての基底オブジェクトとメンバー変数をコピーする場合
- ユーザーが記述するデストラクタが空の場合
C++ のプログラミングを紹介する書籍の中には、コードを読んだ際にコードの作成者がデフォルト演算子の効果を考慮に入れていることがわかるように、常にすべての演算子を定義することを勧めているものもあります。しかし、そうすることは明らかに上記で述べた最適化と相入れないものです。デフォルト演算子の使用について明示するには、クラスがデフォルト演算子を使用していることを説明したコメントをコードに入れることをお勧めします。
値クラスの使用
構造体や共用体などの C++ クラスは、値によって渡され、値によって返されます。POD (Plain-Old-Data) クラスの場合、C++ コンパイラは構造体を C コンパイラと同様に渡す必要があります。これらのクラスのオブジェクトは、直接渡されます。ユーザー定義のコピーコンストラクタを持つクラスのオブジェクトの場合、コンパイラは実際にオブジェクトのコピーを構築し、コピーにポインタを渡し、ポインタが戻った後にコピーを破棄する必要があります。これらのクラスのオブジェクトは、間接的に渡されます。この 2 つの条件の中間に位置するクラスの場合は、コンパイラによってどちらの扱いにするかが選択されます。しかし、そうすることでバイナリ互換性に影響が発生するため、コンパイラは各クラスに矛盾が出ないように選択する必要があります。
ほとんどのコンパイラでは、オブジェクトを直接渡すと実行速度が上がります。特に、複素数や確率値のような小さな値クラスの場合に、実行速度が大幅に上がります。そのためプログラムの効率は、間接的ではなく直接渡される可能性が高いクラスを設計することによって向上する場合があります。
互換モード (
-compat[=4]
) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。標準モード (デフォルトモード) では、クラスに次の要素が含まれる場合、クラスは間接的に渡されます。
クラスを直接渡す
クラスが直接渡される可能性を最大にするには、次のようにしてください。
- 可能な限りデフォルトのコンストラクタ (特にデフォルトのコピーコンストラクタ) を使用する。
- 可能な限りデフォルトのデストラクタを使用する。デフォルトデストラクタは仮想ではないため、デフォルトデストラクタを使用したクラスは、通常は基底クラスにするべきではありません。
- 仮想関数と仮想基底クラスを使用しない
各種のプロセッサでクラスを直接渡す
C++ コンパイラによって直接渡されるクラス (および共用体) は、C コンパイラが構造体 (または共用体) を渡す場合とまったく同じように渡されます。しかし、C++ の構造体と共用体の渡し方は、アーキテクチャによって異なります。
メンバー変数のキャッシュ
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;
サン・マイクロシステムズ株式会社 Copyright information. All rights reserved. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |