本物の ALU
整数の幅が決まり、SHA-256 probe が偽装していた bitwise 演算子がついに存在できるようになった。6 つの builtin —— and, or, xor, not, 左シフト、算術右シフト —— が 4 backend 全部で機械命令に一斉に落ちる。ハッシュの偽装算術 ALU に差し替えると、1 block が 29 ミリ秒から 7 マイクロ秒未満へ、約 4000 倍速くなった。面白いのは書き換えが露わにした残骸だ: 間違った幅のまま残された一時変数、2 backend が emit し忘れた helper、そして前話自身の changelog にあった誤りの一行。
前話は整数の幅を決めた。この話はその地ならしを、SHA-256 probe が本当に欲しかった機能に使い、 それを足したことで見えるようになった散らかりを片付ける —— そちらの方が物語の正直な半分だ。
6 つの builtin、各々一つの機械命令
bitwise 算術は小賢しさの出る幕ではない。言語は 6 つの builtin を得た —— AND、OR、XOR、NOT、
左シフト、算術(符号伝播)右シフト —— そして各々が機械そのものの命令にちょうど落ちる: C の
& 一家の演算子、wasm の i32.and 一家の命令、LLVM の and i32 一家、interpreter の native な
land 一家。infix 演算子でなく関数にしたのは、数話前の Unicode の仕事と同じ本能だ: 能力をまず
素の builtin として足し、演算子構文を主張するのは推測でなく本物の pain に後で任せる。標準
ライブラリの prelude はどの backend でも走らねばならないので、6 つは同一リリースで構成上 4
backend に着地した。3 backend が持って 1 つが持たない窓は一度も無かった。
payoff は SHA-256 の example だった。その inner loop は全ての AND と XOR を 32 反復の除算と剰余の ループで偽装していた —— 正しく、そして醜悪に遅い。本物の演算子に差し替えると、1 compression block が interpreter で約 29 ミリ秒から native 6.7 マイクロ秒へ —— およそ 4000 倍速く —— NIST test vector を全部通したまま。偽装はアルゴリズムを証明する良い方法だった。走らせるには最悪の方法 だった。
書き換えが光の中に引きずり出したもの
綺麗な機能なら短い話になる。この話が長さを稼ぐのは、書き換えが偶然露わにした無関係な 3 つの
ものからだ。第一: abs、min、max、clamp が内部の一時変数をまだ旧 32-bit の C 型で宣言して
いた。整数移行が backend の残りを一掃したときに取り残されたのだ —— だから 20 億超えの値の min
が黙って truncate した。前話とまったく同じバグが、誰も再確認しようと思わなかった helper に潜んで
いた。第二: 整数を変数経由で文字列に変換すると、top-level 束縛の下では show builtin だけが
登録していた helper 関数を参照した —— だから emit されたモジュールは定義されない関数を名指した。
wasm と LLVM の両 backend で、probe の素朴な print (str_of_int x) がついに引き金を引いた潜在
クラッシュだ。第三、そして最も謙虚にさせるもの: 前話の changelog は LLVM backend の整数が既に
64-bit だと主張していた。測ると違った —— LLVM の int は wasm と同じ 32-bit だ。主張はその場で
訂正され、LLVM も wasm と同じ範囲外リテラルの compile エラーを得て、64-bit 化は SHA-256 example を
常設の forcing program として deferred リストに載った。教訓はこの Part が教え続けているものだ:
機能は小さく明るいものであり、それが落とす影こそが実際の仕事の在り処だ。次の話は bytes の物語に
戻る —— ファイルの中の数ではなく、ファイルのバイトがそもそもプログラムに入れるのか、の話だ。