プロが教える店舗&オフィスのセキュリティ対策術

仮想関数を使った方がはるかに楽になる場合で、意味的にもまさに継承がベストという場合を除いては継承は使わない、使う場合も、なるべく1回で、かつ一つで十分になるような方法を考える方針できたのですが

今までは手持ちの情報的に問題ないだろうとおもっていたのですが、ある新事実が発覚したため

一回の継承では別々の親クラスとして作ることとなり、あとで同じ基底クラスのポインタとして扱えないタイミングが生じて面倒が出るか、あるいはそれ以外で一つにまとめようとするとどの方法をとっても非常に大きなコードの無駄が生じることになり

コスト的に問題なく出来るなら二段に継承したいという状況が発生しました。


現在1段の継承をしている状態で、基底クラスは別々に二つあり、どちらも抽象クラスで、もしこれらのさらに親クラスを同じものとして作るような形に変更した場合、二段階に継承するといっても、どちらにしてもその時に最終的に完成する、3段目の孫クラスで始めて実体を作れる(コンストラクタを呼べる)ようになる

つまり、そうなっても親と子は抽象クラスという状態になります。


真ん中のクラスで、一番上のクラスの純粋仮想関数をoverrideする関数もいくらかは出てくるかもしれませんが、定義せずに残す事が確実に決まっている関数も既に存在しますし、逆に真ん中のクラスで増える純粋仮想関数も存在する見込みです。

こういった状況で、問題と考えられるのはただ一つ、コストに関して、なのですが



全ての仮想関数は、オーバーライド回数は親・子・孫間で必ず1回に限定されるようにプログラムする場合

1. 結局それに関しては一回分の継承とまったく同じコストになると考えて良いでしょうか?

つまり仮想関数が、だいたいは関数ポインタのような原理で動いているとすれば、テーブルの要素(?)が増えた場合にコストが変わる、とか、継承回数が変わると一番上で宣言した純粋仮想関数を、真ん中でまったくoverrideせずに孫クラスでoverrideした場合、一段階しか継承せずに(単純に親クラスで宣言したのを子クラスでoverride)した場合とは実はコストが変わるとか

そういう事はまずないと考えて良いのでしょうか?

2. 仮想関数で最も恐れるべきはキャッシュミスだと思うのですが、それについての危険性はそれほどかわりがない、でしょうか?簡単なクラスでベンチマークをとった結果は、「特定の範囲で繰り返してるだけ」に近かったせいか、クラスが小さかったせいか、横にはせまかったせいか、ほとんど変わりが分かりませんでした。)


3. コンストラクタやデストラクタについては、極力初期化リストで初期化、というパターンに頼った場合

かつ、孫クラス以外の、親クラスと子クラスは抽象クラスとなる、場合で

かつ、親クラスのコンストラクタ、デストラクタはインライン化宣言されていて(つまり子クラスのコンストラクタ・デストラクタを呼ぶときにはその実装が既に見える位置に記述されるようになっていて)
子クラスのそれらはインライン宣言されていない


という状況の場合、単純に1段階の継承のみで基底クラスのそれらがインライン宣言されていない場合と、最終的にコストは等価になると考えてもいいのでしょうか?

A 回答 (4件)

既に指摘のあるとおり、コンパイラに依存する部分なので


基本的にVisual Studio2005でどうだったかという話をします。

まず当たり前ですが、継承を重ねればコードサイズは増えます。ただし潤沢にメモリーがあるWindows環境では気になる問題ではありません。
また継承の階層を増やしていくと
new/deleteが多発(秒間10万回等)する場合にコンストラクタ/デストラクタの呼び出しコストが顕著に現れます。
ただ生成(new)の回数を減らし、一度作成したオブジェクトは破棄せず使いまわす事で回避できます。
メモリーには常駐しますが速度的には良いです。

>全ての仮想関数は、オーバーライド回数は親・子・孫間で必ず1回に限定されるようにプログラムする場合
1回だろうが2回継承だろうが、呼び出しのパフォーマンスはほとんど変わらないと思います。
vtable(仮想関数テーブル)
http://ja.wikipedia.org/wiki/%E4%BB%AE%E6%83%B3% …
等を調べてみてください。

>2. 仮想関数で最も恐れるべきはキャッシュミスだと思うのですが、
これは、プラットフォーム、特にCPUの分岐予測の性能に依存します。
経験的に昨今のWindows機ではほとんど影響ありませんが
階層に関係なくオーバーライドするだけでキャッシュヒットしなくなりパフォーマンスに影響を与えるゲーム機等はあります。
その場合に速度を重視するならクラスで継承とvirtualを使わないようにする・・・という前世代的な方法しかありません。

>3...インライン宣言されていない場合と、最終的にコストは等価になると考えてもいいのでしょうか?
vtableを持つクラスのコンストラクタは一般的にインライン化出来ないと思います。
インライン宣言をすると、警告を出すコンパイラが多いかと思います。
    • good
    • 0
この回答へのお礼

おお!
まさにこれっ、て感じの情報を色々とどうもありがとうございます。


>メモリーには常駐しますが速度的には良いです。

そうですね。普通の継承なしクラスでさえ、最適化で一時オブジェクトの生成が完全に消されない、かもしれない、と思うと、頻繁に呼び出されるところではむやみに確保するかもしれない表記すら避けたいときはあるくらいなので

ましてや階層の大きな継承をしたクラスは、確保・解放はなるべく少なく抑えることを心がけています。(それでも、例えばMIDIファイルから読み込みしたときにイベント制御するとなると、仮想関数使いたくもなりますし、数千程度~万単位は普通に出現してもおかしくはないですがw 一回やればしばらく必要はないのでそれも問題はないでしょう。)


>1回だろうが2回継承だろうが、呼び出しのパフォーマンスはほとんど変わらないと思います。

やはりそうですよね。
wikipediaはちら見はしていましたが、精読はしていませんでした。時間ができたらじっくり見てみようと思います。


>経験的に昨今のWindows機ではほとんど影響ありませんが
>階層に関係なくオーバーライドするだけでキャッシュヒットしなくなりパフォーマンスに影響を与えるゲーム機等はあります。


こういう情報は今後の役に立つ可能性が高いので非常にありがたいです。


>その場合に速度を重視するならクラスで継承とvirtualを使わないようにする・・・という前世代的な方法しかありません。


どの道ユーザーが任意のファイル入出力を行えることを想定する場合は、ファイルからロードして最初に確保するときは、何らかの定数をenumやconst整数配列かなにかなどで持っておいて場合分けするしかないと思うので

その時の定数を下のコードのようにクラスのIDとしてconstな整数メンバに埋め込んでおけば

おんなじようにするなら、基底クラスにstaticメンバでも持たせてそこでswitch文を使うという方法を、別の仮想関数のかわりに用いる、ということになるでしょう。

それのチェックがいちいち煩わしいから純粋仮想関数を使いたくなってしまいますが。

しかし、仮想関数ははっきりと呼び出す先が分かってない限りインライン化出来ないので
Windowsとかでも、よほど速度にこだわりたい場合にかぎっては、その手を使うこともあります。

私の場合は


#define change_toaddsub


というのをプリコンパイル済みヘッダーに定義してあるので、そういう場合は「static関数として親クラスにまとめて持たせる」というのを基本方針としてとりつつ
クラスの宣言部に

static 型 関数名();



change_toaddsub static 型 関数名();


などと書き加えておくことで、後でサブクラスを追加するときに手直しすべき関数をチェックするのを楽にしたりしています。


>vtableを持つクラスのコンストラクタは一般的にインライン化出来ないと思います。


コンストラクタ自体は呼び出す先が決まってるから、いつでもインライン化は出来るんじゃないかな?という風におもっていたんですが

その状況ではそれ自体が誤りだったのですね!


確かに、いくらかパターンを用意してアセンブリコードを出力させてみたら、その全てがインライン指定の有無では
変わっていませんでした。

ものすごく簡単な、仮想ではない関数でもコンパイルオプションの最適化の設定であっさり変わりましたが、こちらは最適化の有無で変わらず、また
__forceinline を付けても全然変わりませんでした。


>インライン宣言をすると、警告を出すコンパイラが多いかと思います。


そういうことであれば、やはり多少のコストについては割り切って使う、ということですね。逆に、そうだと分かっていれば、今まで書いてきた基底クラスのコンストラクタの中身によっては、余計なヘッダのインクルード減らせて、コンパイル時間を若干節約できるかもしれません。

お礼日時:2011/04/13 10:01

「コスト」といっても、時間的なコストもあればサイズ的なコストもあります。


いずれにせよ、具体的な利用状況や処理系によります。
具体的なコードを示したうえで、想定している処理系(複数でも構いません)を明らかにしてください。

この回答への補足

↓のコードだと、現状文字数などの関係でBBBに全く存在意義がないですが、もちろん実際には意味があるからこそ二段階に継承させたいので

…疑問は、下の場合だと、BBBがなくてCCCやDDDがAAAから直に継承されている場合と、BBBを一度継承してさらにそれを継承しているかどうかによって、コンストラクタ・デストラクタ・仮想関数のコストは、それほどかわりがないと見て良いかどうか、というものです。

補足日時:2011/04/13 02:34
    • good
    • 0
この回答へのお礼

確かにそうですね。
ありがとうございます♪

今回私が知りたいのは、時間的なコストです。

処理系は、ひとまず
開発環境ではVC++の2008または2010で
OSとしてはWindowsのXP以降、32bit版または64bit版になります。

もちろん、質問の内容が
それ以外にも当てはまりそうな事だった場合はそれらを含んで構いません。

お礼日時:2011/04/13 02:24

文章だけで書かれてもよくわからんので,


「こんな感じ」
というコードを出してほしいかなぁ....

「2段に継承」というのがよくわからん.

この回答への補足

意味を持たせず最小限のコードだと、この場合だめかもしれないのですが
2000文字の制限があるので、補足とお礼欄にまたがって書いてみます。

それでもかなり省略したり、エラーチェックを省いたり、等々しますが、実際のヘッダとソースの分類はインライン化の提案をしてるかどうかで判断してください。ただ、一つのソースファイルにそのままコピペして使えると思います。

AAAが親、BBBが子、CCCとDDDが孫で、CCCとDDDのみ実体を作れます。
それの管理はAAAがまとめて行うことを考えています。


#include <windows.h>
#include <stdio.h>
#include <tchar.h>


#pragma comment ( lib, "winmm.lib")
#pragma comment ( lib, "user32.lib")
#pragma comment ( lib, "Gdi32.lib")


class AAA {

static AAA* first;
static int deletetimes;//デストラクタが呼ばれた回数を確認用

static AAA* New( byte** data );

protected:

AAA* next;
int int1, int2;
const byte id; enum { CCC_CLASS_ = 0 , DDD_CLASS_ };

AAA( int id_, int a_, int b_ ) : next(NULL), id(id_), int1(a_), int2(b_) {}
virtual ~AAA(){ ++deletetimes; }

virtual void Func_V() const = 0;

public:

static void Load( byte** data );

static void Check();

static void Func_S( int index ){
AAA* aaa = first;
while (index--){
if (!aaa) return;
aaa = aaa->next;
}
if (aaa) aaa->Func_V();
}

static void Cleanup(){
AAA* aaa = first, *aaa2;
while ( aaa ){
aaa2 = aaa->next;
delete aaa;
aaa = aaa2;
}
first = NULL;
TCHAR c[30];
_stprintf_s( c, 30, _T("%d"), deletetimes );
MessageBox( NULL , c, NULL, 0 );
}


};

AAA* AAA::first(NULL);
int AAA::deletetimes(0);


void AAA::Load( byte** data ){

int count;
memcpy( &count, *data, sizeof count ); *data+=sizeof count;
first = New( data );
AAA* aaa = first;
--count;

for (int i=count; i--; ){
if (!aaa) return;
aaa->next = New( data );
aaa = aaa->next;
}


}


class BBB : protected AAA {

protected:

BBB( int id, int a_, int b_ );
virtual ~BBB(){}


};

BBB::BBB( int id, int a_, int b_ ) : AAA( id, a_, b_ ) {}

class CCC : private BBB {

friend class AAA;

TCHAR* text;
WORD textlen;

CCC( int*, byte** );
~CCC(){ free(text); }

virtual void Func_V() const override {
TCHAR c[30];
_stprintf_s( c, 30, _T("%d %d %d"), id, int1, int2 );
MessageBox( NULL, text , c, 0 );
}

};

補足日時:2011/04/13 02:14
    • good
    • 0
この回答へのお礼

CCC::CCC( int* ab, byte** data ) : BBB( CCC_CLASS_, ab[0], ab[1] ) {
memcpy( &textlen, *data, sizeof textlen ); *data += sizeof textlen;
text = (TCHAR*)malloc( (textlen+1) * sizeof(TCHAR) );
memcpy( text, *data, textlen*sizeof(TCHAR) ); *data += textlen*sizeof(TCHAR);
text[textlen]=_T('\0');
}


class DDD : private BBB {

friend class AAA;

float x, y, z;

DDD( int*, byte** );
~DDD(){}

virtual void Func_V() const override {
TCHAR c[200];
_stprintf_s( c, 200, _T("%d %d %d\r\n"), id, int1, int2 );
OutputDebugString( c );

_stprintf_s( c, 200, _T("%f %f %f"), x, y, z );
MessageBox( NULL, c , NULL, 0 );
}

};


DDD::DDD( int* ab, byte** data ) : BBB( CCC_CLASS_, ab[0], ab[1] ) {
memcpy( &x, *data, sizeof x ); *data += sizeof x;
memcpy( &y, *data, sizeof y ); *data += sizeof y;
memcpy( &z, *data, sizeof z ); *data += sizeof z;
}


AAA* AAA::New( byte** data ){

const byte id_ = **data; *data += 1;

int ab[2];
memcpy( ab, *data, sizeof(ab) );*data += sizeof(ab);

switch ( id_ ){
case CCC_CLASS_: return new CCC( ab, data );
case DDD_CLASS_ : return new DDD( ab, data );
}

return NULL;

}


ロード・解放・確認用関数を作っていますが
一通りのチェックは↓これで出来るかと思います。


void AAA::Check(){

byte buf[1000];
byte* b = buf;

int i=3;
memcpy( b, &i, sizeof i );b += sizeof i;

for (int j=2;j--;){
*b = CCC_CLASS_; ++b;
memcpy( b, &i, sizeof i ); b += sizeof i;
memcpy( b, &j, sizeof j ); b += sizeof j;
LPCTSTR c = _T("test");
WORD w = _tcslen(c);
memcpy( b, &w, sizeof w ); b +=sizeof w;
memcpy( b, c, w*sizeof(c[0]) ); b += w*sizeof(c[0]);
}

*b = DDD_CLASS_; ++b;
memcpy( b, &i, sizeof i ); b +=sizeof i;
memcpy( b, &i, sizeof i ); b +=sizeof i;
float f_[] = { 0.1f, 2.0f, 30 };
memcpy( b, f_, sizeof f_ );

b = buf;
Load( &b );

while(i--) Func_S( i );
Cleanup();

}

真ん中のクラスはこのコードだとBBBの一つしかありませんが、当然実際にはそれにあたるものがもうひとつ存在することになりますし、実際には遥かに長いので…概要としては、コードで示すと、こういう感じの構造のイメージです。

おー、ぎりぎり文字数大丈夫でしたw(残り1)

というわけで、アドバイスありがとうございます♪

お礼日時:2011/04/13 02:20

「どんな機械語が生成されるか」という問いに対しては「コンパイラの勝手」です。


言語仕様はこの問いに対して何の答えもくれません。
プロファイルするとか、実際に吐いた機械語を吟味する以外、判断するすべはありません。

と前置きしたうえで、「それほどまでにコストを気にするアプリケーションなのか」が気になります。
    • good
    • 0
この回答へのお礼

どうも、ありがとうございます♪

やはりそうですか…


>「それほどまでにコストを気にするアプリケーションなのか」が気になります。

単純に、速度の差があまりに小さくて誤差に埋もれてしまうような話なら、利便性をとるため二段の継承もしたいし、純粋仮想関数も適切に使いたいけども

速度の差が気になるようならそうも言えない

という事を思っていたのですが
どんな機械語が生成されるか言語仕様からは判断できないとなれば

確かに、その角度でもっと具体的に深く追求した方が良いですね。


というわけで


コンストラクタ・デストラクタについては、ファイルの読み込みやアプリケーション終了などの理由から、コンストラクタ・デストラクタがぶん回されてる最中に、他のスレッドがちょっかいを出したり、何か膨大な解析をする可能性は完全に撤廃できる状況にあります。

従って、この点についてはあまりに遅くなければ問題ないということになります。



二段階継承にした時に最も上にくる基底クラスは、『インスタンス変数』としては
3つの単純な整数と、自分自身の型のポインタ(next)をさすメンバの、たった4つのメンバ変数を持ち、コンストラクタ中では初期化リストでそれらを初期化することしかしません。

これが

仮に、相当、かなり多く見積もって10万回呼ばれたとして

例えば、多く普及している平均的なの性能のXP以降ぐらいで差が1秒以内に収まるなら、全く気にならないレベルです。

意図的に余分なことをするわけはないので、それだけの差でさすがにたった10万回で1秒なんて到底かからないと見ていいでしょうか?
ちなみに今ある、そこそこのファイルを読み込んだらコンストラクタが呼ばれた回数3000弱でした。

デストラクタはnextのポインタを使用して一括解放などとからんできますが、結局『継承とかかわりのある』行動としては、単に表記上virtualで一段階だったのが二段階になる、という以外全く変化はありません。
これも10万で1秒以内なら余裕です。




それ以外の仮想関数の問題については

別スレッドから可能な限り速く呼ばれる瞬間が発生するタイミングがあります。
その時に同時にメインスレッドで、仮想関数とは無関係にすさまじく膨大な演算が行われる可能性も、あるにはあります。

ただし、それは全体からみれば稀なケースです。
また、なるべくその回数を減らすようにすることも考えられます。


これは、一段の継承と二段の継承における仮想関数呼び出しの時間差が、もし発生した場合は、そのオーバーヘッドが、30万回で1秒以内ぐらいならよしとしておきます。


そんなに変わるようなことはまずないと思っていい、ですかね?

お礼日時:2011/04/12 22:26

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!