- ベストアンサー
メンバとローカル変数のパフォーマンスについて
- メンバとローカル変数のパフォーマンスについて実験を行いました。実験では、2つの異なる関数パターンを比較し、後者の方が速い結果が得られました。アセンブリ出力を見た結果、前者ではthisポインタ経由で操作が行われ、後者ではローカル変数がスタックのアドレスを使って操作されていることが分かりました。しかし、このオプティマイゼーションは全てのコンパイラで行われるわけではなく、スタックの消費量や関数の呼び出しに影響を与える可能性があるため、注意が必要です。
- この実験では、メンバとローカル変数の違いがパフォーマンスにどのような影響を与えるかを調査しました。実験結果から、ローカル変数を使用する方が高速な結果が得られたことが分かりました。アセンブリ出力を見ると、前者ではthisポインタ経由での操作が行われ、後者ではローカル変数が使用されていることが分かりました。ただし、この最適化がすべてのコンパイラで行われるわけではなく、スタックの消費量や関数の呼び出しに影響を与える可能性があるため、注意が必要です。
- メンバとローカル変数のパフォーマンスについて検証を行いました。実験では、2つの異なる関数パターンを比較し、後者の方が高速な結果が得られました。アセンブリ出力を見ると、前者ではthisポインタ経由での操作が行われているのに対し、後者ではローカル変数を使用して操作が行われていることが分かりました。ただし、このオプティマイゼーションはすべてのコンパイラで行われるわけではなく、スタックの消費量や関数の呼び出しに影響を与える可能性があるため、注意が必要です。
- みんなの回答 (2)
- 専門家の回答
質問者が選んだベストアンサー
ん~, そこまでの最適化はできないんじゃないですかねぇ.... 言われるように, マルチスレッドを考えると無理. そうでなくても c -= int( data[i] ); における -= の副作用は ; の直後で確実に適用していなきゃならない. この場合の「副作用」は「メンバ変数の値を変更する」ことだから, この文が終わった時点でメンバ変数の値が変わっていないとおかしい.
その他の回答 (1)
- jacta
- ベストアンサー率26% (845/3158)
プロセッサのアーキテクチャやコーリングコンベンション、そして最適化性能によります。
お礼
どうもjactaさん、ご回答ありがとうございます♪ なるほど 非staticなメンバ関数は デフォではthiscallになってて、thiscallはコンパイラ依存 ってことでしたが メンバ関数って__stdcallとかって指定していいんでしたっけ? __cdecl、__stdcall、__fastcallを明示的に指定してアセンブリ出力してみたのですが いずれの場合も 上の、移し替えなしの方法では スタックのアドレス操作とスタックレジスタを使う方法に 最適化によって自動的に切り替わっていることはありませんでした。 http://www.thinkridge.com/modules/tinyd1/rewrite/tc_2.html にも書いてあるように この部分は最適化でそう書き変えるのは 私が質問文で書いたように、整合性が取れなくなる可能性があると思うので 実際にはないんじゃないかと思ったのですが (つまり、言語仕様に近い問題…?) プロセッサのアーキテクチャによってはあり得るってことなのでしょうか? ところが 確かにスタックレジスタは使ってないものの __stdcall で上の方法を使った時 出力されたアセンブリは最短の行数となり かつ測定結果、最速となってしまいました。 __stdcallを指定し 移し替えるという方法をとった場合も ほぼ同じ速度になりますが それでも若干移し替えなしの方が勝っている感じもします。 全体としては速い順に stdcall > fastcall > thiscall > cdecl こんな感じです。 アセンブリを完全に読み切れば全貌が理解できるのかもしれませんが 別のソースで同じようになるのかもわかんない現状では 個人的には「?」って感じです また、x64だとfastcallがどうのこうのっていう話を ほんの少し聞いたような気もしなくもないのですが WindowsXP以降用で 何かこう「現状これが良さ気」って言う決定打ってないものでしょうか。
補足
おっと メンバ関数って__stdcallとかって指定していいんでしたっけ? ↓ 非staticなメンバ関数って__stdcallとかって指定していいんでしたっけ? です。
お礼
Tacosanさんもご回答ありがとうございます♪ >c -= int( data[i] ); における -= の副作用は ; の直後で確実に適用していなきゃならない なるほど、そういうことであれば やるコンパイラがあったらそれがおかしいと考えてよさそうですね。 ところが今回下の実験でまた謎なことにw んでも コンパイルオプションのせいだったのかもしれないと思い /O2 /GL /D "WIN32" /D "_DEBUG" /D "_UNICODE" /D "UNICODE" /FD /EHsc /MDd /Yu"stdafx.h" /Fp"Debug\AlgorithmExper.pch" /FA /Fa"Debug\\" /Fo"Debug\\" /Fd"Debug\vc90.pdb" /W3 /nologo /c /Zi /TP /errorReport:prompt プラス /MP の状態に変更しました。 さらに、前回のコンパイルオプション(どこをどういじったのか曖昧ですが、確か「リンク時の最適化あたりが変わってると思います」) において で、非staticの非インラインのメンバ関数の__stdcallで 移し替えなしバージョンが勝りましたが 今回の条件で再度試してみると 4つの呼び出し規約 における 移し替えバージョン 移し替えなしバージョン の8パターン全てにおいて 「呼び出し規約の変更」 では出力アセンブリは変化しませんでした。 また、今回の条件では 移し替えバージョン優勢に逆転したっぽいです。 double data[32]; の添え字やループ回数を 32→10000ぐらいにしてみたら その差はさらに顕著になりました。 ところが もしも「多大な演算の最深部」で「連続して頻繁に呼び出される」ような関数があったら 普通は「インライン展開」の可能性を考慮してみるべき ということになるはずでした なのでfuncにも__forceinlineを付けて実験してみると 移し替えなしバージョン > 移し替えバージョン にさらに逆転しました。 コンパイルオプションを変えると少し事情が変わってくることもありましたが 少なくともこの状況では __forceinlineでインライン展開された場合でも 呼び出し規約の明示的な指定で変化はありませんでした。 (むしろインライン展開では呼び出し規約は関係ないと思うので、変わるほうが変な気がしますが) と・こ・ろ・が その時間を見てみると インライン版 よりも 非インライン版 の方が、いずれも速いです。 移し替え→○ 移し替えなし→× インライン→i で、速い順に ○ > × > ×i > ○i こんな感じです。 × と ×iは僅差ですが ○iはひどいです。 ○ と ×もそこそこの差があります。 呼び出し側のコードはこんな感じです。 void ssss(Widget*, double*); int main(void){ double d[10000] = {777}; //チョット危なげですがあくまで実験なので Widget a; { Debug::Performance z; //詳細はhttp://oshiete.goo.ne.jp/qa/7262790.html ssss( &a, d ); } for ( int i = 32; i--; ) Debug::f( d[i] ); //確認及び万が一最適化で消えるのを防ぐ用 return 0; } void ssss(Widget* w, double* d ){ //ここでのfuncがインラインになるかどうかというだけの差のはず for ( int i = 10000; i--; ) w->func( d ); //10000要素に変えたのでこっちは少なく } どういうキャッシュの持ち方してたらこんな結果になるのか謎ですがw う~ん、もうひとつ何か欲しいところですね。 「この検証に対してはコンパイルオプションが不完全」とかだと一番楽なんですが
補足
さらなる経過報告です。 http://codezine.jp/article/detail/420 このあたりを参考にしながら EXEファイルの解析 を行ったり、全体の大よその配置をつかんだあと ちょこっとだけ書き変えて機械語の該当箇所のアドレスを割り出したり キャッシュの勉強をしたり、とにかく色々しました。 で、コンパイルオプションの、出力ファイルのところを アセンブリ コード、コンピュータ語コード、ソース コード (/FAcs) に変えてじっくり観察してみると 「ある仮説」が浮上してきましたw (といっても、あくまで仮説なので全く的外れかもしれませんが) 私のCPUは L1キャッシュが 2 x 64 KiB で 2-way set associative キャッシュラインサイズは確か64Byte な感じだと思うのですが double data[10000]; に関しては 8*10000で80,000バイト、これはCPUガンガンに使ってるときは、おそらくはL1だけで何とか収まってるか あるいは、L2が使われることになるとしても そのあたりの挙動はあんま変わってない可能性が高めと思うのです。 で ○ > × > ×i > ○i この図式なんですが よーく機械語(というか関数開始位置からの相対アドレス)とアセンブリとコメントで出てくる元のソースを見比べてみて キャッシュの気持ちを読み取ろうと思ってみると 2-way set associativeなんで自由のはずですが とりあえずループのジャンプ先(ラベル)の箇所を起点にL1命令キャッシュを持っておく というのはどうかなと。 そのとき「最も外側にあるjneと、そのジャンプ先のラベルの、アドレスを見比べてみると」 (↓10進表記で、コンパイル後の関数の先頭からの相対アドレスです) ○ 70 153 差:83 × 41 145 差:104 ×i 38 165 差:127 ○i 41 179 差:138 こんな風になってました。 で、ですよ 試しに インライン化して尚且つ無駄に二重ループにしてみました。 void ssss(Widget* w, double* d ){ for ( int a = 90; a--; ) for ( int i = 100; i--; ) w->func( d ); } 実行時間は計測上、これでようやく、×iと○iの中間ぐらいです funcの実行回数は実に1000回(1割)も減っているわけですが。 このときのアドレスの関係ですが 51 225 差:174 2重ループにしたので、また離れてます。 これだったら 64Byteのラインの 2-way set 1つでは確実に無理ですよね。 これらのことから、もしかすると ・キャッシュラインのセットへプリフェッチを行う際ループがある場合、外側のループのラベルを基点にしてやってる ・関数が別途呼び出される場合は別のキャッシュラインのセットへ別途読み込まれる 可能性はないでしょうか? もしこれが正しいのならば、やっぱりまさにアーキテクチャ依存ではあるけども n-way set associativeが一般的で かつ、もし このループに差し掛かった場合のキャッシュの持ち方も比較的一般的な方 であれば ・インライン化で必ずしも高速化するとは限らない。 ・キャッシュラインとジャンプ距離とかのことを考慮して、大きすぎるようなら読み替えなしの手もありかもしれない。 ・といっても、元々それほどは大差というわけでもないので、対象のCPUがかなり限定できない状況では、ある程度その場の気分でもいいかもしれない。 ・将来(未知のCPU)のことを仮定するなら余計「読み替え」は「最終的な、奥の手」的な感じでもいいかもしれない。 ・既に書き変えてしまっている場合は、とりあえず、現状はそのままでもいいかな。 ということになってきそうですね。 まぁ、全く間違ってるかもしれませんがw 個人的には調べまくった結果相当色々勉強になったので、現時点で解決で良いんですがw 「上記解釈は明らかに間違ってる」っていう場合は、分かる方いらっしゃいましたら教えてください。 その点で、すぐ締め切らない方が良いと思うので しばらく待たせてください。