メニュー
ブログ更新履歴
コンテンツ更新履歴
リンク
  • Magome
  • クラウドベースのMIDIシーケンサ
    音楽制作に興味のある方を対象に、スタンドアロンでも使え、ネットならではの面白さも兼ね備えた音楽制作アプリの提供を目指しています。
twitter

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 を使うことによるパフォーマンスの低下はまず無いでしょう。(組み込み関数を有効にする必要があります)
*/


Front page   Freeze Diff Backup Copy Rename ReloadPrint View   New Page Page list Search Recent changes   Help   RSS of recent changes (RSS 1.0) RSS of recent changes (RSS 2.0) RSS of recent changes (RSS Atom) Powered by xpWiki
Counter: 435, today: 1, yesterday: 0
Princeps date: 2005-12-25 (Sun) 08:43:00
Last-modified: 2005-12-25 (Sun) 08:43:00 (JST) (1116d) by takatsuka