システムプログラミングの定義はあいまいだが、ビット、バイト、インストラクション、CPUサイクルレベルで考える必要があるものだと言えるだろう。システムプログラミングには、性能と信頼性が暗に要求される。QCon New Yorkにおいて、Microsoftでエンジニアリングディレクターを務めるJoe Duffy氏がC#のシステムプログラミング戦略について紹介した。彼はまた落とし穴と移行方法についても語った。

Joe氏のトークで語られた教訓は、Midoriと呼ばれる研究プロジェクトに由来する。このプロジェクトはC#を使ってOSをスクラッチから作るもので、コンパイラ構築や高性能なコードのための戦略について、新たな知見をもたらした。

マネージド言語を使ってOSを構築すると、メモリレベルでC#のセキュリティ機能が使えるようになる。これはバッファーオーバーフローやフォーマット文字列の脆弱性によるコードインジェクションなど、メモリに基づく弱点に対する解決策を与えてくれる。ランタイムが境界チェックや型の安全性をケアするためだ。

コード生成

コードはAOT (Ahead of time) もしくは JIT (Just in time) でコンパイルできる。JITには高速なコンパイル時間というメリットがある。これに対して、AOTはコンパイラによるさらなる最適化が行われるため、優れたマシンコードを生成することができる。

ネイティブ言語コンパイラが行う複数の最適化は、これまでマネージド言語で使えなかった。よくある理由は、JITコンパイラで最適化するには、計算負荷が高すぎたり、複雑すぎるためだ。このことは、無駄なく効率良い低レベルコードを生成するという領域において、C#に悪評をもたらした。最近、RyuJitにより、以下の最適化が実装された。

- インライン化 (関数コールサイトを呼び出された関数ボディで置き換える)

- フローグラフとループの解析

- 静的単一代入 (SSA) と大域値番号付け

- 共通式の削除

- コピー/定数伝播

- デッドコード削除

- 範囲解析

- 脱仮想化

- ループ不変コードの巻き上げ

- SIMDとベクトル化

- ジェネリック共有

- スタックアロケーション (開発中)

ガベージコレクション

.NETのガベージコレクタは3世代からなる世代別ガベージコレクタだ。データプログラム解析が、ガベージコレクションの実行時間 (実際に作業をしていない時間) の半分以上を費やすこともある。

性能を改善するためのひとつの方法は、構造体 (struct) を使うことだ。構造体は次のレベルで性能を改善する。

- GCのプレッシャー低下。構造体はスタックに割り当てられるため。

- メモリ局所性の向上、キャッシュヒット率を改善。

- 全体のメモリ消費低下。32-64ビットアプリケーションにおけるオブジェクトの8-16バイトオーバーヘッドを回避。

構造体を使う際には注意すべきことがある。あるサイズを越えると、コピーがmemcopyを引き起こす恐れがあることだ。最適な性能を出すためには、構造体を小さく保つ必要がある (32/64バイト以下)。

C# 7には、構造体を使った低レベルの最適化を容易にするための機能が含まれている。C# 7のタプル (tuple) は構造体だ。以前のバージョンはオブジェクトだった (System.Tuple<>)。refのリターンも構造体の機能で、コピーを避けながら関数から構造体を返すことができる。

エラーハンドリング

例外は復旧可能なエラーのためにある。しかし、多くのエラーは復旧不可能だ。不正なキャスト、スタックオーバーフロー、ヌル参照といったエラーは、実際にはバグである。これに対して、I/Oエラーとバリデーションエラーは予期して復旧可能にすべきだ。

エラーからの復旧は「Fail fast」戦略につながる。Fail fastは.NETに含まれているメカニズムで、StackOverflowのような例外が発生したとき、例外ハンドラをバイパスして、プロセスをクラッシュさせる。これはあまりに汎用の例外ハンドラにエラーが捕捉されるのを防ぎ、エラーを発見しやすくする。Midoriチームは、復旧可能なエラー (例外) とバグ (Fail fast) が1:10の比率になることを見つけた。

詳しい情報がJoe氏のブログにある。彼はMidoriに関する複数の記事を書いている。