GCCのおすすめ最適化オプション

2019年4月17日

プロセッサー

GCC(ジーシーシー)とはGNU Compiler Collection(グヌーコンパイラコレクション)の略で、GNUプロジェクトのコンパイラの集まりです。

GNU(グヌー)とは全てフリーソフトウェアからなるOS(オペレーティングシステム)およびコンピュータ用ソフトウェアの集まり(コレクション)のことです。

ソースコードをコンパイルするときにgccを利用していることも多いと思いますので、おすすめの最適化オプションをご紹介したいと思います。

最適化オプション

-O2または-O3

O2は広くコンパイル時に指定されることが多く、ほとんどの環境で効果のある基本的な最適化に加えてコードサイズが増加してしまう各種最適化を行います。

O3はO2の最適化に加えてベクトライズ化(-ftree-vectorizeなど)やインライン展開(-finline-functionsなど)などのさらなる積極的な最適化が行われ、環境やコードの作りによっては実行効率がよくなりますが、逆に遅くなったり不具合がでたりすることもありますので注意が必要です。

GCCのバージョンによってO2とO3を指定したときに適用される最適化は以下のコマンドで調べることができます。

gcc -O2 -Q --help=optimize
gcc -O3 -Q --help=optimize

O2とO3で適用される最適化の差分は上記コマンドの結果をdiffコマンドなどで比較すると分かりやすいかもしれません。

gcc -O2 -Q --help=optimize >o2.txt
gcc -O3 -Q --help=optimize >o3.txt
diff o2.txt o3.txt

最適化処理に詳しくてカリカリにチューニングを施したい場合を除いて、ほとんどの環境でO2を指定しておくのが無難です。

-march=native

通常marchにはi686やcore2、skylake-avx512などといったCPUの種類を指定(詳しくはman gccなどで参照して下さい)しないといけませんが、nativeを指定すると現在のCPUに合った指定を自動でしてくれます。

バイナリを他の環境で使用する予定であるならば特定のCPUに最適化してはいけませんが、コンパイルした環境でしか使用しないのであれば最適化の効果が期待できます。

-march=nativeで指定されるオプションは以下のスクリプトで調べることができます。

#!/bin/sh
CC="gcc"
OPT="-march=native"
NATIVE=$(echo | ${CC} -E -v ${OPT} - 2>&1 | grep cc1)
NOARCH=$(echo | ${CC} -E -v - 2>&1 | grep cc1)
for native in ${NATIVE} ; do
        FOUND=0
        for noarch in ${NOARCH} ; do
                if [ "${native}" = "${noarch}" -a "${native}" != "${OPT}" ] ; then
                        FOUND=1
                        break
                fi
        done
        if [ ${FOUND} -eq 0 ] ; then
                echo -n "${native} "
        fi
done
echo

スクリプト参照元:gcc -march=nativeが生成する最適化オプション

※複数バージョンのgccが混在している環境などでは、変数CCにCC="gcc-6.5.0″などで調べたいgccコマンドを設定して下さい。

-mssse3 -mfpmath=sse

SSE有効化のオプションと演算にSSEを使用するようにするオプションです。

-mssse3はCPUによって-msse2,-msse3,-mssse3,-msse4.1,-msse4.2,-msse4など変わりますので注意して下さい。(詳しくはman gccなどを参照)

x64(64bit)環境だと自動的に-msse2と-mfpmath=sseは有効になっており演算はSSEを使用するようになっているのですが、x86(32bit)環境だとFPU(i387)を使用するようになっています。

FPUを使用する演算はSSEを使用しての演算に比べて遅いですので、x86環境でSSE2以降が使用できるCPUであれば演算速度が向上します。

またi387とのやりとりで発生する計算精度(ビット数)の違いによる丸め誤差もおきなくなります。

-fexcess-precision=fast

計算手順を古いgccに合わせて簡略化するオプションです。

最近のgccはデフォルトだと計算手順を仕様どおり厳密に行うようになっており、手順が省略できない分旧仕様のgccに比べてパフォーマンスが落ちています。

このオプションをしていすると計算手順が旧gcc仕様の簡略化されたものになり、結果として全体的にパフォーマンスが向上します。

ただし厳密な計算結果を要する科学技術計算ようなコードに適用する場合は注意が必要です。

-ffast-math

このオプションを指定するとgccは演算の誤差を全く気にせず最適化を行うようになります。

演算が高速化する代わりに不具合が起きやすくなりますので、使用できるコードは限定的です。

汎用的に使用できる最適化ではありませんが、実験を繰り返して不具合が出ないことを確かめてカリカリにチューンする際には試してもよい最適化オプションになります。

-funroll-loops

ループ展開を行うオプションです。

ループ展開とは簡単に言うとループ中の処理を繰り返し回数分ならべて実行するように変更するということです。

このオプションは効果がある場合もありますが、コードサイズが大きくなるだけで効果がないかパフォーマンスが低下することもありますので、実験して効果を確かめて指定する必要があります。

-flto

LTO(Link Time Optimization)を行うようにするオプションです。

通常ソースファイル単位で行っている最適化をリンク時に全体を見て最適化をかけることで、さらなるパフォーマンスアップが期待できます。

ただし莫大なリンク時間がかかる割に効果が薄く、リンク時にエラーが起きることもおおくて、通常このオプションを使うのは時間的余裕がないと困難かもしれません。

チューニングの可能性を追い求める人には指定する価値のあるオプションの一つだと思います。

-fopenmp

このオプションを指定するとOpenMPが有効になり、コード次第ですが処理が並列化されます。

並列処理で複数コアを効率よく使い切ればパフォーマンスアップが期待できますが、完全にコード次第です。

OpenMPに対応したコードや対応できそうなコードであれば有効化する価値があります。

最適化オプションというよりは並列化技術ですので、興味を持たれた方はOpenMP関連を調べて詳しく調べてみるとよいと思います。

関連する主な環境変数

CC

使用するCコンパイラのコマンドを指定できます。

CXX

使用するC++コンパイラのコマンドを指定できます。

LD

使用するリンカのコマンドを指定できます。

AR

使用するarコマンドを指定できます。

NM

使用するnmコマンドを指定できます。

RANLIB

使用するranlibコマンドを指定できます。ranlibはarの別名です。

CFLAGS

Cコンパイラに渡すオプションを指定できます。

CXXFLAGS

C++コンパイラに渡すオプションを指定できます。

LDFLAGS

リンカに渡すオプションを指定できます。

CPPFLAGS

プリプロセッサに渡すオプションを指定できます。

環境変数指定の例

souceコマンドを使用して以下の内容が記述されたファイルを読み込んで使用しています。(上記で説明していないオプションや環境変数がありますが割愛)

unset CC CXX AR AS NM RANLIB LD STRIP CFLAGS CXXFLAGS CPPFLAGS LIBCFLAGS LIBCXXFLAGS LDFLAGS USE
#TUNE='-O2 -march=native -mssse3 -mfpmath=sse -fexcess-precision=fast -fno-aggressive-loop-optimizations -Wl,--hash-style=both -pipe'
#TUNE='-O2 -march=native -mssse3 -mfpmath=sse -fexcess-precision=fast -ftree-vectorize -Wl,--hash-style=both -pipe'
TUNE='-O2 -march=native -mssse3 -mfpmath=sse -fexcess-precision=fast -pipe'
CFLAGS="${TUNE}"
CXXFLAGS="${CFLAGS}"
GCC_OP_TYPE=sse
export CFLAGS CXXFLAGS GCC_OP_TYPE

ラッパースクリプト

最適化オプションを指定してコンパイルしたいにも関わらず、CFLAGSを見てくれない場合などに使用する自前で作成して使用しているラッパースクリプトです。

自己責任において自由に使用して頂いて構いません。

/usr/local/bin/gccなどに作成して実行権を付け、CCに指定して使用します。

GCC_NAMEは環境によって調整して下さい。

/usr/local/binにパスが通っていて優先順位が高いのであればCCに指定する必要はない場合があります。

環境変数GCC_OP_TYPE=xxxの内容に応じて/etc/gcc.option.xxxのオプションが指定されます。(未指定なら/etc/gcc.option)

#!/bin/sh
GCC_NAME=`basename $0`-6
GCC_OP_FILE=/etc/gcc.option
if [ -n "$GCC_OP_TYPE" ] && [ -r $GCC_OP_FILE.$GCC_OP_TYPE ]; then
        . $GCC_OP_FILE.$GCC_OP_TYPE
else
        . $GCC_OP_FILE
fi
for ARG in "$@"
do
case $ARG in
-march=* | -mtune=* | -mcpu=* | '' )
;;
-O0 )
O_LV=0
;;
-O | -O1 )
O_LV=1
;;
-O2 )
#O_LV=2
;;
-O3 )
O_LV=3
;;
-Os )
O_LV=s
;;
-Ofast )
O_LV=fast
;;
-Og )
O_LV=g
;;
* )
GCC_ARGS="$GCC_ARGS $(printf %q "$ARG")"
;;
esac
done
O_ARG=" -O$O_LV "
CMD=$GCC_NAME$O_ARG$GCC_ARGS
#ulimit -s 32768
eval $CMD
#echo $CMD >>gcc_cmd.log

-Oオプションが指定されていないとO2指定となりますが、他のレベルが指定されていた場合は指定されたレベルに従います。

O_LV=の部分を書き換えれば強制的にレベルを指定可能です。

またmarch指定は捨てて最終的にgcc.optionが適用されます。(gcc.option.xxxで指定されたmarch指定になる)

/etc/gcc.option.sseの例

O_LV=2
GCC_ARGS='-march=native -mssse3 -mfpmath=sse -fexcess-precision=fast'

O_LV=2はO2指定となります。

まとめ

gccの最適化オプションの一部をご紹介しましたがいかがでしたでしょうか?

最適化やチューニングは奥が深く時間がかかりますが、深く追求していくと結構楽しいものです。

Linuxディストリビューションなどで、コンパイルされたパッケージでソフトウェアを入れるのはサーバの管理上楽でありメンテナンス性に優れますが、パフォーマンスでは自前で最適化オプションを付けてコンパイルしたものに劣ります。

ぜひ自分の環境にあった最適化オプションを探してみて下さい。