百万要素
スケール probe はサイズを上げながら一つの問いを訊いた: どの prelude 関数が百万要素を生き延びるか?sort が最初に、最悪の形で死んだ —— 二次関数的かつスタック大食いを同時に。置き換えの merge sort は、全関数が末尾再帰に見えるのに 20 万で落ち続け、address sanitizer は TCO 待ち伏せカタログの 4 匹目を指した: tuple を返すと呼び出しは sret 形式になり、sret は sibling-call 最適化を殺す。修正はホットパスで tuple を返すのをやめること。続く監査は 10 の prelude 関数を accumulator 形式に書き直した。百万要素はいま 0.65 秒でソートされ —— Part XIII は方法自身の帳簿で閉じる。
Part 最後の probe は、手持ちで最も無骨な問いを訊いた: prelude のリスト関数群を取り、1 万要素、 次に 10 万、次に 100 万を食わせ、どれが帰ってくるか書き留める。4 backend とメモリモデルを持つ 言語には格下に感じる種類の測定で —— だからこそ一度もやられていなかった。答えは二つのカテゴリで 届いた: 二次関数的な関数と、末尾再帰でない関数。sort は両方だった。prelude の挿入ソート —— リストが 10 個の設定項目を持っていた時代の正直なプレースホルダ —— は 1 万要素で数分かかり、 10 万では遅くなる前にスタックを吹き飛ばした。
四匹目の待ち伏せ
置き換えは、リストライブラリに配列を持たない言語のために設計された安定 merge sort だった: ポインタ追跡でなく長さで分割、反転 accumulator を通した merge、全関数を末尾再帰で記述。それでも 20 万要素で落ちた。全関数が末尾再帰に見え、生成された C は呼び出しが末尾位置にあることを 確認し、それでもスタックは伸びた。address sanitizer が飯代を稼ぎ、分割関数を指した —— 二つの リストの tuple を返す、あの関数だ。struct を値で返す関数は sret 形式にコンパイルされる: 呼び出し側が自分のフレーム内の領域への隠しポインタを渡す。呼び出し元のフレームへ書き込ま ねばならない呼び出しは sibling call になれず、最適化器は黙って辞退する。プロジェクトの TCO 待ち伏せカタログの 4 匹目 —— escape するスタックアドレス、inline 化された asprintf、 thread-local キャッシュに続く —— で、形は他と同じだ: ソースは無実、裏切りは C ツールチェーンの 細則で起きる。修正はホットパスの de-tuple: 分割は二つの単リスト関数になり、sort は立ち上がった。
監査と、Part の帳簿
sret が sort を待ち伏せたなら、他に何が待っている?監査は 50 余りの prelude 関数全てを歩き、 二つの問いを訊いた —— 再帰はスタックを伸ばすか、仕事は二次関数的に増えるか。10 関数が落ち、 accumulator 形式に書き直された: length、map、range、take、zip、concat、flat-map、max、min など。 どれもテストが使うリスト長では見えず、どれもスケールでは致命的だった。再走は別のライブラリの ように読める: 百万要素のソートが 0.65 秒、書き直された全関数が百万までフラット。そして、これで Part の帳簿が閉じる。6 話、パターンは両方向に成り立つ: 安い測定は実仕事を見つけ(trait システムは 単相化に溶け、短絡評価は 2 backend で欠け、wasm の回収、codepoint view、このスケール一巡)—— そして二度、仕事が既に済んでいるのを見つけた。それはそれでデータだ。方法の要約は一行に収まる: probe が仕事を選ぶ。そして probe は安い。 次の Part が何を測るかはまだ決まっていない。 それこそが要点だ —— 何か小さなものが言語に打ち込まれ、言語が答えるだろう。