C++言語 パフォーマンス

Last-modified: 2015-04-14 (火) 22:29:56

何か気付いた点があれば、追記をよろしくお願いします。

参考

http://www.open-std.org/JTC1/SC22/WG21/
http://www.open-std.org/JTC1/SC22/WG21/docs/TR18015.pdf
http://www.s34.co.jp/cpptechdoc/article/sizeof/index.html

参照 - reference

  • 自前のクラスなどの場合、参照を使う方が早い
  • 組み込みの型の場合、参照を使わない方が早い

名前空間 - namespace

名前空間が、パフォーマンスには影響を与えることはない。
メモリ的にも実行速度的にも。
ただし

  1. 関数などの識別子を短くすることの妨げになることがある
  2. メモリ上の配置が変わる可能性もある

キャスト - cast

C表記、C++表記いずれにしても一時的なオブジェクトが作られる可能性がある
const_cast,static_cast,reinterpret_castはメモリ的にも実行速度的にもペナルティがない
dynamic_castは、余計なオーバーヘッドが生まれる可能性がある。
dynamic_castを使うぐらいなら、RTTIによってキャストなしで動作する設計にすべし。
ちなみに、テレビゲーム用のC++コンパイラは、デフォルトでRTTIや例外がオフになっていることが多い。遅いから。

クラス - class

シンプルなクラス

(英語ドキュメントでの「virtual」が仮想関数のことなのかどうかわからなかったが次のどちらかの意味だと思う)

  1. 仮想関数のないクラスは、同じデータメンバを持つ構造体と同じだけのメモリを消費する
  2. 実質的な関数のないクラス(実装がないという意味かな?)は、同じデータ・・(以下同文)
  3. コンパイラの最適化依存で、実行速度が犠牲になる(スマートポインタと従来のポインタで1%の差異しかないコンパイラもある)

仮想関数をもつクラス

仮想関数を持つクラスは、次のような損失を招く。

  1. オブジェクト毎に、ポインタ分のメモリ消費
  2. クラス毎に、仮想関数テーブル分のメモリ消費(仮想関数テーブルは、1つの仮想関数ごとに1~2ワード分のメモリ消費がある)
  3. クラス毎に、RTTIのために40バイトほど消費する(クラス名とかその他情報とか)。

クラス内の関数呼び出しにかかる実行速度的なコスト

非仮想関数、非スタティック関数、非インライン関数

グローバルな関数にクラスオブジェクトのポインタを与えてグローバル関数を呼び出す場合と、クラスの非スタティック関数を呼び出す場合でも、どちらの場合でも大して変わらない。
またアロー演算子(->)でもドット(.)でも呼び出しにかかるコストは、ほとんど変わらない(結局thisを渡すから?)。

スタティック関数

thisを渡さないだけあって、クラスの非スタティックな関数よりも若干高速になる。
グローバル関数も引数にクラスオブジェクトを渡さないので、クラスのスタティック関数を呼び出す場合と同程度の処理コストがかかる。
総じて、スタティック関数においても、たいした実行速度的なコストはない。

仮想関数

自前で関数テーブルを用いた方が若干、高速。

テンプレートクラス中の仮想関数

だいぶ低速?

インライン関数

実質的に関数の呼び出しではなくなるので、他関数に比べて高速。
ただしプリプロセッサマクロの方が若干高速。

多重継承

非仮想関数を継承している場合、子クラスでの呼び出しは低速。
仮想関数を継承している場合は、子クラスでも親クラスでも同じ速度で呼び出せる(ただし、元々低速)。

仮想クラス?仮想継承?

仮想関数のないクラス?に比べて仮想関数のあるクラス?は低速。
それぞれを継承したクラス?も若干低速になる。

型情報

次のケースで、typeidを行ったときのコスト

  1. グローバル
  2. 親クラス
  3. 子クラス
  4. 仮想親クラス?
  5. 仮想親クラスを継承したクラス

コンパイラにもよるが、子クラスの方が高速。

dynamic_cast

  1. 親クラスへのアップキャスト
  2. 子クラスへのダウンキャスト
  3. 兄弟へのクロスキャスト
  4. 親クラスへのアップキャスト(2階層)
  5. 子クラスへのダウンキャスト(2階層)
  6. 兄弟へのクロスキャスト(2階層)

親クラスへのアップキャストを基準として
子クラスへのダウンキャストは低速。
兄弟へのクロスキャストは更に低速。

キャスト先の階層には関係ない。

エラー処理

戻り値としてエラーを返すか、例外機構によってエラーを返すか。
基本的に例外機構は、非常に低速である。一方、戻り値によって返すエラーの方が高速である。
が、しかし、めったに起こらないエラーに対しては例外機構を用いた方が若干高速となる。
これは戻り値によって、常にエラーコード(正常終了)を返して、呼び出し側に戻り値をチェックさせると
余計なコードを作る必要があるためである。

一般的に考慮すべきこと

初期化(構築)に関して

関数呼び出し

  • 関数呼び出し1 組み込みの型が引数
    void f1(double);
    f1(7.0); // 値のコピーが発生
    f1(7);  // 一時オブジェクトが作られて、さらにコピーが発生 → f1( double(7) );
  • 関数呼び出し2 組み込みの型のconst参照が引数
    void f2(const double&);
    f2(7.0);
    f2(7);  // 一時オブジェクトが発生 → const double tmp=7; f2(tmp);
  • 関数呼び出し3 オブジェクトが引数
    void f3(std::string);
    std::string s = "TEXT";
    f3(s); // オブジェクトのコピーが発生
    f3("TEXT");  // 一時オブジェクトが作られて、さらにコピーが発生 → f3(std::string("TEXT")
  • 関数呼び出し4 オブジェクトのconst参照が引数
    void f4(const std::string&);
    std::string s = "TEXT";
    f4(s);
    f4("TEXT");  // 一時オブジェクトが発生 → const std::string tmp("TEXT"); f4(tmp)

以上のような自動的な型変換や一時オブジェクトの作成を抑止したいなら
クラスオブジェクトのコンストラクタ(すべての1引数コンストラクタ)をexplicitにしておく

コンストラクタ

コンストラクタなどを書かない場合でも、コンパイラが自動で追加してしまう。
また対象のクラスが別のクラスオブジェクトをメンバに持っているなら、
その持っているクラスのデフォルトコンストラクタが暗黙的に呼ばれてしまう。

一時オブジェクト

基本的なこと

  • Matrix a;
    効率的でない。本当にaが必要となるまでオブジェクトは作られない。
    デフォルトの初期化が行われてしまう。
  • a = b + c;
    効率的でない。(b+c)によって一時オブジェクトが作られる。
  • Matrix a = b;
    ベター。デフォルトの初期化が行われない。
  • a += c;
    ベター。一時オブジェクトが作られない。

戻り値

以下のコードでは、戻り値のインスタンスが一時いオブジェクトとして構築されてしまう。

const A operator * (const A & l, const A & r)
{
    A tmp;
    tmp.val = l.val * r.val;
    return tmp;
}

しかし以下のようにコーディングしておけば、余計なコピーを抑止させるためのヒントをコンパイラに与えることができる。

const A operator * (const A & l, const A & r)
{
    return A(l.val * r.val);
}

ただし、必ずコピーが抑止されるわけではない。また、最初の例で示した書き方をしても最適化されるコンパイラも存在する(VC2005)。→「RVO (Return Value Optimization)」と呼ばれる。

イテレータ

たいていのイテレータでは後置インクリメント(it++)で一時オブジェクトが作られてしまう。
前置インクリメント(++it)の場合、先に値をインクリメントするので一時オブジェクトを作る必要がない。
つまり、イテレータを使う場合は次のようにする。

for (it = obj.begin();
     it != obj.end();
   ++it)  // 一時オブジェクトは作られない。it++だと一時オブジェクトが作られる。
{
  // something to do
}

デフォルト引数

関数の引数のうち、オブジェクトの引数で、その引数がデフォルト引数を取るような場合に注意。

  • 何度も同じオブジェクトが・・・
    void f1( const C & obj = C(0) )
    {
       // something to do
    }
    これでは、毎回C(0)の一時オブジェクトが作られることになる。
  • 関数の呼び出し時に1度だけオブジェクト生成 その1
    void f2( const C & obj )
    {
       // something to do
    }
    void f2( )
    {
       static const C c_default(0);
       f2(c_default);
    }
    ただし関数が二つになるのでコーディングが面倒。
    それにc_defaultがconstである必要がある。
  • 関数の呼び出し時に1度だけオブジェクト生成 その2
    const C c_default(0);
    void f3( const C & obj = c_default ) {
       // something to do
    }
    ただし、c_defaultがconstである必要がある。