Forth によるさっぱり週末でないレイトレーシング
成果物
kirisaki/forth-raytracing: An implementation of ray-tracing in Forth.
はじまり
週末レイトレーシング(原題: Ray Tracing in One Weekend) という課題がある。これは簡単なレイトレーシングを土日で書き上げようと言うものだ。 私は以前 Rust を用い実装したことがあるのだが、今回はそれを Forth という言語で書こうと思ったわけだ。正気か?
なぜ狂気か
Forth で週末レイトレーシングを書くのは割と正気を失っている。
-
ガベージコレクションなんてものはない
- 所有権みたいなのもない
- メモリを生のまま扱う
-
変数がない
- あるにはあるがだいぶ独特
- gforth の浮動小数点数には本当に無い
-
名前空間がないワードリストがありました(2025.10.11 追記) -
型がない
-
スタックに値を詰めて関数(ワードと呼ぶ)を呼び出しスタックに結果を詰めて返す
- 当然のことながらスタックのアンダーフローやオーバーフローが頻発する
ないないづくしである。コンパイル時チェックというものがないので C よりもつらい。アセンブラよりはマシという程度か、 こんな言語でなぜ書こうと思ったのか。そこにスタックがあったから。
とはいえ粛々と書けばなんとかなる
癖はあるものの仕組みを理解してしまえば割と簡単である。例えば乱数を生成するコードは以下のようになる。 (もちろん乱数のライブラリはないので自分で書かなければいけない。)
\ Generate a random number
: rand ( u -- u u )
\ Xorshift64 PRNG
dup 0= if drop 1 then
dup 12 rshift xor
dup 25 lshift xor
dup 27 rshift xor
2685821657736338717 *
;
( u -- u u ) と丸括弧で囲われた文字列は実はコメントなのだが「unsigned integer を 1 つ詰めて呼び出すと unsigned integer を 2 つ詰めて返しますよ」と言っている。
言っているだけで何も拘束力はないが、ちゃんと動作を確かめつつ書いていけば乱数生成くらいなら簡単に書ける。
変数もある。
\ Create a new ray
: ray-new ( origin direction pool -- ray-addr )
locals| p d o |
p pool-alloc
dup r-origin o swap vec3-move
dup r-direction d swap vec3-move
;
こんな感じで locals| ... | と書くとスタックから取り出した順に変数を割り当てる。なのでコメントとは逆順に割り当てられていく。
これに起因するバグが頻発する。あと gforth の浮動小数点数スタックには locals| ... | のような仕組みがないので完全にスタックの動きを読まなければいけない。
このあたりの勘所(とメモリの扱い方)さえわかっていればまあまあ書ける。そんな感じで課題を進めていき週末に終わると思われていた。
リーク、リーク、リーク
メモリの管理なんて OS に任せておけばいい――もちろんサーバのような定常で動くソフトウェアなら別だがどうせワンショットで終わるプログラム、 そう思い私はメモリ管理を全くしていなかった。その結果訪れる毎秒 300 MB のメモリリーク。メモリは 128 GB 積んでいるがそのメモリがあれよあれよと埋まっていく。 その結果最終課題である球をたくさん散りばめた画像の生成ができなかった。そこから始まる地獄のリーク潰し。しかし一向にメモリリークは直らない。 そこで決断をする。
――はじめから書き直してまともなメモリ管理をしよう。
秩序の導入
初めから書き直すにしても malloc (allocate) と free を頻繁にやって良いものか。
ChatGPT くんにお伺いを立てるとやっぱり実行速度に影響があるらしい。そこで提案されたのがアリーナである。
アイディアはシンプルでアリーナと呼ばれる大きいメモリ領域を確保したあとそこから自前の allocate と free でメモリを管理するというものである。
またプールも導入した。アリーナではメモリを末尾に確保しながらアドレスを進めていくだけなので無駄遣いが多くなる。
プールでは同じサイズのメモリをリストでまとめて管理し空きはそこから捻出することによってメモリを使い回すというものだった。
1 週間ほどかけてアリーナとプールを導入したコードに直し、そしてついには上の画像を生成した。私の勝ちである。
課題もある
完成はしたものの課題もある
実行速度が遅い
上の 1024x576 の画像を生成するのに 4 時間もかかっている。最適化もへったくれもないので書いたコードがそのまま実行される。なのでナイーヴな実装では時間がかかる。
呼び出し規約がばらばら
場当たり的な設計をしまくった結果引数の順序がバラバラになってしまった。 Forth 特有の問題ではない……と言いたいが Forth ではスタックに積まれている順番でワードが呼び出されるため後から呼び出し順を変えるのが七面倒なのである。 とはいえプールを 4 種類も渡してるのは流石に構造体作りなさいよとは思った。
おわりに
Forth を書くのはつらかったが、とても楽しかった。C 言語のようにコンピュータを直に触っている実感がありつつも LISP のような書き味がある。 是非試してみてはいかがだろうか。