SlideShare a Scribd company logo
Photo by Markus Spiske · CC-License: CC BY
www.temporausch.com
C++でCプリプロセッサを
作ったり速くしたりしたお話
Ladies++meetup#1
@kinu/KinukoYasuda
やすだ きぬこ (@kinu)
WhoamI
普段は C++ でブラウザ (Chrome) を作ってます
ときどきルンバをハックしたり家庭運用の
ための趣味アプリを作ったりしている
C++11 勉強中
C++ の話…と見せかけて 概ね
CPP (C Preprocessor・プリプロセッサ)
のお話をしたいと思います
1. C Preprocessor を使ってみよう!
2. C Preprocessor を (C++で) 作った話
3. C Preprocessor を高速化した話
今日のお話!
Part1:CPreprocessor を
使ってみよう!
❄ #include とか #define とかを解釈するアレ
❄ C や C++ とはまったく関係ない独立した文法/処理
○ コメント処理 (削除)・’¥’ で終わる行の連結
○ 外部ファイル読み込み (#include, #include_next)
○ 条件分岐 (#ifdef / #if / #else / #elif / #endif)
○ 単語置換・マクロ展開 (#define)
○ 文字列化 (#google → “google”)
○ 結合 (a##b → ab)
❄ 頑張れば割といろいろできる
○ でもマクロ展開の仕様が謎で使いこなすのはけっこう難しい
○ というか使いこなす必要は特にない…
CPreprocessorのキホン
❄ ファイルを include する。以上!
❄ その他の使い方は特に C++ ではあまり推奨されない
○ C で型共通のツールを書く (リンクリストとか)
○ template では面倒なちょっとしたマクロ
○ デバッグ用や個人用コードでの手抜き
○ 任意個引数 template の繰り返し記述を手抜きする
(可変長 template で出番がなくなった気がする)
CPreprocessor何に使う?
❄ # による文字列化 (べんり)
に展開
❄ ## による結合 (べんり)
❄ マクロ名でもなんでも結合できる (なかなかキモい)
33 に展開される
CPPのややマイナー機能を使う
これは “fooVER.h” を include してしまう!
❄ 以下のように書き換えれば動く…
まず base と v を展開させる
これは “foo1.h” を include!
CPPの悩ましい展開例 (1)
・・・!?
❄ 再帰呼び出しはできない
になる; 再帰しない
❄ こんな風に擬似的に回避するのが普通
❄ (もっとトリッキーにちゃんと再帰する方法もある)
CPPの悩ましい展開例 (2)
・・・
❄ 少し一般化するとやや汎用的に (boost 風味)
❄ 使い方例
CPPのマクロ展開で繰り返しを頑張る
なかなか便利。
とはいっても…
C++11, C++14 で C++ 本体が
大幅パワーアップ!
range for や可変長テンプレートなどで
オワコン化が進む
基本機能以外は FizzBuzz を書いたり
謎マクロ展開を楽しむための趣味言語になった… (私感)
…気にせず次のパートへ
Part2:CPreprocessor を
C++で作ってみた
❄ 背景:クラウドコンパイラ
○ Chromium のコンパイルが遅い!
○ Google インフラを使って大量のファイルを高速コンパイルするた
めのシステムが ukai, shinh らにより開発される (参照)
■ このシステムのためにコンパイルに必要な include ファイルを
検知してクラウドに送りつける部分が必要に
● → #include 行を適当に処理すればいいじゃん?
● → 適当に作ったら問題たくさん発覚
● → マジメな C Pre Processor を作ることに
❄ ということで 20% タイムで作ってみた (5年くらい前)
なぜ CPP を作ったのか?
❄ C++ で 2500 行くらい
❄ 基本構造
○ シンプルな手書き再帰下降パーサ
■ # ではじまる行を探して
■ define ならマクロ定義部分を記録
■ include や include_next ならファイルを読み込む
■ if, ifdef, else, elif, endif なら条件分岐
○ include ファイルリストを求める機能に特化
■ C や C++ のプログラム部分はパースしなくてよい
● でも include のためにマクロ展開と演算評価は必要
❄ そこそこ高速
○ gcc -E の 6倍くらい (単純比較はできないけど)
お手製 CPP パーサの概要
❄ CPP のマクロ展開は謎が多い
○ → なぜなら仕様が謎だった!
■ 仕様通り実装しても書かれてない部分があって完全に実装
できない・現在知られてる挙動にならない
○ ANSI C Standard Committee の Dave Prosser とい
う人によるアルゴリズムが公開されている
■ http://www.spinellis.gr/blog/20060626/
CPP のマクロ展開を実装する
これなかったら実装できなかった…
CPP のマクロ展開アルゴリズム (1)
CPP のマクロ展開アルゴリズム (2)
CPP のマクロ展開アルゴリズム (3)
❄ …とにかくアルゴリズム通りに書けば動く
❄ なんとなくの概要:
○ 各トークンに “hide set” (HS)というのが結びついている
○ マクロを展開するたびに展開結果のトークンの HS にそのマクロ
が足される
○ 展開は再帰的に行われるが、”hide set” に入っているマクロはそ
のスキャン中はもう展開されない (再帰の限定的禁止)
○ ## はマクロ展開より先に起こる (遅延させて回避可能)
。。。。。
CPP を作る・完成
❄ その他
○ __LINE__ や __DATE__ もちまちま作る
○ 条件分岐のために数式のパースと整数演算評価もする
❄ 少しずつ複雑そうなソースを使ってテストしながら修正・テ
ストケースに足していく
○ Chromium, WebKit, elfutils, Linux Kernel, boost, ...
❄ だいたい正しく動くように (∩´∀`)∩
Part3:CPreprocessor を
高速化してみた
マジメに作ってみたのはいいが…
❄ 最初に作られた単純な実装 (string::find + substr + いろ
んなハック) に比べると半分くらいの速度
❄ コンパイル速度を速くするのがクラウドコンパイラの目標
なので、高速に動かないと意味がない…
→ 高速化しよう!
なんか遅い。。
❄ まずは細かい部分を端から修正
○ switch などはできるだけテーブルに
○ locale 依存の ctype.h の関数 (isalnum など) は使わない
○ 再帰的なアルゴリズムを再帰を使わない形で再実装
○ ホットな場所での深いポインタ参照は避ける
○ stringstream は激重なので (例えエラー出力でも) 避ける
○ トークンのサイズをなるべく小さく・頻度の高いトークンに最
適化 (ちなみに小さくし過ぎたら遅くなった)
1文字の記号など用
2文字の演算子用 (“==” など)
CPP の高速化 (1)
❄ 一旦処理したファイルはなるべくもう読まない
○ マクロを再定義して自分を再帰的に include するファイルもある
ので単純にはできない
■ → ファイル毎に評価されたマクロと変更したマクロを覚えておき、
評価されたマクロが変更されない限り二度は読まない
■ → 全マクロIDの評価と変更を記録するとメモリを食いすぎて逆に遅
くなるので、bloomfilter っぽく false positive は許す
→ 20% 高速化!
❄ #endif やコメント終端までできるだけ速くジャンプ
○ ‘if/ifdef’ 毎に対応する endif の位置を憶えておく
→ ~15% 高速化! (でも後に削除)
CPP の高速化 (2)
❄ ‘#’ ディレクティブマッチに Double-Array Trie を使う
○ 大した種類はないけど頻繁に呼ばれる・テーブルは静的なので
更新の心配はなし
→ 5% 高速化!
❄ 読み飛ばし処理に SSE を使う
○ 次の ‘#’ まで読み飛ばす、次の ‘*/’ まで読み飛ばす、次の改行ま
で読み飛ばすなど、最頻出処理
○ SSE2 で16文字一度に compare し、popcount で改行も数える
(__LINE__ のために行カウントが必要なため)
→ 25% 高速化!
CPP の高速化 (3)
❄ 最終的には最初の単純実装より2倍くらい速くなった
○ (現在は社内で引き継がれてさらに高速化されたようです)
❄ 雑な知見
○ ctype.h, stringstream は速度が気になるなら使わない
SSE は意外と普通に速くなる
○ 大幅高速化は大体「速くする」より「処理しない」
めでたしめでたし!
CPP の高速化まとめ
おしまい
HappyC++(andCPP)Life!

More Related Content

C++でCプリプロセッサを作ったり速くしたりしたお話

  • 1. Photo by Markus Spiske · CC-License: CC BY www.temporausch.com C++でCプリプロセッサを 作ったり速くしたりしたお話 Ladies++meetup#1 @kinu/KinukoYasuda
  • 2. やすだ きぬこ (@kinu) WhoamI 普段は C++ でブラウザ (Chrome) を作ってます ときどきルンバをハックしたり家庭運用の ための趣味アプリを作ったりしている C++11 勉強中
  • 3. C++ の話…と見せかけて 概ね CPP (C Preprocessor・プリプロセッサ) のお話をしたいと思います 1. C Preprocessor を使ってみよう! 2. C Preprocessor を (C++で) 作った話 3. C Preprocessor を高速化した話 今日のお話!
  • 5. ❄ #include とか #define とかを解釈するアレ ❄ C や C++ とはまったく関係ない独立した文法/処理 ○ コメント処理 (削除)・’¥’ で終わる行の連結 ○ 外部ファイル読み込み (#include, #include_next) ○ 条件分岐 (#ifdef / #if / #else / #elif / #endif) ○ 単語置換・マクロ展開 (#define) ○ 文字列化 (#google → “google”) ○ 結合 (a##b → ab) ❄ 頑張れば割といろいろできる ○ でもマクロ展開の仕様が謎で使いこなすのはけっこう難しい ○ というか使いこなす必要は特にない… CPreprocessorのキホン
  • 6. ❄ ファイルを include する。以上! ❄ その他の使い方は特に C++ ではあまり推奨されない ○ C で型共通のツールを書く (リンクリストとか) ○ template では面倒なちょっとしたマクロ ○ デバッグ用や個人用コードでの手抜き ○ 任意個引数 template の繰り返し記述を手抜きする (可変長 template で出番がなくなった気がする) CPreprocessor何に使う?
  • 7. ❄ # による文字列化 (べんり) に展開 ❄ ## による結合 (べんり) ❄ マクロ名でもなんでも結合できる (なかなかキモい) 33 に展開される CPPのややマイナー機能を使う
  • 8. これは “fooVER.h” を include してしまう! ❄ 以下のように書き換えれば動く… まず base と v を展開させる これは “foo1.h” を include! CPPの悩ましい展開例 (1) ・・・!?
  • 9. ❄ 再帰呼び出しはできない になる; 再帰しない ❄ こんな風に擬似的に回避するのが普通 ❄ (もっとトリッキーにちゃんと再帰する方法もある) CPPの悩ましい展開例 (2) ・・・
  • 10. ❄ 少し一般化するとやや汎用的に (boost 風味) ❄ 使い方例 CPPのマクロ展開で繰り返しを頑張る
  • 12. C++11, C++14 で C++ 本体が 大幅パワーアップ! range for や可変長テンプレートなどで オワコン化が進む 基本機能以外は FizzBuzz を書いたり 謎マクロ展開を楽しむための趣味言語になった… (私感)
  • 15. ❄ 背景:クラウドコンパイラ ○ Chromium のコンパイルが遅い! ○ Google インフラを使って大量のファイルを高速コンパイルするた めのシステムが ukai, shinh らにより開発される (参照) ■ このシステムのためにコンパイルに必要な include ファイルを 検知してクラウドに送りつける部分が必要に ● → #include 行を適当に処理すればいいじゃん? ● → 適当に作ったら問題たくさん発覚 ● → マジメな C Pre Processor を作ることに ❄ ということで 20% タイムで作ってみた (5年くらい前) なぜ CPP を作ったのか?
  • 16. ❄ C++ で 2500 行くらい ❄ 基本構造 ○ シンプルな手書き再帰下降パーサ ■ # ではじまる行を探して ■ define ならマクロ定義部分を記録 ■ include や include_next ならファイルを読み込む ■ if, ifdef, else, elif, endif なら条件分岐 ○ include ファイルリストを求める機能に特化 ■ C や C++ のプログラム部分はパースしなくてよい ● でも include のためにマクロ展開と演算評価は必要 ❄ そこそこ高速 ○ gcc -E の 6倍くらい (単純比較はできないけど) お手製 CPP パーサの概要
  • 17. ❄ CPP のマクロ展開は謎が多い ○ → なぜなら仕様が謎だった! ■ 仕様通り実装しても書かれてない部分があって完全に実装 できない・現在知られてる挙動にならない ○ ANSI C Standard Committee の Dave Prosser とい う人によるアルゴリズムが公開されている ■ http://www.spinellis.gr/blog/20060626/ CPP のマクロ展開を実装する これなかったら実装できなかった…
  • 20. CPP のマクロ展開アルゴリズム (3) ❄ …とにかくアルゴリズム通りに書けば動く ❄ なんとなくの概要: ○ 各トークンに “hide set” (HS)というのが結びついている ○ マクロを展開するたびに展開結果のトークンの HS にそのマクロ が足される ○ 展開は再帰的に行われるが、”hide set” に入っているマクロはそ のスキャン中はもう展開されない (再帰の限定的禁止) ○ ## はマクロ展開より先に起こる (遅延させて回避可能) 。。。。。
  • 21. CPP を作る・完成 ❄ その他 ○ __LINE__ や __DATE__ もちまちま作る ○ 条件分岐のために数式のパースと整数演算評価もする ❄ 少しずつ複雑そうなソースを使ってテストしながら修正・テ ストケースに足していく ○ Chromium, WebKit, elfutils, Linux Kernel, boost, ... ❄ だいたい正しく動くように (∩´∀`)∩
  • 23. マジメに作ってみたのはいいが… ❄ 最初に作られた単純な実装 (string::find + substr + いろ んなハック) に比べると半分くらいの速度 ❄ コンパイル速度を速くするのがクラウドコンパイラの目標 なので、高速に動かないと意味がない… → 高速化しよう! なんか遅い。。
  • 24. ❄ まずは細かい部分を端から修正 ○ switch などはできるだけテーブルに ○ locale 依存の ctype.h の関数 (isalnum など) は使わない ○ 再帰的なアルゴリズムを再帰を使わない形で再実装 ○ ホットな場所での深いポインタ参照は避ける ○ stringstream は激重なので (例えエラー出力でも) 避ける ○ トークンのサイズをなるべく小さく・頻度の高いトークンに最 適化 (ちなみに小さくし過ぎたら遅くなった) 1文字の記号など用 2文字の演算子用 (“==” など) CPP の高速化 (1)
  • 25. ❄ 一旦処理したファイルはなるべくもう読まない ○ マクロを再定義して自分を再帰的に include するファイルもある ので単純にはできない ■ → ファイル毎に評価されたマクロと変更したマクロを覚えておき、 評価されたマクロが変更されない限り二度は読まない ■ → 全マクロIDの評価と変更を記録するとメモリを食いすぎて逆に遅 くなるので、bloomfilter っぽく false positive は許す → 20% 高速化! ❄ #endif やコメント終端までできるだけ速くジャンプ ○ ‘if/ifdef’ 毎に対応する endif の位置を憶えておく → ~15% 高速化! (でも後に削除) CPP の高速化 (2)
  • 26. ❄ ‘#’ ディレクティブマッチに Double-Array Trie を使う ○ 大した種類はないけど頻繁に呼ばれる・テーブルは静的なので 更新の心配はなし → 5% 高速化! ❄ 読み飛ばし処理に SSE を使う ○ 次の ‘#’ まで読み飛ばす、次の ‘*/’ まで読み飛ばす、次の改行ま で読み飛ばすなど、最頻出処理 ○ SSE2 で16文字一度に compare し、popcount で改行も数える (__LINE__ のために行カウントが必要なため) → 25% 高速化! CPP の高速化 (3)
  • 27. ❄ 最終的には最初の単純実装より2倍くらい速くなった ○ (現在は社内で引き継がれてさらに高速化されたようです) ❄ 雑な知見 ○ ctype.h, stringstream は速度が気になるなら使わない SSE は意外と普通に速くなる ○ 大幅高速化は大体「速くする」より「処理しない」 めでたしめでたし! CPP の高速化まとめ