『コンピュータ・システム プログラマの視点から』 はコンピュータ・サイエンスの教科書です.
コードを書いて, コンパイルして, プログラムを実行する一連の流れにおいて, コンピュータの中では実際のところ何が起きているのかを知りたい人のための本です.
扱う範囲が幅広く
- CPU アーキテクチャ
- メモリ階層
- リンクの仕組み
- 仮想メモリ
- 並行プログラミング
など, 盛り沢山な内容となっています.
非常に分厚い本で 900 ページ近くあるのですが, これでも内容は絞られています.
副題の「プログラマの視点から」というのは大事な指針です. 多岐に渡る本書の内容は, どれも「C 言語を書くときに知っておいたほうが良いかどうか」という基準で取捨選択されています.
例えばアセンブラを学びますが, アセンブラを自力で書けるようになることは目的としていません. そうではなく, コンパイラが出力したアセンブラを読んで挙動を追ったり性能を最適化したりすることができるようになることが目的です.
ほぼ全てのものが順を追って説明されるので前提となる知識は少ないです (大学のコンピュータ・サイエンス入門コースを元にした本らしい). 例えば CPU のパイプライン制御の章では「論理ゲートとは」というところから話が始まります.
個人的には, これまで学んできたことが関連付けられたり補強されたり, 知識や理解を整理する良い機会となりました.
以下印象に残った点をかいつまんでまとめます.
浮動小数点
浮動小数点はなんとなく地味な存在だと思っていて, これまであまり深く考えたことがなかったのですが, 今更ながらよく考えられたフォーマットだなと思いました.
昇順に並べた時ビットが符号なし整数と同じになる, 非正規化数から最小の正規化数まで等間隔に滑らかにつながると言う事実を知って驚きました. かつては仕様が乱立していた時代もあったそうですが, 今の形に落ち着いたのも納得です.
実用的には小数と整数の変換で誤差が出るかどうか, 丸めがどのように行われるのかといったことは把握しておくと役に立つかもしれないと思いました.
アセンブリ
C 言語がどのようにアセンブリに変換されるのかが説明されています. 例えば if は条件ジャンプを使って実現されることをなどを学びます.
このあたりは昔 『コンピュータシステムの理論と実装』で自力で考えたことがありました が, 本書を先に読んでいれば楽だったかもしれないと思いました.
多くは既知の内容でしたが, switch については認識を改めました. これまで if-else と同じようなものだろうと思っていたのですが, アセンブリのレベルで見ると実装方法が異なります. switch はジャンプテーブルを用いて対象の case に直接ジャンプするので効率が良いです. if-else の連続だとその回数分条件式が評価されますしね.
switch の case には定数しか書けないというような制約があったりして疑問に思っていたのですが, アセンブリを知れば納得です.
CPU のパイプライン処理
命令レベル並列化が行われているということは知っていましたが, 具体的な仕組みは全く知りませんでした. 一つの機械語命令を実行する処理をいくつかのステージに分けて, 回路の間に挟んだレジスタに計算結果を保存することで並行処理が可能になるという素晴らしいアイデアです.
本書では x86-64 を元にした独自の小さな命令セット Y86-64 を定義して, HDL (ハードウェア記述言語) で実装していくという流れです.
後半のパイプライン制御のあたりでは話が複雑過ぎて, じっくり読む気にもならず半分も理解できてない気がします.
特に複雑なのは例外的なパターンへの対処で, 前の命令への依存, return アドレス, 分岐予測といったことが特殊な場合として扱われます. そしてこれらが各ステージに存在する組み合わせも考えると, いよいよ特殊ケースが複雑になりすぎます.
これは別に本書のやり方が悪いというわけではなく, そもそも命令を並列に行うといういかにも難しそうな処理なので実装が難しいのは仕方ないかもしれません. それにしても, もっときれいな解決法がないものかなぁと思いますが.
プログラムの性能最適化
ある小さなコードを題材とし, アセンブラや CPU のアーキテクチャの知識を動員して性能の最適化を進めて行きます.
いくつかの工夫を経て着実に最適化を進めて, 最終的には CPU の命令レベル並列化を利用しスループット限界に迫る流れは読んでいてわくわくしました.
実用的にどこまで最適化をするかというのは考えどころかもしれません. 個人的には, 簡単にやれることは常にやる, それ以上の最適化は特別に高速化が必要なときに行うという指針が良いのではないかと思いました. 限界まで最適化するとマシンの性能に依存したコードになって移植性が落ちたり, 可読性が下がったりするためです.
- 常に意識すること
- 関数呼び出しを減らす
- メモリアクセスを減らす (ポインタの指す値をローカル変数にキャッシュするなど)
- 普段はやらないこと
- ループアンローリング
- 条件演算を条件付き move にコンパイルしやすいように書き直す
続く章でキャッシュの仕組みについての説明があります. ハードウェアレベルの仕組みがいかにプログラムの性能に影響を与えるかを知って驚きました.
リンカ
少し浮いているように見えるリンカの章ですが, リンカの仕組みを理解していれば, 不可解なエラーに苛立つことをなくせたり, 共有ライブラリを便利に使ったりできるとのことです.
リンカの役割は大雑把に言えば以下の 2 つです.
- シンボル解決: シンボルの参照をシンボルの定義に対応付ける
- 再配置: 最終的なシンボルのアドレスを決めて参照を修正する
オブジェクトファイルの構造を知って, これがメモリにロードされるのかと感動しました. objdump
や readelf
でファイルの中を見るのは理解の大きな助けになりました.
同名のシンボルがあるときの微妙な挙動や静的ライブラリ利用時の参照解決の仕方などは知らないとハマりそうで, 学ぶ機会があって良かったと思いました.
仮想メモリ
仮想メモリは各プロセスがメモリ全体を専有しているように見せる抽象化の仕組みです. 仮想メモリのアドレスを物理メモリのアドレスに変換する仕組みや, 動的なメモリ割り当てについて説明されています.
仮想メモリを知って, ようやく fork 時に何が共有されるのかということや, 共有ライブラリがどのように共有されるのかが腑に落ちた気がします.
mmap
というライブラリ関数を使って, ファイルの内容を仮想メモリに直接読み込めるのは驚きました. 仕組みを知れば, 確かにこういう関数があっても不思議ではないと思います. しかし, read
を使わずにファイルの中身を得ることができるのですが, どういった用途があるのでしょう. ファイルディスクリプタやストリームが不要なら read
や fread
を使うよりも効率が良い?
並行処理
最後はソケットと HTTP サーバです.
並行処理はプロセス, スレッド, IO 多重化という方法があるという話です.
このあたり, いまいちよく納得できていないので更に深く学んでみたいところです. おそらく実際のサーバはこれらを組み合わせて使っているのではないかと思いますが, どうなのでしょう. 少し調べてみたところ, イベント駆動, C10K 問題などがキーワードになりそうです.
考えてみれば Go の goroutine などは, おそらく従来の平行処理の課題に対する回答なのでしょうね. まずは何が課題なのかを理解するところから勉強を進めたいと思っています.
CGI を使って動的にコンテンツを生成する HTTP サーバの例も紹介されていました. 高度な仕組みでも低レベルな仕組みの組み合わせで作られているのだろうという実感が湧いてきます.
おそらく CGI のようなアイデアは昔からあって, 今の Web 界隈ではさらに洗練された手法が取られているのではないかと思いますが, 知らなかったのでためになりました.
結語
読み切るのにかなり時間が掛かりましたが, 幅広いトピックに触れられて楽しく読めました. 本当は本書にあるような内容をもっと早いうちから習得して常識にできていればよかったのでしょうが, 楽しみながら読めるだけ成長したと思いたいです.
たとえ高級言語を使ってプログラミングをするにしても, 深いところまでたどって物事を考えるには低レイヤーの知識が必要だと実感しました. 低レイヤーは学びがいがあって面白いです.