技術情報ブログ
技術情報ブログ コーディング
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 で最適化した結果を逆アセンブルで見てみます。
![20220819-1.png](https://hitechs.co.jp/blog/2022/08/19/20220819-1.png)
注目は
![20220819-2.png](https://hitechs.co.jp/blog/2022/08/19/20220819-2.png)
の部分です。
・10 の階乗を何度も計算する必要が無いこと
・この計算結果は常に変わらないこと
を見事看破したコンパイラは、コンパイル時に
の部分をあらかじめ計算してしまい、x375F00(10進数で 3,628,800) という数字をそのまま使った処理にしています。
この数字を使って、出力に使用する変数が x229B600(10進数で 36,288,000) になるまで都度足しては出力する、
という内容に最適化したようです。
最適化した結果をソースコードに直して比較すると、以下のようになるでしょうか。
○最適化前
○最適化後
こちらが想定していた最適化案よりも、さらに効率が良いですね!
このように、コンパイラの最適化はとても賢いです。
コンパイラの最適化機能はどんどん発展しており、プログラマがあまり意識せずとも
高速に動作するプログラムが作成できるようになってきています。
さて、ようやく本題です。
上の例で示した通りとても賢い最適化機能ですが、一方で、
この機能によってプログラマが意図しないプログラムになってしまうこともあり、注意が必要です。
今回は、その1例を紹介します。
例えば、以下のようなソースコードがあります。
パスワードを char pwd に代入して操作し終わった後、
速やかに memset で 0 初期化するソースコードです。
いつまでもメモリ上に残存していることは、セキュリティ上リスクがあるためにこのようにしています。
さて、これが最適化されるとどうなるでしょうか。
先ほどと同じく、VisualStudio 2015 で、 /O2 で最適化した結果を逆アセンブルで見てみます。
![20220819-3.png](https://hitechs.co.jp/blog/2022/08/19/20220819-3.png)
memset の下に命令が何もありません。
メモリを覗いてみると、return 0 まで辿りついても当然、パスワード("abcd")が残ってしまっています。
![20220819-4.png](https://hitechs.co.jp/blog/2022/08/19/20220819-4.png)
これはどういうことでしょうか。
pwd は if (GetPassword(pwd)) {} 以降一切利用されない変数です。
コンパイラ目線で見れば、プログラムが終了したら勝手に開放されるのだから、
今後利用されない変数 pwd をわざわざ 0 初期化しなくても良いはずだ、と判断できます。
そのため、最適化によって memset~ の一文はコンパイルせずにスキップしてしまっています。
このように、プログラマの意図しない最適化により、セキュリティ上リスクのある
プログラムになってしまったり、想定しない動作を引き起こすことがあるのです。
今回の例の回避策として、例えば以下のようにします。
最適化結果を逆アセンブルで見てみます。
![20220819-5.png](https://hitechs.co.jp/blog/2022/08/19/20220819-5.png)
今回はちゃんと命令がありますね。
メモリを覗いてみると、
![20220819-6.png](https://hitechs.co.jp/blog/2022/08/19/20220819-6.png)
のタイミングで、1変数ずつ0初期化されていっています。
![20220819-7.png](https://hitechs.co.jp/blog/2022/08/19/20220819-7.png)
↓
![20220819-8.png](https://hitechs.co.jp/blog/2022/08/19/20220819-8.png)
無事、すべて 0 初期化されました。
![20220819-9.png](https://hitechs.co.jp/blog/2022/08/19/20220819-9.png)
Windows API には、最適化によってスキップされないことを保証する
ZeroMemory(), SecureZeroMemory() があります。
これらを使うことで、意図しないスキップを回避することが出来ます。
コンパイル時最適化には、他にもプログラマが意図しない結果を引き起こすものがあります。
他の事例は機会があればまたいずれ。
ハイテックスではこのような事例も考慮して、日々開発を行っています!
C++ CString 文字列比較ってどれが速いの?
2022/05/11
はじめまして、富山オフィスの小坂です。
今年の4月で入社11年目になりました。
自分はまだまだ若い気持ちでいますが、新入社員の方を見ると歳を感じますね...。
さて、久しぶりではありますが技術ブログの更新です。
前回の更新が5年前のようで...スミマセン。
今年の4月で入社11年目になりました。
自分はまだまだ若い気持ちでいますが、新入社員の方を見ると歳を感じますね...。
さて、久しぶりではありますが技術ブログの更新です。
前回の更新が5年前のようで...スミマセン。
復帰第1回目のテーマは...
「C++ CString 文字列比較ってどれが速いの?」
「C++ CString 文字列比較ってどれが速いの?」
Windowsのアプリ開発でよく使われているライブラリと言えば「MFC」が思い浮かぶと思います。(もしくはATL)
更にその中で、文字列を扱うクラスと言えば「CString」がありますね。
サイズを気にせず代入や結合が簡単にでき、フォーマットや文字列比較もできるという、至れり尽くせりな便利クラスです。
ということで、今回は「CString」について紹介していきたいと思います。
といっても全部紹介していると日が暮れるので「文字列比較」、更にはその「処理速度」にスポットを当てて話をしたいと思います。
更にその中で、文字列を扱うクラスと言えば「CString」がありますね。
サイズを気にせず代入や結合が簡単にでき、フォーマットや文字列比較もできるという、至れり尽くせりな便利クラスです。
ということで、今回は「CString」について紹介していきたいと思います。
といっても全部紹介していると日が暮れるので「文字列比較」、更にはその「処理速度」にスポットを当てて話をしたいと思います。
文字列比較でよく見るコードは以下のような比較演算子を使った形ですね。
上記のようなコードでもプログラム的に問題はありません。
しかし処理速度の観点から見ると、もっと良いコードが書けます。
ではどのようなコードが速いのか?
検証を行ってみたので、その結果を紹介したいと思います。
- CString str1 = _T("abc");
- CString str2 = _T("123");
- bool bSame = false;
- if (str1 == str2) {
- bSame = true;
- }
上記のようなコードでもプログラム的に問題はありません。
しかし処理速度の観点から見ると、もっと良いコードが書けます。
ではどのようなコードが速いのか?
検証を行ってみたので、その結果を紹介したいと思います。
【検証内容】
以下の文字列の比較を 1,000,000回 行った速度を計測。
Vdk4jQnifyheeUVSJMAD2FdUXyCfR9HRTACieCMBVDguC3ALbU
vDK4JqNIFYHEEuvsjmad2fDuxYcFr9hrtacIEcmbvdGUc3alBu
使用した関数は以下の4つ。
Vdk4jQnifyheeUVSJMAD2FdUXyCfR9HRTACieCMBVDguC3ALbU
vDK4JqNIFYHEEuvsjmad2fDuxYcFr9hrtacIEcmbvdGUc3alBu
使用した関数は以下の4つ。
- Cstring::Compare
- Cstring::CompareNoCase
- _tcscmp
- CStringの等価演算子(==)
【検証コード】
比較結果をbool型の変数に代入するだけのシンプルなコードです。
測定する関数以外のコードはコメントアウトしています。
測定する関数以外のコードはコメントアウトしています。
- bool bSame = false;
- CString str1 = _T("Vdk4jQnifyheeUVSJMAD2FdUXyCfR9HRTACieCMBVDguC3ALbU");
- CString str2 = _T("vDK4JqNIFYHEEuvsjmad2fDuxYcFr9hrtacIEcmbvdGUc3alBu");
- // 測定start
- auto start = std::chrono::system_clock::now();
- for (int i = 0; i < 1000000; i++) {
- //bSame = (0 == str1.Compare(str2));
- //bSame = (0 == str1.CompareNoCase(str2));
- //bSame = (0 == _tcscmp(str1, str2));
- bSame = (str1 == str2);
- }
- // 測定end
- auto end = std::chrono::system_clock::now();
- // ミリ秒→秒 変換
- double dSec = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() * 0.001;
- // 出力
- CString strResult = _T("");
- strResult.Format(_T("%lf"), dSec);
- ::AfxMessageBox(strResult);
【検証結果】
結論:_tcscmpが一番速い
上記の結果を見ると、基本的には「_tcscmp」を使うのが良さそうですね!
- Cstring::Compare 0.030
- Cstring::CompareNoCase 1.380
- _tcscmp 0.003
- CStringの等価演算子(==) 0.037
上記の結果を見ると、基本的には「_tcscmp」を使うのが良さそうですね!
【Tips】
今回の速度計測に用いたライブラリ「std::chrono」を簡単に紹介しておきます。
C++11から導入された標準ライブラリで、現在時刻の取得や時間計測ができます。
以下は処理時間を計測する時のサンプルコードです。
応用すればラップタイムの出力もできるので、ぜひ使ってみてください!
C++11から導入された標準ライブラリで、現在時刻の取得や時間計測ができます。
以下は処理時間を計測する時のサンプルコードです。
- auto start = std::chrono::system_clock::now();
- // この間に測定したい処理を書く
- auto end = std::chrono::system_clock::now();
- // 処理時間を取得(ミリ秒→秒に変換)
- double dSec = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() * 0.001;
応用すればラップタイムの出力もできるので、ぜひ使ってみてください!
以上、いかがでしたでしょうか?
ちょっとテーマがピンポイントすぎたかもしれませんが、皆さんのお役に立てれば幸いです。
次回も乞うご期待ください!
ありがとうございましたm(_ _)m
ちょっとテーマがピンポイントすぎたかもしれませんが、皆さんのお役に立てれば幸いです。
次回も乞うご期待ください!
ありがとうございましたm(_ _)m
« オープンソース・クラウド | 技術情報ブログトップページ | 技術情報ブログの記事一覧 | システムリフォーム »