HOME » 技術情報ブログ » C » C/C++ のコンパイル時最適化に注意する
技術情報ブログ
技術情報ブログ
C/C++ のコンパイル時最適化に注意する
2022/08/19
はじめまして、富山オフィスの田財です。
私は普段 C++ を利用してシステム開発することが多いので、これに関連したテーマにしたいと思います。
テーマは「C/C++ のコンパイル時最適化に注意する」です。
まず、C/C++を含むプログラミング言語は、言語仕様に従ったソースコードから、
機械語あるいは元のプログラムよりも低い水準のコードに
変換(コンパイル)するコンパイラが存在します。(コンパイラ(Wikipedia)より引用)
また、C/C++の一般的なコンパイラには「最適化」という機能が備わっています。
ソースコードをコンパイルする際に、より高速で動くプログラムとなるように
ソースコードの示す結果が変わらない範囲で最適化してコンパイルする、という機能です。
今回はこのコンパイル時最適化についてのお話ですが、
テーマの本題に入る前に、まずはこの最適化がどのような機能なのか、説明します。
例を1つ提示してみます。
上記のソースコードは、10 の階乗に 0 ~ 9 を順に掛けて、
都度標準出力する単純なものです。
実は、このソースコードには、効率の悪い処理となっている部分があります。
プログラミング経験のある人であれば一目でわかるかもしれませんが、
の部分です。
このソースコードだと、i が 0 ~ 9 の範囲で順に 1 ずつ増えていく間に
何度も 10 の階乗を計算しているため、効率が悪いです。
最適化案としては、「上記の部分を for (int i = 0; i < 10; i++) {} の外に出す」でしょうか。
こんなイメージです。
これであれば、10の階乗の計算を最初に一度行うだけで済みますね。
では、実際にコンパイラに最適化してもらいましょう。
VisualStudio 2015 で、 /O2 で最適化した結果を逆アセンブルで見てみます。
注目は
の部分です。
・10 の階乗を何度も計算する必要が無いこと
・この計算結果は常に変わらないこと
を見事看破したコンパイラは、コンパイル時に
の部分をあらかじめ計算してしまい、x375F00(10進数で 3,628,800) という数字をそのまま使った処理にしています。
この数字を使って、出力に使用する変数が x229B600(10進数で 36,288,000) になるまで都度足しては出力する、
という内容に最適化したようです。
最適化した結果をソースコードに直して比較すると、以下のようになるでしょうか。
○最適化前
○最適化後
こちらが想定していた最適化案よりも、さらに効率が良いですね!
このように、コンパイラの最適化はとても賢いです。
コンパイラの最適化機能はどんどん発展しており、プログラマがあまり意識せずとも
高速に動作するプログラムが作成できるようになってきています。
さて、ようやく本題です。
上の例で示した通りとても賢い最適化機能ですが、一方で、
この機能によってプログラマが意図しないプログラムになってしまうこともあり、注意が必要です。
今回は、その1例を紹介します。
例えば、以下のようなソースコードがあります。
パスワードを char pwd に代入して操作し終わった後、
速やかに memset で 0 初期化するソースコードです。
いつまでもメモリ上に残存していることは、セキュリティ上リスクがあるためにこのようにしています。
さて、これが最適化されるとどうなるでしょうか。
先ほどと同じく、VisualStudio 2015 で、 /O2 で最適化した結果を逆アセンブルで見てみます。
memset の下に命令が何もありません。
メモリを覗いてみると、return 0 まで辿りついても当然、パスワード("abcd")が残ってしまっています。
これはどういうことでしょうか。
pwd は if (GetPassword(pwd)) {} 以降一切利用されない変数です。
コンパイラ目線で見れば、プログラムが終了したら勝手に開放されるのだから、
今後利用されない変数 pwd をわざわざ 0 初期化しなくても良いはずだ、と判断できます。
そのため、最適化によって memset~ の一文はコンパイルせずにスキップしてしまっています。
このように、プログラマの意図しない最適化により、セキュリティ上リスクのある
プログラムになってしまったり、想定しない動作を引き起こすことがあるのです。
今回の例の回避策として、例えば以下のようにします。
最適化結果を逆アセンブルで見てみます。
今回はちゃんと命令がありますね。
メモリを覗いてみると、
のタイミングで、1変数ずつ0初期化されていっています。
↓
無事、すべて 0 初期化されました。
Windows API には、最適化によってスキップされないことを保証する
ZeroMemory(), SecureZeroMemory() があります。
これらを使うことで、意図しないスキップを回避することが出来ます。
コンパイル時最適化には、他にもプログラマが意図しない結果を引き起こすものがあります。
他の事例は機会があればまたいずれ。
ハイテックスではこのような事例も考慮して、日々開発を行っています!