こんにちはゲストさん。会員登録(無料)して質問・回答してみよう!

解決済みの質問

メンバとローカル変数のパフォーマンスについて

実験用に以下のstructを作りました。

struct Widget {

double data[32];
int a, b, c;

Widget();

__forceinline int abs_(int) const;
Void func( double* );

};


/////////ソース/////////


Widget::Widget() : a(1), b(2), c(52000) {
for ( int i=32;i--; ) data[i] = 0.8649 * ( i & 1 ? -i : i );
}

__forceinline int Widget::abs_( int i ) const { return i < 0 ? -i : i; }



そしてfuncの内容について

Void Widget::func( double* const d ){

for ( int i = 32; i--; ){
c -= int( data[i] );
b = -int( abs_(b)*0.999 + 1.5 );
d[i] = a * d[i] + d[i&3] / b + c * data[i];
}

}

のパターンと

Void Widget::func( double* const d ){

const double* const data_ = data;
const int a_ = a;
int c_ = c;
int b_ = b;

for ( int i = 32; i--; ){
c_ -= int( data_[i] );
b_ = -int( abs_(b_)*0.999 + 1.5 );
d[i] = a_ * d[i] + d[i&3] / b_ + c_ * data_[i];
}

c = c_;
b = b_;

}

のパターンを使い
(同じコンパイラ・コンパイルオプションならどちらのfuncでも数値は必ず同じになります。(のはず))

それぞれ100万回呼び出したところ
比較的安定して0.01~0.02秒程度の差が発生しました。

それは後者の方が速いという意味です。


アセンブリ出力を見てみると
(アセンブリの行数的にはもちろん前者の方が短いです)

全部読み解いてはいませんが
ざっと見た感じ

前者では
; _this$ = ecx
というコメントがあり
これを

movedi, ecx
moveax, edi

などとした後に
edi やeaxレジスタを使っているように見えました。

対して後者は

ebpレジスタを使っているように見えました。

要するに
前者では毎回thisポインタ経由でやってて
後者ではローカル変数なのでスタックのアドレスを使って操作してる

という感じだと思うのですが

確かにコードからこれは妥当だとも思うのですが

上記の場合、abs_のインライン(だいたいconstメンバ関数ですし)を除き
別途関数を呼び出したりしていません。

つまり、今回の実験においては、このfunc関数実行中
func関数外でメンバが書き変わるということはないという事になっています。


こういう場合
前者の書き方でも最適化によって後者の効果が得られる
という事があると便利な場合もあるんではないかと思うのですが

・スタックの消費量が意図せず膨張すると困るかもしれない
・関数の呼び出しの確認などが広範に必要で、コンパイラの負荷がかなり増えるかもしれない
・そういえばマルチスレッドを考えると、同期がプログラマ委託である限り不可能かな(これは最難関?)

などの「それは難しすぎる」という理由も考えられます。

現状では
thisポインタ経由を自動的にスタック操作に書き換える
といったことはどのコンパイラでも行わないのでしょうか?


仮に、そうなのであれば
やはり「ここぞという多大な演算の最深部」の関数とかでは
スタックの消費が異常にならないような配慮はしつつ

こういう風にローカル変数に読み直した方がいいと考えて良いでしょうか?

投稿日時 - 2012-02-13 18:41:36

QNo.7303173

困ってます

質問者が選んだベストアンサー

ん~, そこまでの最適化はできないんじゃないですかねぇ.... 言われるように, マルチスレッドを考えると無理. そうでなくても
c -= int( data[i] );
における -= の副作用は ; の直後で確実に適用していなきゃならない. この場合の「副作用」は「メンバ変数の値を変更する」ことだから, この文が終わった時点でメンバ変数の値が変わっていないとおかしい.

投稿日時 - 2012-02-14 13:30:04

補足

さらなる経過報告です。

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

「上記解釈は明らかに間違ってる」っていう場合は、分かる方いらっしゃいましたら教えてください。

その点で、すぐ締め切らない方が良いと思うので
しばらく待たせてください。

投稿日時 - 2012-02-16 05:55:32

お礼

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

う~ん、もうひとつ何か欲しいところですね。
「この検証に対してはコンパイルオプションが不完全」とかだと一番楽なんですが

投稿日時 - 2012-02-14 15:05:31

ANo.2

このQ&Aは役に立ちましたか?

0人が「このQ&Aが役に立った」と投票しています

回答(2)

ANo.1

プロセッサのアーキテクチャやコーリングコンベンション、そして最適化性能によります。

投稿日時 - 2012-02-13 23:04:34

補足

おっと


メンバ関数って__stdcallとかって指定していいんでしたっけ?



非staticなメンバ関数って__stdcallとかって指定していいんでしたっけ?


です。

投稿日時 - 2012-02-14 01:37:44

お礼

どうも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以降用で
何かこう「現状これが良さ気」って言う決定打ってないものでしょうか。

投稿日時 - 2012-02-14 01:20:19

あなたにオススメの質問