GPGPU上の言語という性質から来る特徴
MFGはGPGPU上で動くという事から他の言語と違うシンタックスになっている部分があるので、どうしてそうなっているのかという話をここに書いていく。
C言語ベースのシェーダー言語の問題点
OpenCL, Cuda, HLSL, Metalなどは全てC言語ベースだが、これにはすでに広く知られているという大きなメリットがある一方で、大きなデメリットもある。
問題点その1、C言語の想定するセマンティクスと実際の動きが異なる
普通のC言語はCPUに近い形でセマンティクスが決まっているが、SIMD上だとかなり予想外の振る舞いとなる。
例えばif文などはジャンプとして成立しないbodyはコストに影響しないように感じられるが、SIMDでは全文の上をPCが通るので、かなりコスト意識を変えて実装する必要がある。
また、関数コールもC言語では普通callとretのようなジャンプで実装されているように思うが、GPGPUでは普通インライン展開されて、これが上記のifの問題と合わさって普通のC言語が想定しているのと実際の動きが大きく異なる。 C言語を書く時には、実行モデルとしてDRAMのスタックというものを暗黙のうちに想定しているが、GPGPUプログラムではこのDRAMの上のスタックというものは実質使えない。
見た目と下での動きが乖離しているのは現状のGPGPUプログラミング全般の問題点で、MFGはなるべくそういう事を無くそうと作られた。
問題点その2、使える抽象化機構に制限がある割に素の言語が貧弱
C言語は素の言語は貧弱でありながら、その上に構造体や関数といった仕組みで必要な抽象を問題に合わせて構築していく、という前提の言語である。
だが、GPGPUでは、globalメモリに制約があったり配列が連続領域では無くレジスタにコンパイルされる都合でポインタと配列の間の変換が通常のC言語と違い、その為関数にも配列周辺の制約が多いため、必要な抽象を構築出来ない事が多い。(特にHLSLで大きい)
globalメモリ周辺の制約と関数の制約は、原始的な言語の上に抽象化を進めていくというC言語の思想にかなりの制限を与えていて、C言語の原始的な側面が目立ってしまう。
問題点その3、言語が低レベルなため、解析がしづらい
MFGではホスト側のコードもスクリプトを解析して実行するが、こういうスクリプトを解析してその構造に合わせて何かをやる、という事は、C言語ベースの言語では非常にやりにくい。
これはプラグインとしてdllなどをロードする事がほぼ禁止されているiPadにおいては、ユーザーにホスト側のコードを書かせる仕組みが無いため、大きな問題となる。
MFGの特徴
既存のC言語ベースのシェーダー言語の欠点を踏まえて、MFGの特徴を見ていく。
globalなメモリを一級市民として設計された言語
GPGPUではglobalなメモリは制約が多い。一方でGPGPUプログラミングではglobalなメモリが一番重要な操作対象である。
そこでMFGではglobalなメモリ、というものを特別な存在として言語の設計の最初から考えている。これはテンソルという存在となっていて、この周辺のシンタックスはタプルやintなどの他の値とは明確に変えてある。
関数のような仕組みが実現出来ない代わりに、MFGではテンソルに対して再利用出来る機能の組み合わせ方法としてデコレータというものを特別なシンタックスで用意している。このように、globalメモリに制約があるため、それらの制約で出来る最大限の機能を特別に設計している。
ホスト側のコードを書く必要が無い
シェーダー言語は全体的に、それと対応するホスト側にも多くのプログラミングが必要となる。これはシェーダーとは別のものとしてホスト側の言語の中で書かれるが、一方でシェーダーと密接にやりとりする為に両方を変える必要がある。
MFGはホスト側を書く必要は無いし書く手段も無い。その代わり、IRが通常のシェーダー言語よりリッチな構造を保持する為、それを解析して必要なホスト側のコードが実行される。これはMFGの大きな特徴であり、必要な事でもある。
通常、シェーダー言語だけでは必要な事はできず、プラグインのような形でホスト側でも自分のコードを入れられるような仕組みが必要となる。これはセキュリティの面でもよろしくないし、また本当にやりたい事からあまり関係無い雑用的なコードでもあるので不要に複雑になってしまっている所でもある。
MFGはスクリプトのみで多くのフィルタを書くのに必要な機能を提供できていて、ホスト側のコードを用意する必要が無いという点で既存のシェーダー言語環境とは大きく異なる。
言語自体をリッチにして、その代わり拡張性は制限がある
GPGPUではさまざまな制約があるため、強力な抽象化機構を提供するのが難しい。
また、用途もグラフィックスと限られているので、ユーザーが一からシンプルな言語を拡張しても、皆が同じようなものを作るようになる為、C言語のようにシンプルで貧弱な言語に強力な抽象化機構を持たせて使う側に拡張させる、という路線はうまく機能していないと思っている。
そこでMFGは、言語自体の機能をリッチにして、そのままやりたい事が簡潔に書けるように設計されている。これはLLなどと似た考えである。具体的にはタプル、destructuring、サンプラー、様々なブロックを引数にとるreduceなどのプリミティブなどである。
その代わり拡張性には制約がある。元々GPGPUプログラムは実行時間に大きな制約があるため、大規模で複雑なプログラムを書く事は難しい。だからC言語のような何にでもなれる言語では無く、グラフィックスを処理する事しか出来ない言語となっている。
ジャンプやループ的な要素はなるべく無くす
MFGでは意図的にif文をifel関数的なシンタックスにしていて、内部的にも三項演算子にコンパイルされる。シンタックス的にGPUの実際の動きに近い。
また、ループなどもほとんど無い。(ts.for_eachだけはループと言えるが、これはかなり限定的な使い方しか出来ない)
そもそもにGPGPUではループというのはあまり相性が良くない。各カーネルには実行の制限時間が厳しく決められているため、いつ終わるかわからないロジックを安全に書くのは多くの場合に不可能だ。そういったものはGPUでは計算出来ない類の問題という事になると思う。そもそもにSIMDでループなどがある時の挙動はかなりわかりにくくもあると思う。
MFGはループやifなどをジャンプ的に実装するのでは無く、reduceなど値を返すものとして実装してある。これは最近の言語の多くもバグの少なさなどの理由から採用されているトレンドであるが、MFGではGPGPU上であるのでより一層そのメリットが大きい。
fcoordなどが補完方法を引数で指定するのでは無くデコレータのシグネチャで指定するもの、コンパイル時に確定させやすくする為である。実行時に複数の方法に分岐するのは内部的にはジャンプとなってしまい、GPGPU的には計算コストがわかりにくく増えてしまう。
コンパイル時に解決するようなシンタックスを強制する事で、そうした分かりにくい問題を排除している。
コンパイル時の変形を駆使してGPUの制約下でなるべくリッチな言語にする
C言語はコンパイル前と後が割と近い言語と言える。
一方MFGはコンパイル時に多くの変形をする事でGPGPUでは実現出来ない多くの機能を提供しようとする言語である。具体的にはブロックやタプル、デコレータなどは、コンパイル時の変形で実現されている。また、こうした変形が可能なように言語に多くの制約、特にループや副作用については多くの制約がある。
コンパイル時の変形を可能にする為に、C言語よりもIRの段階で多くの情報を持つように言語が設計されている。これは現代的な言語の多くでも見られる特徴と思うが。
また、ジャンプ的な要素が少ないのもGPGPU的であると同時に、変形のしやすさにも寄与している。
コンパイル時の様々な機能で現代的なスクリプト言語のような簡潔な記述を実現しつつ、現代的なスクリプト言語が前提とするヘビーウェイトなランタイム環境を必要としないように作られている。具体的にはヒープのオブジェクトという存在や動的ディスパッチのようなものが無い。
MFGはランタイムが原始的な環境でなるべく現代的なスクリプト言語に近い書き味を実現しようとした言語と言える。
カーネルを中心とした構造
MFGのプログラムは、基本的にはテンソルの生成ルールを並べたものとなる。
そして各テンソルの生成ルールがそれぞれ一つのカーネルに対応する。
機能を分割する為には中間テンソルを作るのを言語として推奨していて、複数のカーネルに分けてコードを書くのが自然なように言語が設計されている。
複数のカーネルに分けるのはGPGPU的にも望ましい事だ。一つの長いカーネルが帰ってこないと実行を破棄されてしまう事も多い。だから短く実行出来る複数のカーネルに分ける方が良い。また、短く実行出来る複数のカーネルはGPUの計算資源に応じてスケールしやすく、貧弱なデバイスからハイエンドのPCまで、そのデバイスの実力に応じてスケールする。
将来的にはユーザー定義関数やユーザー定義デコレータも実装できるようにするかもしれないが、それでも最初の抽象化構造がテンソル定義であって、それはカーネルに対応するというのは意図した設計である。