ログイン

2016年3月 弊社ホームページは新しくなりました。 https://thinkridge.com

メインメニュー

携帯公式サイト


携帯電話をもっと便利に
もっと楽しく


史上初の吹奏楽専門着メロサイト


POPで癒しでライトでとんがって気持ちのいい〜オルゴール着メロをあなたに

Magome

クラウドベースの MIDI シーケンサ Magome

音楽制作に興味のある方を対象に、スタンドアロンでも使え、ネットならではの面白さも兼ね備えた音楽制作アプリの提供を目指しています

for 携帯電話

https://thinkridge.com/m/
ケータイはこちらへ

最適化オプション"エイリアスを使わないと仮定する"の意味を熟考すべし

VC++には(それ以外のコンパイラにもきっと)、"エイリアスを使わないと仮定する"(/Ow,/Oa)という最適化オプションが用意されています。

このオプションは、"サイズ優先"とか"実行速度優先"という最適化にしても有効にはならず、明示的に有効にしないといけないものなので、恩恵に預かっているケースは少ないと思います。

まずリスト1に例を挙げます。上段がC++のソースコード。下段がコンパイラが出力したアセンブラコードです。 

List1

ソース
void MemClear(int *pSize,char *pBuffer)
{
    for(int i=0; i<*pSize; i++ ){
        pBuffer[i] = 0;
    }
}
出力
void MemClear(int *pSize,char *pBuffer)
{
    mov  ecx, pSize
    xor  eax, eax
    cmp  DWORD PTR [ecx], eax
    jle  SHORT LABEL2
LABEL1        // ループ戻り先(このラベルは次のmovの下でいい気がする…)
    mov  edx,pBuffer           // クリアするバッファを取得
    and  BYTE PTR [eax+edx],0  // 1バイトクリア
    inc  eax                   // カウンタをインクリメント
    cmp  eax, DWORD PTR [ecx]  // クリアサイズとカウンタを比較して
    jl   SHORT LABEL1          // まだ途中ならLABEL1に戻って繰り返す
LABEL2
    ret
}

このコードは一見してわかるように、指定された領域のメモリをクリアする関数です。
出力されたコードをみると、コンパイラはとても素直に最適化されていないコードを吐き出していることがわかります。

今回の最適化オプションは、このような繰り返し処理を効率良いコードにする為の物です。リスト2。 

List2

ソース
// "エイリアスを使わないと仮定する"を有効
#pragma optimize ( "a", on )

void MemClear(int *pSize,char *pBuffer)
{
    for(int i=0; i<*pSize; i++ ){
        pBuffer[i] = 0;
    }
}

#pragma optimize ( "", on ) // 元に戻す
出力
void MemClear(int *pSize,char *pBuffer)
{
    mov     eax, pSize
    mov     ecx, DWORD PTR [eax]
    test    ecx, ecx
    jle     SHORT LABEL1
    mov     edx, ecx
    push    edi
    mov     edi, pBuffer
    xor     eax, eax
    shr     ecx, 2   // クリアサイズ(の1/4。32BITで処理する為)
    rep stosd        // 一気にクリア
    mov     ecx, edx
    and     ecx, 3   // クリアサイズ(の4で割った余り)
    rep stosb        // 一気にクリア
    pop     edi
LABEL1
    ret
}

リスト1と比べると、繰り返し処理(条件分岐)も無くなっており、見るからに早そうです。


本ネタで重要だと思うのは以下になります。


なぜ、リスト1はリスト2のように最適化されないかというと、
コンパイラは、繰り返しの条件であるクリアサイズ(*pSize)が、pBufferの示すバッファに値を書き込むことで、書き換えられてしまうかもしれない。
と考えるからです。

リスト2は、"エイリアスを使わないと仮定する"としているので、コンパイラは、pSizeとpBufferの領域がダブることはない。という前提があるので最適化されたコードを出力出来ます。

ここまでの説明だと、"エイリアスを使わないと仮定する"を常に有効にすることで、何も考えずに今以上に最適化されるのではないか。と思ってしまうかもしれませんが、それは危険です。
むやみやたらに最適化させると、ソースコード的には正しいけれど、その通りに動いてくれないコードが出力される可能性があります。


僕の個人的意見では、"エイリアスを使わないと仮定する"は無効にしたまま、ソースコード上で最適化される書き方をするという手段が吉だと思います。
例えばリスト3のようにします。 

List3

ソース

void MemClear(int *pSize,char *pBuffer)
{
    int nSize = *pSize;  // サイズをローカル変数にコピー
    for(int i=0; i<nSize; i++ ){
        pBuffer[i] = 0;
    }
}
出力
(リスト2と同じコードなので割愛)

最初にクリアサイズ(*pSize)をローカル変数にコピーして、そのローカル変数がforを抜ける条件であると書くことで、
pBufferへの書き込みによってループ条件が変わることはないので、コンパイラは最適化されたコードを出力できます。


以上のことから、自分の個人的意見を言わせてもらうと、
"エイリアスを使わないと仮定する"最適化オプションは使わないほうが吉。
使うことで最適化されるケースがあるのであれば、コードの書き方を工夫することで最適化するのが吉。

"エイリアスを使わないと仮定する"最適化オプションの利用法としては、
無効の場合と有効の場合とで、出力されるアセンブラを比較して、
もし有効にすることで最適化されるようなケースがあったなら、そこはまだコードの書き方を工夫できる。
という判断材料として"エイリアスを使わないと仮定する"を使用するのが吉と思われます。

/*
最近のコンパイラは優秀だと改めて思いました。
for文でメモリクリアって、昔は rep stosd とか使ってくれなかった気がします。
今回の例であるメモリクリアであれば memset とか使うのが常識なのですが、memsetだとBYTE値でしか埋めれないので、16Bit値や32Bit値で埋めたいときは for ループを使うしかないのですが、その場合でも rep stosd とか使ってくれるようになってました。

なお、かといって memset を使わずに for でメモリクリアを実装しなきゃならないかというと、一概にはそう言えません。
VC++(他のコンパイラは良く知りません)には、組み込み関数という仕組みがあって、memset は組み込み関数として用意されていますので、memset を使うことによるパフォーマンスの低下はまず無いでしょう。(組み込み関数を有効にする必要があります)
*/

プリンタ出力用画面

前のページ
メンバ変数の並びを考慮すべし
コンテンツのトップ 次のページ
try 〜 throw 〜 catch の乱用を避けるべし