メソッド呼び出しをポータブルかつ高速に書けないか問題
FFIの考察が大分進んできたので、次の課題。yuniライブラリの重要なターゲットはゲームなので、プラットフォームAPIに最適化されたコードを書けるようにしておく必要がある。このとき、Scheme側のAPIは同じだけど呼び出すDLLが違うライブラリをホットなパスで使うことになる。
課題
- 可能な限りパフォーマンスロスなく、同じAPIをエクスポートする複数のライブラリを状況によって呼び分けたい
単純には、何か"中間ライブラリ"を用意して、適当なフラグを見て分岐する方法が有る。例えばレンダラがDirectXとOpenGLの両方をサポートしていたとして:
(import (prefix (backend dx) dx:) (prefix (backend gl) gl:)) (define (draw-model ctx model) (if direct-x-mode? (dx:draw-model ctx model) (gl:draw-model ctx model)))
これは分岐ぶんのコストが掛かるし、サポートするバックエンドが増えるたびに分岐が増える。あと、そもそもR7RSマクロで書けない(シンボルを合成する方法が存在しない)し、中間ライブラリが事前に全てのバックエンドを知っている必要がある。
さらに、プラットフォーム依存コードはビルドの問題もある; Win32固有のバックエンドライブラリはUnix上でビルドできるとは限らないため、import自体できないかもしれない。もっとspecificな話としてはゲーム機のSDKはNDA配下なのでライブラリの存在自体公開コードに書けないという問題が有り、中間ライブラリ自体も公開できなくなってしまう。
他所のOOPをサポートしたプログラミング言語では、これはmethod callに抽象化されているので、特にコレといった考察は必要無い。
ctx = new Backend(gl); ctx.draw_model(model);
R6RS/R7RS Schemeで、このような記述をして、かつ、パフォーマンスを維持するにはどのような抽象化が望ましいかを考える必要がある。
方法1: Message passing、構造体、CLOS、...
よくある方法としてはmessage passingが有る。つまり、ライブラリがエクスポートする手続きは(名前の衝突を避けつつ)1つに絞り、メソッド名はシンボルで指定させる。
(define (ctx/gl method . params) (case method ((flip) (apply flip params)) ((draw-model) (apply draw-model params)))) ;; ctx = new Backend(gl); (define ctx ctx/gl) ;; ctx.draw_model(model); (ctx 'draw-model model)
...言うまでもなく遅そうである(個人的には良く使ってるけど)。改善できそうなアイデアとしては、applyの代りに手続きを返してそれを呼ばせるという方法がある。
(define (ctx/gl method) (define (flip) ...) (define (draw-model model) ...) (case method ((flip) flip) ((draw-model) draw-model))) ;; ctx.draw_model(model); ((ctx 'draw-model) model)
記憶が正しければ、ctxが定数であるような状況ではChickenはこれをインライン化でき、直接の呼び出しになる。R7RSには構造体を定義するためのdefine-record-typeが有り、それを使ってほぼ同じことが実現できるが、chickenはrecordを通してしまうと呼び出しをインライン化できなかった記憶がある(要検証)。
(Typed)Racketのような型の有る処理系であったり、GaucheやSagittariusのようなCLOS風のオブジェクトシステムの有る処理系であれば、それらを活用するのが多分最速だろう。
要するに、呼び出し可能なメソッドを備える構造体を(最適化可能な形で)宣言する方法は処理系毎にまちまちであり、何か抽象化レイヤを用意する必要がありそうと言える。recordに手続きを入れておけば後は面倒を見てくれるという状況であれば色々と簡略化できるんだけど。。
方法2: 値をキャッシュ(メモ化)させる構文を用意する
呼び出し毎にディスパッチさせるのは勿体無いため、メソッドの解決は1発だけにし、それ以降は解決しないという方法が考えられる。
簡単には、中間ライブラリを用意し、中間ライブラリ側にキャッシュを持たせれば良い。
(define current-ctx #f) (define draw-model/cache #f) (define (draw-model ctx model) (unless (eq? ctx current-ctx) (set! draw-model/cache #f)) (unless draw-model/cache (set! current-ctx ctx) (set! draw-model/cache (ctx 'draw-model))) (draw-model/cache model))
... これもJITC側に相当機能がある処理系が(もし有れば)冗長なので止めた方が良いかもしれない。あと、これをR7RSのsyntax-rulesで良く記述する方法が思いつかない。define-interface / declare-interfaceみたいな名前の構文を作って、
(library (backend) (define-interface graphics-device ;; Javaで言うInterface宣言 (method (draw-model model)) (method (flip)))) (library (backend gl) (define (draw-model/gl model) ...) (define (flip/gl) ...) (declare-interface graphics-device ;; Javaで言うInterfaceを継承したクラスの宣言 (method (draw-model model) (draw-model/gl model)) (method (flip) (flip/gl))))
みたいに書かせることが果たしてR7RSで可能なのか。。