multi-opts - gccの-Oオプションを複数個付けたらどうなるか?

目的

"gcc -O2 -O0" のように複数最適化オプションを付けた場合に、ちゃんと -O0 だけが有効になるかを確認したい。

結果

アセンブル結果を比較して、確かに、最後に付けた -O* だけが有効になっていた。

以下、詳細。

背景と動機

大きなツリーで共通のMake変数定義を使っていて、その中に "-O2" が入っているとしよう。とあるディレクトリでだけ最適化をオフにするために引数の最後に "-O0" を付けたのだが、本当にそれで最適化がオフになっているのか、ちょっと不安。確認しておきたい。

gccのドキュメントでは、一応「最後のオプションだけが有効になる」と書いてあった。

If you use multiple -O options, with or without level numbers, the last such option is the one that is effective. 

from 3.10 Options That Control Optimization(Optimize Options - Using the GNU Compiler Collection (GCC))。
ただ、gccの機能はバージョンによって変わることが多いから、念の為確認してみる。

確認方法

最適化状況によってコンパイル結果(assemblerソース)が異なるソースを用意して、オプションを変えながら結果を比較する。

使用するソース

check-optimize.c
https://sssvn.jp/svn/spikelet/c/gcc-O-multiple/check-optimize.c
static int func1(void) { return 1; }
int func2(void) { return 2 + func1(); }
int main(int argc, char *argv[])
{
  int i=0;
  int val;

  for (i=0; i<10; i++) {
    val = 5;
  }

  val += func1() + func2();
  return 0;
}

最適化の違い

ざっくりと言うと、以下のように最適化が進んでいた:

  • -O0: 最適化無し
  • -O1: 定数を返す関数を定数に展開、変数をレジスタに割当て
  • -O2: forループの向きを加算(inc)から減算(dec)に変更、0クリアをxorで実行
  • -O3: さらなる関数の展開と、展開して不要になったstatic関数削除
  • -Os: 関数展開を抑制してコード全体量を抑える

詳細: gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-8) の場合

最適化 "-O0" "-O1" "-O2" "-O3" "-Os"
func1の存在 あり あり あり なし あり
func1の呼出し(mainから) あり なし なし なし なし
func1の呼出し(func2から) あり あり あり なし あり
func2の存在 あり あり あり あり あり
func2の呼出し(mainから) あり なし なし なし なし
変数格納場所 主記憶 レジスタ レジスタ レジスタ レジスタ
mainのforループ あり あり あり あり あり
ループの向き 0から10 0から10 10から0 10から0 10から0
`0'の格納方法 即値をmov 即値をmov xor xor xor

詳細: 某クロスコンパイラの場合(どちらのバージョン(謎)とも)

最適化 "-O0" "-O1" "-O2" "-O3" "-Os"
func1の存在 あり あり あり なし あり
func1の呼出し(mainから) あり なし なし なし なし
func1の呼出し(func2から) あり あり あり なし あり
func2の存在 あり あり あり あり あり
func2の呼出し(mainから) あり なし なし なし あり
変数格納場所 主記憶 レジスタ レジスタ レジスタ レジスタ
mainのforループ あり あり なし なし なし
ループの向き 0から10 0から10 - - -
`0'の格納方法 クリア命令 クリア命令 クリア命令 クリア命令 クリア命令
関数出入口でのスタック操作 あり あり あり なし あり

確認用script

最適化オプションを一つだけ付けた場合と、ごちゃごちゃと前に付けた場合でそれぞれ出力されるアセンブラコードを比較するスクリプトを作った:

do_test.sh
https://sssvn.jp/svn/spikelet/c/gcc-O-multiple/do_test.sh
#!/bin/sh

CC=${1-gcc}
RANDOM_ARGS='-O2 -O3 -O0 -Os'

cfile=check-optimize.c
asm1=single-arg.s
asm2=multi-arg.s

trap 'rm -f $asm1 $asm2' 0

for opt in -O0 -O1 -O2 -O3 -Os
do
  echo -n "trying $opt ... "
  $CC -S $opt -o $asm1 $cfile
  $CC -S $RANDOM_ARGS $opt -o $asm2 $cfile
  if cmp $asm1 $asm2
  then echo "same."
  else echo "diff!!"
  fi
done

一つだけオプション版と、ごちゃごちゃオプション版を -S でアセンブラ出力して、それを cmp で比較する。

実行結果

RHEL4のgccでは、こうなった:

% gcc --version|head -1
gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-8)
% ./do_test.sh
trying -O0 ... same.
trying -O1 ... same.
trying -O2 ... same.
trying -O3 ... same.
trying -Os ... same.

某クロスコンパイラの場合も、どちらのバージョン(謎)でも同じ結果になった。
また、gcc-2.95からgcc-4.2.3 までの各バージョンでも、同じ結果(gcc-4.3.0はなぜかビルドに失敗中)。

これで最後に付けた -O* だけが有効になっていることが確認できた。

おまけ:ソースを見てみる

gcc-4.2.3の場合。

gcc/opts.c:decode_options() の中で、-O* のオプションの解析をしている。
argvを順番に見ていって、 "-O" で始まるオプションなら、変数optimize_sizeとoptimizeを設定。
optimizeには、-Oの後ろに指定した数値が入る…"-O99" とかも有効なんだね(ただし3以上は3と同じ意味)。
argv処理の後に、optimizeの値に応じて、グローバル変数の flag_* を設定したり、set_param_value() して、コンパイラの振舞いを設定している。

今回のポイントは、「argvを順番に見ていって」いる箇所で、これなら確かに最後に指定した -O* だけが有効になる。