インクリメントの前置後置速度比較(vc++/gcc/clang)

後置インクリメントと前置インクリメントは基本的に前置の方がよいそうですが、自分は後置の方が書きやすくて好きなので、どこまでなら後置でもいいのか検証する目的で速度を測ってみました。

検証に使用したコンパイラのバージョン:
VisualStudio C++ 19.00.22609 for x86
gcc (GCC) 4.8.1
clang version 3.6.0 (tags/RELEASE_360/final)

検証環境はWindows10 TechnicalPreview

検証内容

典型的なfor文を前置後置それぞれのインクリメントを使用する形で作成。
インクリメント対象をunsigned int/iterator/適当に重めのクラスの三種それぞれを対象。
また、最適化により返り値計算が消えることを考えforの真偽判定でインクリメントの返り値を使用するものも用意。
以上をそれぞれ100,000,000回実行をさらに10回繰り返しかかった時間の平均をとる。
また、比較用に拡張forにてループまわすだけのも計測。

具体的なソースコードとコンパイルオプションは以下を参照。
ソースコード
コンパイルに使ったバッチファイル

以下読み飛ばしても通じる、検証をこの内容にした理由やらなんやら:

インクリメント演算子と言えば基本的にループで使用することが多いはず。
まれに+=1の代わりに使うこともありますが、それで行いうる処理はループも全て含んでいるでしょう。
他にも演算子オーバーライドなどでクラス独自に定義して使うこともあるかもしれませんが
そこまで行くと後置不利の根拠である値コピーがどーたらという前提すら崩れかねないですし
そもそも滅多にあるものでもないので今回は除外しました。

また、普通にforをまわすだけだと最適化で消え去ってしまうし、ある程度最適化を妨げようとすると今度はforが誤差になるぐらい処理速度を持って行かれてしまいます。
なので、インラインアセンブラでNOPを突っ込むことで対応。
それでもintのインクリメントをデクリメントに置換されたりいろいろしていますが、それは通常利用でも起きうるものとして許容しています。

クラス作成もいろいろ大変で、単にintをラップしたようなのを書くと当然ながら全部インライン展開されてint直と同じレベルまで最適化されてしまいます。
ですが、今回は後置の不利を検証するためなので残す必要があり、いい具合に重い処理として乱数を生成させました。
とはいえ乱数処理は重すぎるので1/1000しか動かないコードが入っています。

ちなみに、最適化の抑止の大変度合はclang>gcc>vcでした。
vcはNOP入れた以外では全部素直なコードを吐いて順当に遅かったです。
gccはクラスのインクリメントにて使いもしないメンバー配列をstd::copyしたら順当に遅くなりましたが、clangはそれすら最適化で消し去りました。
つまりclangが一番最適化は賢い、と思ったら最終的な計測結果ではgccに負ける感じに、詳しくは結果欄をどうぞ。

結果

vc++ gcc clang
拡張for 57ms 53ms 48ms
int前置インクリメント 46ms 61ms 41ms
iterator前置インクリメント 47ms 47ms 45ms
クラス前置インクリメント 320ms 47ms 316ms
int前置インクリメント、返り値使用 44ms 47ms 46ms
iterator前置インクリメント、返り値使用 44ms 37ms 46ms
クラス前置インクリメント、返り値使用 317ms 51ms 313ms
int後置インクリメント 51ms 44ms 53ms
iterator後置インクリメント 51ms 50ms 46ms
クラス後置インクリメント 2206ms 388ms 446ms
int後置インクリメント、返り値使用 55ms 50ms 50ms
iterator後置インクリメント、返り値使用 97ms 50ms 54ms
クラス後置インクリメント、返り値使用 2253ms 458ms 431ms

まず独自クラスは前置と後置で明らかに結果が異なり、後置が圧倒的に遅いです。
最小のclangでも1.5倍、最大のgccだと実に約9倍。
もちろんこれはコピーコンストラクタの重さに依存しますが、ある程度以上のクラスであれば必要のない後置インクリメントは避けた方がよさそうです。
また、地味ですがvc++のみ返り値使用の後置iteratorが前置の約2倍かかっています。
vc++においてはまだ「iteratorは最適化すれば消え去る」は幻想のようですね。

返り値の使用有無による変化は例外を除いて後置のクラスでのみでした(例外:前述のvc++後置iterator)。
おそらくintやiteratorであれば最適化の結果前後が消え去り、通常のメモリ=>レジスタ移動の範囲で解決できてしまうからでしょう。

なぜかgccだけクラス使用時の速度がとても速いです。
が、試行錯誤している間はずっとclangの方が速かったので、偶然今のコードではインスタンスが全部レジスタに載ったとかだと思われます。
ただしvc++がクラス使うと遅いのは最初から最後まで一貫していたので、この三つの中で一番遅いことは確定してよさそう。

他は特に速度の変化は見られません、また拡張forとの速度差も見られません。
なので、後置インクリメントでもプリミティブ型かiteratorなど薄いラッパークラスであれば速度に影響が出ることはない、と言っていいと思います。
(例外:vc++で後置インクリメントの結果を使う)
それ以外で後置インクリメントを使うこと自体そうあることではないですし、別に後置インクリメントをつける癖がついていても害はないと言っていいでしょう、あーよかった。

終わりに

前回せっかくclangやgccの環境を整えたので、以前から気になっていたインクリメントの速度計測をやってみました、いかがだったでしょうか。
個人的には後置の方が書きやすくて好きなので、現代の最適化の力を使えば特に害はなさそうとわかって一安心でした。

次はまた何かの速度計測をやるかもしれませんし、何かアプリ作るまで放置かもしれません。
ではまた。

windowsにおけるただフォワードするだけの関数の機械語比較(VC++/gcc/clang)

本の虫さんの記事にて
linux上のGCCではただ別の関数へフォワードするだけの関数をコンパイルしてもPICが有効だとjmp一文にならない事とその理由を説明した記事を翻訳したものが載った。
そこでふと疑問に思ったのだが、PICとか関係のないwindows上ではどうなっているのだろうか?
というわけで、GCCとclangとVisualStudio2015それぞれでコンパイルして結果をまとめてみた。
なお、最適化の結果消え去るならば問題ないはずとの考えから最適化OFF/ON両方で検証している。

検証に使用したコンパイラのバージョン:
VisualStudio C++ 19.00.22609 for x86
gcc (GCC) 4.8.1
clang version 3.6.0 (tags/RELEASE_360/final)

検証コード

まずは検証に使うソースコードを載せる。

main.cpp

#include <iostream>

void OtherFile();
void TailProc();

void NormalProc() {
  std::cout << "Normal\n";
}

void CheckOtherFile() {
  OtherFile();
}

void CheckTailProc() {
  TailProc();
}

void CheckNormalProc() {
  NormalProc();
}

void TailProc() {
  std::cout << "Tail\n";
}

int main() {
  CheckOtherFile();
  CheckTailProc();
  CheckNormalProc();
  return 0;
}

lib.cpp

#include <iostream>

void OtherFile() {
  std::cout << "OtherFile\n";
}

一応解説しておくと、CheckOtherFileは定義が別の.cpp、CheckTailProcは同じ.cppだが後方で宣言、CheckNormalProcは前方で宣言となっている。

コンパイルに使用した.bat

cl /EHsc /Ox /GL /Fevc_exe.exe main.cpp lib.cpp
cl /EHsc /Od /Fevc_exe_O0.exe main.cpp lib.cpp
g++ -O3 -flto -o gcc_exe.exe main.cpp lib.cpp
g++ -O0 -o gcc_exe_O0.exe main.cpp lib.cpp
clang++ -O3 -o clang_exe.exe main.cpp lib.cpp
clang++ -O0 -o clang_exe_O0.exe main.cpp lib.cpp

clangのみ手元ではリンク時最適化できなかったため通常の最適化のみ。

gcc

最適化OFFの場合

main

各チェック関数

mainでなぜかcallが四つあるが一つ目おそらくコンパイラがつけた特殊関数だと思われる。
問題のcheck部分は元の記事ほど悪くはないがjmp命令ひとつまで縮んでいない。

最適化ONの場合
main

各チェック関数は三つとも完全にインライン展開されている。

VisualStudio C++

最適化OFFの場合

main

各チェック関数

チェック関数は三つともほぼ同じだったため他二つは割愛。

最適化ONの場合
main

三つとも完全にインライン展開されている。

clang

最適化OFFの場合

main

各チェック関数

mainの一つ目のcallはgccと同じくコンパイラが付与したもの。
チェック関数は三つともほぼ同じだったため他二つは割愛。

最適化ONの場合
main

OtherFileだけインライン展開されていないが、それは前述のとおりリンク時最適化が入っていないためと思われる。
CheckOtherFile自体はインライン展開されている。

まとめ

windows上だとVC++/gcc/clangのどれであっても、フォワードするだけの関数をjmp一文に翻訳してくれたりはしない。
だが、最適化を有効にすればどのコンパイラでも、別の.cppでも同じ.cppでも、大体インライン展開されてほぼコスト0になるので気にする必要はほぼない。

終わりに

VC++ではjmp一文にならないのは知っていたが、記事を読んだ範囲ではgccがjmp一文に翻訳するとしかおもえなかったため、コンパイラ比較にいい題材だろうと試してみた結果
このようなどれも結果がほぼ一緒で大した価値のない比較記事が生まれることとなりました、なんてこったい。

一応clangが文字数情報を生成してくっつけてくれてるとか、VC++はmainはほぼ書いたまんまだけどその他は前後にちょっと追加があるとか、clangのフォワード関数部分はgccではなくVC++寄りだとか、見どころはないでもないが全部本題とは無関係である。

まぁ、可読性のために別関数にしたりするぐらいではそう滅多にオーバーヘッドは生まないことが分かっただけでも収穫だろうか。

なにか間違いなどありましたらコメントください。
ではまた。

Windowsのセキュリティ機能Control Flow Guard解説

Windows8.1 updateから入ったセキュリティー機能であるControl Flow Guard(以降GuardCF)について調べたのでまとめておきます。

本記事を書く上で、以下のサイトを参考にさせていただきました。
http://www.ffri.jp/blog/2015/01/2015-01-05.htm

IMAGE_OPTIONAL_HEADER32など本稿にあまり関係のない部分については特に解説しないので、必要なら別途ググるなどしてください。

GuardCFとは何か

ざっくりと説明すると「呼び出す関数が実行時に決定されるコードから呼び出せるアドレスをホワイトリストで管理し、それ以外を実行しようとしたらクラッシュする機能」です。
たとえばC++の仮想関数、dllを動的リンクした際のGetProcAddressの返り値、関数ポインターを経由した呼び出し、などなどが対象。
主に、仮想関数テーブルを上書きすることで仮想関数実行時に任意のアドレスを実行させる、俗にいうvtable overwriteを防ぐための機能です。

パスの解析やらは一切やっておらず、バグか脆弱性がない限りここではAは呼ばれないと自明な場合でも、Aがホワイトリストに入っていればAの呼び出しを許可してしまいます。
おそらくこれは速度的な都合か、キャストを駆使されると確実な追跡が難しいというあたりかと。

また、仕組み上GuardCFは.exeだけではなくリンクする.dllでもすべて有効になっていなければ効果が激減します。
win8.1 update以降であればwindows提供の.dllの大半はGuardCFがかかった状態で提供されていますが、サードパーティー製の.dllなどを使う場合、GuardCFが有効かチェックしてから使うとよいでしょう。

実際に作成してみたい場合、2015/04/01現在preview版であるVisualStudio2015の14.0.22609.0 D14REL以降を使う必要があります。
C/C++のコンパイルオプションに「/d2guard4」、Linkerのオプションに「/guard:cf」を追加してコンパイルすればGuardCFが有効な.exe/.dllを作成できます。
詳しくはFFRI様の記事を参照のこと。

次は具体的にどのようにして処理されているのかを説明しましょう。

具体的にはどういう仕組みなのか

まずコンパイル時に動的呼び出しされうるアドレスの一覧を作成する必要があります。
正式に資料に載っていたわけではなく動作から推測した内容ですが、仮想関数/dllexportした関数/&演算子などでアドレス取得された関数、のどれかに合致した物をすべてホワイトリストとして扱うようです。

次に実行時に呼び出すアドレスが確定するコードの直前に「guard_check_icallというホワイトリストに含まれるかを調べる関数」を呼び出すようコードを追加します。

次はリンクして.exeを生成する際にGuardCFにまつわる情報を付加します。
中身は、guard_check_icallで呼び出すアドレス格納位置、GuardCFのホワイトリスト、GuardCFの動作にまつわるフラグ、の3種になります。
これの詳細ついては後述します。

そして実行時、まず通常通り.exeをロードしますが、その際にguard_check_icallは通常のリロケーション処理に基づき、単にRETNするだけの関数へ向けます。
その後、win8.1 update以降である場合はGuardCF用情報を元に、guard_check_icallが呼び出す先のアドレスをどこに保持しているかを突き止め、それをntdll.guard_check_icall_fptrに変更します。
これにより、旧来のOS側の専用処理をいれることなく従来のロード処理だとチェックがスキップされ、win8.1 update以降でのみGuardCFが有効になるわけです。

次に実際のチェックです。
以下のうちどれかに合致すると実行してもよいと判断するようです。

  • 実行しようとしているアドレスが事前に各種.exeや.dllから集めたホワイトリストの中に含まれている
  • GuardCFがかかっていない.dll/.exe上のアドレスを指している
  • .dllや.exeに属さない動的確保したメモリー上で、なおかつそのメモリーに実行可能属性がついている

そして、実行するべきではないと判断した場合は強制的にクラッシュして、被害を最小化する、という動作になります。

.exe/.dllに付与されるGuardCF情報詳細

IMAGE_OPTIONAL_HEADER32内のDataDirectoryのIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG(=10)に格納されています。
これはIMAGE_LOAD_CONFIG_DIRECTORY32構造体がそのまま入っていて、VC++2015以降であればGuard*というメンバーがあるのでそれを使います。
※GuardFlagsは0x58にあるのに14.0.22609.0 D14REL時点ではIMAGE_DATA_DIRECTORY上はsizeが0x40と出力されます、が、IMAGE_DATA_DIRECTORY.sizeが間違っているだけで、IMAGE_LOAD_CONFIG_DIRECTORY32.Sizeは正しい値なのでそちらを参照のこと。

以下各パラメーターの説明。

  • GuardCFCheckFunctionPointer:前述したguard_check_icallで呼び出す先のアドレスが格納されている場所の仮想アドレス、offsetではないのでベースアドレスの変動を考慮する必要がある
  • GuardCFFunctionTable:ホワイトリストの仮想アドレス、4byteのアドレスがGuardCFFunctionCount個分並んでいる、こちらもoffsetではない点に注意
  • GuardCFFunctionCount:ホワイトリストの数
  • GuardFlags:WinNT.h内のIMAGE_GUARD_*のフラグ群をorしたもの、ちゃんと調べたわけではないがIMAGE_GUARD_CF_FUNCTION_TABLE_PRESENTを入れるとGuardCF有効とみなされる模様

Q&A

その他雑多にいろいろ書いておきます。

    • Q結局どうすればいいの
    • A何も考えずに.exe/.dllすべてでGuardCFを有効にすればいいとおもいます
    • Qバイナリしか提供されていない.dllにGuardCFかかってなかったんだけど
    • Aどうしようもありません、あきらめましょう
    • Q GuardCFをかけることのデメリットは?
    • Aだいぶ安全側に倒した実装になっているので一般的には処理速度の低下だけだと思われます、また一部の人にとっては外部ツールによる機能拡張が大変になるというデメリットもあります。
    • Q処理速度にどれぐらい影響するの?
    • A自分も知りたいので計測して記事書いてください、見に行きますので。
    • Q IMAGE_OPTIONAL_HEADER32内の~あたりからさっぱりわからん
    • A IMAGE_DOS_HEADER、IMAGE_NT_HEADERS32、IMAGE_DATA_DIRECTORYあたりでググって出てきたページ片っ端から読む方が自分が説明するよりわかりやすいとの判断により説明していません。

終わりに

どうでしたでしょうか?
実はこれエクスプロイトを書きつつ学ぶWindowsセキュリティー機能 ~番外編SEHOP~の続編にするつもりだったんですが、/DYNAMICBASE:NOを指定するとなぜかGuardCFが働くなってしまい、どうしてもexploitがASLR回避に比重が置かれるため断念したという経緯で書かれたものになります。
そのためただ情報を列挙しただけで、読みづらい感じに……。
まぁそれでも人によっては役に立つ資料となるでしょう。

どうか、一人でも多くこういったセキュリティー機能に興味を持ってくれることを祈ります。
そして自分で書かなくてもググれば見つかる世の中になったらいいな。