1. C が「機械に近い」とされた理由
「C は低水準言語だ」というのは、プログラマの間でほぼ自明の前提として扱われます。
計算機がどのように動作するかを考えるときに、配列やポインタや構造体の考え方は役に立ちます。
でも、それはいつの時代の、どんな機械に対しての話なのでしょうか。
2018年、David Chisnall が ACM Queue に寄稿した “C Is Not a Low-level Language” は、その前提を静かに、しかし鮮やかに崩しました1。
1.1. PDP-11 と C
1970年代、C は PDP-11 という小型ミニコンピュータの上で育ちました。
PDP-11 のアーキテクチャは、整数型のサイズ、ポインタ演算、逐次的な命令実行、平坦なメモリ空間といった点で、C の言語モデルとよく対応していました。
C のポインタがそのままアドレスになり、C の式がそのまま数命令に落ちる、という感覚は当時の機械では大きく外れていなかったのです2。
「C は機械に近い」という感覚は、当時の文脈では正しかったといえます。
1.2. 現代 CPU は別の生き物
現代の CPU は、プログラムに書かれた命令を上から順に一つずつ実行しているわけではありません。
アウト・オブ・オーダー実行 (out-of-order execution) とは、命令をプログラム上の順序ではなく、データの依存関係に応じて並べ替えて処理する仕組みです。
投機実行 (speculative execution) は、分岐の結果が確定する前に、おそらくこちらだろうという方向に先行して計算を進めます3。
分岐予測ユニットがその判断を担い、外れた場合は巻き戻します。
SIMD (Single Instruction, Multiple Data) は一つの命令で複数のデータを同時に処理する機構で、画像処理や数値計算では大きな性能差を生みます4。
キャッシュ階層は L1、L2、L3 と複数段あり、メモリアクセスのパターンが性能を大きく左右します。
これらの仕組みは、C の言語仕様の外にあります。
C のソースコードを見ているだけでは、プログラムが実際にどう実行されるかは見えてきません。
2. C の抽象機械が想定しているもの
C の言語仕様は「観測可能な振る舞い (observable behavior)」が等しければ、コンパイラはコードをどう変形してもよいという考え方を採っています。
これを as-if rule といいます5。
「書かれた通りに実行したかのように見えればよい」という規則です。
つまり、C のソースコードは実行手順の厳密な記述ではなく、コンパイラが変形するための素材でもあります。
C の抽象機械は逐次実行と平坦なメモリを中心に設計されていて、キャッシュ階層も投機実行も SIMD も、その仕様には含まれていません。
「C では SIMD やプリフェッチがまったく使えない」ということではありません。
コンパイラ組み込み関数 (intrinsics) やアセンブリの埋め込みで使うことはできます。
ただしそれは標準 C ではなく、対象 CPU とコンパイラへの依存を明示的に引き受ける選択です。
2.1. 「C は速い」の本当の理由
C が速いのは、C の抽象機械が現代 CPU の動作をそのまま反映しているからではありません。
C の仕様には、未定義動作 (undefined behavior) という概念があります。
配列の範囲外アクセス、符号付き整数のオーバーフロー、NULL ポインタの参照などが該当し、言語仕様がその結果を保証しない動作のことです。
コンパイラはこれらが起きないと仮定して、大胆な変換を行えます。
エイリアス解析はポインタが同じメモリを指していないという前提に基づく最適化で、ループの自動ベクトル化なども同様の仮定の上に成り立っています6。
C のコードが速いのは、現代のコンパイラと CPU がそのギャップを埋めているからです。
C のソースと実行コードの間には、思っている以上に大きな距離があります。
2.2. 皮肉な反転
C のプログラマはしばしば、Java や Python、Lisp などに対して「コンパイラやランタイムが賢ければ速くなる、というのは甘い見通しだ」と言います。
しかし現代の C も、「十分に賢いコンパイラ」と「C を高速に動かすために設計された CPU」に、かなりの部分を依存しています。
Chisnall が指摘したのは、まさにこの逆転です。
C を速くするための議論が、他言語に対して批判として使ってきた議論と、構造として同じになっているわけです7。
3. では C は「高水準言語」なのか
そこまで振り切る必要はありません。
C は Python や JavaScript と比べれば、メモリ管理、ポインタ、データ構造の表現という点で、機械の動作に近い側にあります。
その意味では相対的に低水準です。
一方で、現代 CPU のキャッシュ挙動、命令スケジューリング、投機実行の深さ、SIMD 幅といった性能に直結する要素を、C のソースコードで直接表現する手段は乏しい。
「現代の CPU に対しては、C をもって低水準とは言い切れない」という評価はそこから来ています。
「低水準」という言葉が時代と対象に依存することも、この議論の背景にあります。
アセンブリでさえ、マイクロアーキテクチャの内部から見れば十分に低水準ではない、という立場もあるくらいです8。
3.1. 何が変わるか
C で書いたコードが「機械に近いから速い」という理由づけは、かなり怪しいです。
実際に速くするためには、コンパイラがどう変換するか、キャッシュアクセスのパターンはどうか、ループが自動ベクトル化されるかどうか、といった視点が必要になります。
C のソースを読んで性能を推測するより、生成されたアセンブリやプロファイル結果を見る方が正確なことが多いです。
「C は低水準だから速い」という文脈で C を選ぶとき、その前提は一度確認した方がよいでしょう。
高速化の恩恵の多くはコンパイラ最適化から来ていて、それは C 固有の特権ではありません。
Rust や、適切に書かれた他の言語でも同じ恩恵を受けられることがあります9。
「C が低水準だった時代の機械像」は、現代の CPU にはもう当てはまりません。
C の抽象機械は逐次実行と平坦なメモリを想定していますが、実際の性能はその抽象の外にある仕組みによって成り立っています。
C のソースコードと実際の実行の間を埋めているのは、機械に近い言語設計ではなく、コンパイラと CPU が協調して動く変換の層です。
- ACM Queue Volume 16, Issue 2 (2018年4月30日公開)。後に Communications of the ACM Vol.61 No.7 (2018年7月) にも転載された。DOI: 10.1145/3212477.3212479 – C Is Not a Low-level Language
- C は Dennis Ritchie と Ken Thompson が Bell Labs で Unix を開発する過程で生まれた。Thompson が PDP-7 上で Unix の初版を書き、その後 PDP-11 への移植にあわせて Ritchie が C を設計した。Unix のカーネルが C で書き直されたのは1973年のことで、C と PDP-11 は互いを前提に発展した。 – The Development of the C Language (Dennis M. Ritchie)
- Chisnall の論文は Spectre・Meltdown 脆弱性の公開直後に書かれており、投機実行が引き起こしたセキュリティ問題をその動機の一つとして挙げている。Spectre と Meltdown は2018年1月3日に公開され、ほぼすべての現代 CPU に影響した。 – Meltdown and Spectre Side-Channel Vulnerability Guidance (CISA)
- x86 アーキテクチャでの SIMD 命令セットは、Intel が1999年に Pentium III で導入した SSE から始まり、SSE2〜SSE4、AVX(2011年、256ビット幅)、AVX-512(2017年、512ビット幅)へと拡張されてきた。標準 C には SIMD を直接表現する構文がなく、利用するには処理系依存の組み込み関数を使う必要がある。 – Streaming SIMD Extensions – Wikipedia
- as-if rule は ISO/IEC 9899(C 標準)の §5.1.2.3 に規定されている。「抽象機械の意味論に従って実行したかのように見える限り、実装はいかなる変換を行ってもよい」という趣旨の規則で、コンパイラ最適化の根拠となっている。 – ISO/IEC 9899:1999 §5.1.2.3
- これは strict aliasing rule と呼ばれる。C 標準では異なる型のポインタが同じメモリを指すことを原則として未定義動作とし、コンパイラはそれが起きないと仮定して最適化できる。Linux カーネルは gcc の strict aliasing 最適化によりコードが壊れる問題が発生したため、現在も -fno-strict-aliasing フラグをデフォルトで使用している。 – Undefined Behaviour in C: The Compiler’s Licence to Delete Your Code (SpeyTech)
- 「十分に賢いコンパイラ (Sufficiently Smart Compiler)」という言い回しは、高水準言語の支持者が「コンパイラが賢ければ性能は問題ない」と主張するときに使われ、しばしば現実的でない楽観論として批判される。Chisnall はこの皮肉を、C 擁護者自身が同じ議論に依存していることを示す文脈で用いている。 – C Is Not a Low-level Language (ACM Queue)
- 現代の CPU はアウト・オブ・オーダー実行や投機実行により、アセンブリの命令順序通りに実行しない。アセンブリは CPU の「命令セット(ISA)」を記述するが、ISA の下にあるマイクロアーキテクチャ(実際の実行ユニットやパイプライン)は ISA と別物であり、アセンブリはそれを直接制御しない。 – Spectre (security vulnerability) – Wikipedia
- Rust の所有権モデルは、コンパイル時にポインタのエイリアスを型システムのレベルで制限する。これにより C の strict aliasing rule と同等以上の保証をコンパイラが静的に検証でき、エイリアス解析に基づく最適化を安全に適用できる。 – Memory Safety and Performance: Rust’s Theoretical Edge Over Traditional Languages