言語を測る 1 行ループ
メモリ修正を設計する前に、規律は数字を要求した。probe は「割当てをやめないもっとも単純なプログラム」= ストリームの行数えだ。それはそもそも書けなかった —— read_line は interpreter にしか無かった。書けるようになると、測定は誰の物語とも一致することを拒んだ: string 版は行あたり 30 バイトが永久残留し、倹約のはずの byte バッファ版は 10 倍悪かった。その異常を追うと、コンパイラ史上もっとも波及の大きい性能バグに行き当たった —— そして修正後、数えるループは wc より痩せていた。
サーバのメモリ問題は thread と channel と store に絡まっていて、修正を考えるには動く部品が多すぎた。
方法は言う: 単離せよ。同じ病気を持つ最小のプログラムは、行を読み、眺め、忘れる単一スレッドのループ ——
言語自身で書いた wc -l に数百万行を食わせ、常駐メモリだけを出力として見る。per-item の回収がいつか
成立するなら、まずここで成立しなければならない。そして probe は最初の測定の前に、見慣れた形で仕事を
した: 言語には行読みの builtin があったが、interpreter にだけ —— native プログラムが発見し続ける
interpreter-only 一家の 6 匹目だ。read_stdin は設計からして全量読み。C backend が read_line を
覚えるまで、native のストリーミング行読みはそもそも書けなかった。小さなリリースを一つ挟んで、測定が
始められた。
二つの方言、二つの間違った数字
string 方言のカウンタ —— 慣用的なコード、read_line と str_len —— は予想どおりの病状を見せた:
常駐メモリが入力サイズを追いかけ、行あたりおよそ 30 バイトの永久残留。wc が 2.5 メガバイトで数える
800 万行に、4 分の 1 ギガバイト。全ての行文字列が program-lifetime region に着地して居座る。この数字が、
いずれ来る修正が裁かれる基準線になった。対照実験は言語を引き立てるはずだった: byte 方言 —— tcp_read
で一つの再利用バッファへ読み、mem_get_u8 で改行を数え、文字列を一切作らない —— は定数メモリで走る
はずだ。実際には 2 ギガバイトで走った。倹約版が浪費版の 10 倍悪い。言語について誰かが信じている
どんな物語も、これを説明できなかった。測定とはこのためにある: 全員のモデルと矛盾する数字は、バグの
現住所だ。
全ての呼び出しが家賃を払っていた
異常の二分探索は、誰も見たことのない場所へ導いた。それが言語でもっともありふれたコードの形の中に
隠れていたからだ。ML の多引数関数は単一引数関数の連鎖で、C backend は全ての呼び出しを段ごとに
コンパイルしていた: count buf i n acc は「count に buf を渡し、closure を受け取り、それに i を
食わせ、closure を受け取り……」になり —— その一段ごとに closure 環境を program-lifetime region に、
region の thread lock 越しに、毎呼び出し割り当てていた。3 億バイトを回るループの中の 4 引数関数は、
バイトごとに環境を 3 つ、永久に積む。全 dogfood の全ホットループが、この家賃を見えないまま払って
いた。byte 方言の probe は、それを 3 億回連続で払っただけだ。修正は古典そのもの —— top-level の
カリー化関数ごとに直接 N 引数の C 関数も emit し、ちょうど飽和した呼び出しをそこへ直結する。C の
引数評価順は未規定なので、一時変数で interpreter の左から右を固定する。部分適用は closure 連鎖の
まま。観測可能なものは何も変わらない。変わるのはメータだ: 環境ゼロ、lock 通行ゼロ、そして自己再帰は
本物の C 末尾呼び出しになる。
wc より痩せて、そして正直な分割
uncurrying が入ると、byte 方言のカウンタは 3 億バイトを 0.1 秒で、1.5 メガバイト・入力サイズに
よらず定数のメモリで処理した —— wc 自身より下だ。数字は気持ちいいが、診断の方が大事だった。
string 方言を測り直すと、前とほぼ同じ 4 分の 1 ギガバイト。それこそが要点で、二つの問題が綺麗に
分離された。一方は事故 —— 全ての飽和呼び出しに黙って課税していたコンパイル戦略で、これで永久に
廃止された。他方が本物の設計負債 —— 慣用的な string コードには per-line のメモリを返す術が依然
無い。永久なのは呼び出し規約でなく、値そのものだからだ。probe は probe の仕事をした ——「サーバが
どうにかして leak する」を二つの精密な文に変え、一つは既に直り、一つはもう避けられなくなった。
次の回はその避けられない方に取り組む —— ただしその前に寄り道が一つ: メモリ工事の段取りの最中、
generic なデータ構造が、interpreter とコンパイラがプログラムの意味について常には一致していない
ことを静かに実演してみせたのだ。