もうちょっと真面目な光線追跡 - その2 [光線追跡エンジンを作る]
去年始めたものの、Bezier近似に割り込まれたので忘れていたSwiftによる光線追跡エンジンの続き。Objective-CではなくSwiftで実装するにあたって気になることがあって、設計から離れて考えてみたいことがある。Objective-CからSwiftに移ってきた人はみんな悩むんじゃないか、と思う、あるオブジェクトをclassにするかstructにするか、ということ。Webにはわかりやすく説明してくれているサイトがいっぱいあって、値型/参照型、継承可/不可などの違いがあることがわかる。僕はMulti-threadに対しての有利不利について考えてみる....
例えば僕のこの光線追跡エンジンの場合、単一の媒質内の光線はstrutでいい。作られてしまえば変更されることはない。その意味で面もstructでいい。媒質オブジェクトはどうか。光線の波長によって屈折率という内部状態が決定される。その意味ではclassがふさわしいような気がするけど、内部状態があるからといってclassでなければならないというわけではない。
例えば、SwiftUIのオブジェクト(Appleのいいかたでは「インスタンス」)のうち、テキストやボタンなどの目に見えるものは全部が(チェックしてないけど)structである。入れ子にできる描画領域やテキストボックスなど大きくて複雑なオブジェクトもstructである。
structは値型なので複数のオブジェクトからアクセスされるときコピーが作られる。このルールのおかげでclass(あるいはクロージャ)オブジェクトと違って参照カウタンを持たなくても、ARCがどの時点でオブジェクトをリリースするかを決める(というか、コンパイラがスタティックに決める。Objective-CではこれもARCの仕事と説明されていた)ことができる。たくさんのオブジェクトからアクセスを受けるstructはたくさん作られることになるけど、copy-on-writeという仕組みがあって、書き換えがない限りは同じものが使われるようになっている。しかしcopy-on-writeのタイミングをプログラマに伝えたりプログラマが制御する手段は提供されていないので、もし、大きなstructのコピーが起こらないようにするには書き換えのタイミングを意識する必要がある。
SwiftUIではオブジェクトがスタティックな、つまりSwiftUIの記述に従ってstructが作られたあとは全体が破棄されるまで構造が変更されないツリーになっていて、あるボタンにアクセスしようとすると、入れ子になったViewをたどるようになっているようである。普通のユーザインターフェイスを作る上ではそれを気にしなくてもいいようになっているらしい。
Objective-Cではオブジェクトの間でダイナミックな相互作用が発生するイメージだったけど、Swiftはその意味ですごくスタティックである。Objective-Cのオブジェクトは必要になったときに作られて、それらのオブジェクト同士のやったりとったりがプログラミングだ(オブジェクト指向の考え方ではそういうイメージが多いような気がする)、というのとは違って、Swiftはコンパイル時点でなるべく多くのことを決定してしまおうという思想があるらしくて、実行時点ではなくソースに記述された時点で決めてしまって、実行時はそれをなぞるだけにしたい、という感じになっている。
したがってオブジェクトの生存期間もスタックで決まってしまう、つまり多くのオブジェクトの生存期間は入れ子になっていて、そうでないオブジェクトはデータをやりとりする通信用などのオブジェクト(例えば関数の戻り値として)に限られる、という風に思える。
だから良いの悪いの、という話ではなく、class vs. structはマルチスレッドとの関係で重要な問題である。これからのMacは16コア32コアといった超マルチコアの(GPUコアの数が、ではなく)CPUが当たり前になる(と思う。たぶん)。これを使い倒そうとすると、コア数ぶんのthreadを滞りなく走らせる必要がある。そのためには粒度(threadでの実行単位)を小さくしてたくさん投入できるようにする。しかしそのときmutexでロックしないといけないデータがたくさんあると、大きなペナルティになってしまう。できれば排他制御の必要のない、つまり走り出したらひと段落するまで外部に依存しないプログラミングが望ましい。
それを考えるとstructはclassや変数キャプチャのあるクロージャに比べて有利である。なぜならstrcutでは内部状態を変更するような関数はmutatingというキーワードをつけることになっている。structは完全にImmutable、つまり全てのプロパティがlet宣言されているなら、そのstructの初期化も含めてthread-safeであある。またvarで宣言された変数を持っていてもcopy-on-writeの動作そのものはthread-safe(Appleのドキュメントにそう言う記述を見つけることができなかったけど、さっきも書いた通りcopy-on-writeのタイミングをプログラマが制御する手段は提供されていないので、そうでなければならない)である。
さらにthreadに投入する単位となる関数がmutatingでなければ、関数呼び出しによって変化するのはそのstructの外とスタック上だけなので、少なくともそのstructには排他制御が必要ない、ということを確定できる(その関数が別の関数を呼ぶ場合には、呼ばれる関数のツリーがmutatingでなければ。スタックの上は変化しても構わない。なぜならスタックはスレッドごとに独立しているので)。これはthread-safeなプログラミングをするときに「内部変数を持たない」という一番基本的な約束事のひとつだけど、structを使えばそれをコンパイラがチェックしてくれることになって、精神衛生上有利である。
光線追跡のプログラミングはもともとMulti-threadと相性がいい。例えば面での屈折を計算するとき、面の内部状態は変化せず、入射した光線から射出光線を決めるという作業をする。このときの実装上、面オブジェクトSurfaceがstructで、屈折を計算する関数reflactがmutatingでなければ、入射光線オブジェクトRay${}^i_n(n=0...N)$から射出光線オブジェクトRay${}^o_n$への計算ではnに関して独立に、つまり排他制御なしに実行できることがあきらかになる。そしていくつthreadがあってもSurface structはメモリ上のオブジェクトとしてひとつだけでいい。
これはもちろん光線追跡計算そのものの内部構造からきてるわけで、classで書いても同じだけど、structで書くことで明確にすることができる。こういう単純な場合ではそれほどありがたみはないけど、もっとたくさんのstructが関係するような場合、一連の作業の中でmutatingな関数が呼ばれないなら、新しいstructが作られたりしていても排他制御なしにthread-safeだと確定できる。letとvarの区別やmutatingキーワードはSwiftのシンタクスを決めるときにコンパイラでの最適化を念頭に置いた結果だろうけど、これはありがたい。
とはいうものの、Objective-Cに10年以上慣れてしまった体にとっては切り替えにかなりのエネルギーが必要になってしまう。その昔、CodeWarrior+PowerPlantからXcode+Objective-Cに切り替えるときにもかなり時間がかかって苦しんだ。もう一回それをやるのは、特に年寄りには厳しい山登りである。
例えば僕のこの光線追跡エンジンの場合、単一の媒質内の光線はstrutでいい。作られてしまえば変更されることはない。その意味で面もstructでいい。媒質オブジェクトはどうか。光線の波長によって屈折率という内部状態が決定される。その意味ではclassがふさわしいような気がするけど、内部状態があるからといってclassでなければならないというわけではない。
例えば、SwiftUIのオブジェクト(Appleのいいかたでは「インスタンス」)のうち、テキストやボタンなどの目に見えるものは全部が(チェックしてないけど)structである。入れ子にできる描画領域やテキストボックスなど大きくて複雑なオブジェクトもstructである。
structは値型なので複数のオブジェクトからアクセスされるときコピーが作られる。このルールのおかげでclass(あるいはクロージャ)オブジェクトと違って参照カウタンを持たなくても、ARCがどの時点でオブジェクトをリリースするかを決める(というか、コンパイラがスタティックに決める。Objective-CではこれもARCの仕事と説明されていた)ことができる。たくさんのオブジェクトからアクセスを受けるstructはたくさん作られることになるけど、copy-on-writeという仕組みがあって、書き換えがない限りは同じものが使われるようになっている。しかしcopy-on-writeのタイミングをプログラマに伝えたりプログラマが制御する手段は提供されていないので、もし、大きなstructのコピーが起こらないようにするには書き換えのタイミングを意識する必要がある。
SwiftUIではオブジェクトがスタティックな、つまりSwiftUIの記述に従ってstructが作られたあとは全体が破棄されるまで構造が変更されないツリーになっていて、あるボタンにアクセスしようとすると、入れ子になったViewをたどるようになっているようである。普通のユーザインターフェイスを作る上ではそれを気にしなくてもいいようになっているらしい。
Objective-Cではオブジェクトの間でダイナミックな相互作用が発生するイメージだったけど、Swiftはその意味ですごくスタティックである。Objective-Cのオブジェクトは必要になったときに作られて、それらのオブジェクト同士のやったりとったりがプログラミングだ(オブジェクト指向の考え方ではそういうイメージが多いような気がする)、というのとは違って、Swiftはコンパイル時点でなるべく多くのことを決定してしまおうという思想があるらしくて、実行時点ではなくソースに記述された時点で決めてしまって、実行時はそれをなぞるだけにしたい、という感じになっている。
したがってオブジェクトの生存期間もスタックで決まってしまう、つまり多くのオブジェクトの生存期間は入れ子になっていて、そうでないオブジェクトはデータをやりとりする通信用などのオブジェクト(例えば関数の戻り値として)に限られる、という風に思える。
だから良いの悪いの、という話ではなく、class vs. structはマルチスレッドとの関係で重要な問題である。これからのMacは16コア32コアといった超マルチコアの(GPUコアの数が、ではなく)CPUが当たり前になる(と思う。たぶん)。これを使い倒そうとすると、コア数ぶんのthreadを滞りなく走らせる必要がある。そのためには粒度(threadでの実行単位)を小さくしてたくさん投入できるようにする。しかしそのときmutexでロックしないといけないデータがたくさんあると、大きなペナルティになってしまう。できれば排他制御の必要のない、つまり走り出したらひと段落するまで外部に依存しないプログラミングが望ましい。
それを考えるとstructはclassや変数キャプチャのあるクロージャに比べて有利である。なぜならstrcutでは内部状態を変更するような関数はmutatingというキーワードをつけることになっている。structは完全にImmutable、つまり全てのプロパティがlet宣言されているなら、そのstructの初期化も含めてthread-safeであある。またvarで宣言された変数を持っていてもcopy-on-writeの動作そのものはthread-safe(Appleのドキュメントにそう言う記述を見つけることができなかったけど、さっきも書いた通りcopy-on-writeのタイミングをプログラマが制御する手段は提供されていないので、そうでなければならない)である。
さらにthreadに投入する単位となる関数がmutatingでなければ、関数呼び出しによって変化するのはそのstructの外とスタック上だけなので、少なくともそのstructには排他制御が必要ない、ということを確定できる(その関数が別の関数を呼ぶ場合には、呼ばれる関数のツリーがmutatingでなければ。スタックの上は変化しても構わない。なぜならスタックはスレッドごとに独立しているので)。これはthread-safeなプログラミングをするときに「内部変数を持たない」という一番基本的な約束事のひとつだけど、structを使えばそれをコンパイラがチェックしてくれることになって、精神衛生上有利である。
光線追跡のプログラミングはもともとMulti-threadと相性がいい。例えば面での屈折を計算するとき、面の内部状態は変化せず、入射した光線から射出光線を決めるという作業をする。このときの実装上、面オブジェクトSurfaceがstructで、屈折を計算する関数reflactがmutatingでなければ、入射光線オブジェクトRay${}^i_n(n=0...N)$から射出光線オブジェクトRay${}^o_n$への計算ではnに関して独立に、つまり排他制御なしに実行できることがあきらかになる。そしていくつthreadがあってもSurface structはメモリ上のオブジェクトとしてひとつだけでいい。
これはもちろん光線追跡計算そのものの内部構造からきてるわけで、classで書いても同じだけど、structで書くことで明確にすることができる。こういう単純な場合ではそれほどありがたみはないけど、もっとたくさんのstructが関係するような場合、一連の作業の中でmutatingな関数が呼ばれないなら、新しいstructが作られたりしていても排他制御なしにthread-safeだと確定できる。letとvarの区別やmutatingキーワードはSwiftのシンタクスを決めるときにコンパイラでの最適化を念頭に置いた結果だろうけど、これはありがたい。
とはいうものの、Objective-Cに10年以上慣れてしまった体にとっては切り替えにかなりのエネルギーが必要になってしまう。その昔、CodeWarrior+PowerPlantからXcode+Objective-Cに切り替えるときにもかなり時間がかかって苦しんだ。もう一回それをやるのは、特に年寄りには厳しい山登りである。
2021-02-28 21:04
nice!(0)
コメント(0)
コメント 0