死亡コード除去を実装しました。
前のエントリ で書いた
{
var a = 0;
var b = false;
if(b) a = 1;
return a;
}
このコードはついにやっと以下のコードに。
00000 const %0, *0 // *0=0 00003 return %0
つまり、定数 0 を常に返します。
こういうのもいける口のよう。
{
var counter = 0;
false && (counter +=4) ; // no count
true && (counter +=5) ; // count
false || (counter +=6) ; // count
true || (counter +=7) ; // no count
counter ;
}
これはこうなります (11 を常に返す)。
00000 const %0, *0 // *0=11 00003 return %0
まだ大量の演算子のルール (どの型とどの型を掛け合わせるとどの型がでてくる) を書き終わってないので、それを書きます。
TJS2だと定数畳込みは限定的だったので、
{
var a = 2, b = 1;
return a + b;
}
のように文が分かれていると定数畳込みをしてくれませんでした。
Risseの定数畳込みは文を超えても定数畳込みをしてしまうことができます。
Risseでは今のところ、暗黙の型変換をあまり行わない方向です。たとえば"A" + aのようにして、aが数値だった場合は例外が発生します。Risseの場合はこのようなことをやりたい場合は "A\{a}" のようにすることが推奨されることになります。
これに関連して、たとば以下のように0以上の連続する整数をコロン区切りで返す関数を書いたとして、
function t(l)
{
var s = '';
for(var i = 0; i < l; i++) {
if(s != '') s += ':';
s += i;
}
return s;
}
ここで s += i が例外を発生するのは明かです。ここでは常に s が文字列で、i が整数だからです。
Risse はある程度まで型を追うことができるので、これをコンパイル時にみつけて、以下のように警告することがあります。
warning: String::+(Integer) will cause an exception at runtime
「警告することがある」というのは、警告しない場合があるということです。Risseのコンパイラはすべての変数の状態を詳細に追っている訳ではないので、場合によってはエラーになる条件を見つけることができません。ちなみに上記の場合は、Risseの現在の内部の実装では、 String + Integer の条件を検出するよりも前に、ループの最初の状態である '' + 0 の計算をコンパイル時に試行するため、そこで例外が発生してしまっているようです。最初に試行するのがエラーがでない条件だった場合、警告をしないことがあります。
また、理論的に到達しないプログラムの部分をある程度 検出する事はできますが、場合によっては実行が行われない部分でも警告が表示される事があるかもしれません。
参考程度の情報ということですね。
型推測と型伝播、定数伝播、定数畳込みを仮実装しました。なんか書いたコードが一発で動いて少し拍子抜け。
前のエントリで書いたこのコードは
{
var a = 0;
var b = false;
if(b) a = 1;
return a;
}
こう解釈されるようになりました
*entry_2 // alive [1] _tmp@4 = AssignThisProxy() // _tmp@4 = constant type object [2] _tmp@10 = AssignConstant() Value=void // _tmp@10 = constant void [3] _tmp@17 = AssignConstant() Value=0 // _tmp@17 = constant 0, coalescable to id 0x01EA9608 [4] _tmp@26 = AssignConstant() Value=false // _tmp@26 = constant false [5] if _tmp@26 then *if_true_37 else *if_pseudo_false_48 // LiveOut: _tmp@17 *if_true_37 // dead, pred: *entry_2 [6] _tmp@39 = AssignConstant() Value=1 // coalescable to id 0x01EA9608 _tmp@77 = _tmp@39 // coalescable to id 0x01EA9568 [7] goto *if_exit_53 // LiveOut: _tmp@77, _tmp@39 *if_pseudo_false_48 // alive, pred: *entry_2 // LiveIn: _tmp@17 [8] _tmp@50 = AssignConstant() Value=void // _tmp@50 = constant void, coalescable to id 0x01EA9568 [9] goto *if_exit_53 // LiveOut: _tmp@50, _tmp@17 *if_exit_53 // alive, pred: *if_true_37, *if_pseudo_false_48 // LiveIn: _tmp@77, _tmp@50, _tmp@39, _tmp@17 [10] a#15@60 = PHI(_tmp@39, _tmp@17) // a#15@60 = constant 0, coalescable to id 0x01EA9608 [10] _tmp@55 = PHI(_tmp@77, _tmp@50) // _tmp@55 = constant void, coalescable to id 0x01EA9568 [11] a#15@60.Return()
ちょっと見にくいですが、最後の [11] a#15@60.Return() で使われている変数 a#15@60 について、[10] で a#15@60 = constant 0 と書かれているように、必ず結果が 0 になることをコンパイラが理解できるようになっています。
絶対に通らないブロック *if_true_37 も dead としてマークされています。
また、
{
var sum = 0;
for(var i = 0; i < 10; i++) sum += i;
return sum;
}
のようなコードでは
*entry_2 // alive [1] _tmp@4 = AssignThisProxy() // _tmp@4 = constant type object [2] _tmp@10 = AssignConstant() Value=void // _tmp@10 = constant void [3] _tmp@17 = AssignConstant() Value=0 // _tmp@17 = constant 0, coalescable to id 0x01EA9360 [4] _tmp@26 = AssignConstant() Value=0 // _tmp@26 = constant 0, coalescable to id 0x01EA92C0 [5] goto *for_cond_35 // LiveOut: _tmp@26, _tmp@17 *for_cond_35 // alive, pred: *entry_2, *for_iter_67 // LiveIn: _tmp@75, _tmp@58, _tmp@26, _tmp@17 [6] sum@116 = PHI(_tmp@17, _tmp@58) // coalescable to id 0x01EA9360 [6] i@118 = PHI(_tmp@26, _tmp@75) // coalescable to id 0x01EA92C0 i#24@39 = i@118 // i#24@39 = constant type integer sum#15@89 = sum@116 // sum#15@89 = constant type integer [7] _tmp@43 = AssignConstant() Value=10 // _tmp@43 = constant 10 [8] _tmp@45 = i#24@39.Lesser(_tmp@43) // _tmp@45 = varying [9] if _tmp@45 then *for_body_48 else *for_exit_81 // LiveOut: sum#15@89, i#24@39 *for_body_48 // alive, pred: *for_cond_35 // LiveIn: sum#15@89, i#24@39 [10] _tmp@58 = sum#15@89.Add(i#24@39) // _tmp@58 = constant type integer, coalescable to id 0x01EA9360 [11] goto *for_iter_67 // LiveOut: _tmp@58, i#24@39 *for_exit_81 // alive, pred: *for_cond_35 // LiveIn: sum#15@89 [12] _tmp@83 = AssignConstant() Value=void // _tmp@83 = constant void [13] goto *for_break_target_93 // LiveOut: sum#15@89 *for_iter_67 // alive, pred: *for_body_48 // LiveIn: _tmp@58, i#24@39 [14] _tmp@69 = AssignConstant() Value=1 // _tmp@69 = constant 1 [15] _tmp@75 = i#24@39.Add(_tmp@69) // _tmp@75 = constant type integer, coalescable to id 0x01EA92C0 [16] goto *for_cond_35 // LiveOut: _tmp@75, _tmp@58 *for_break_target_93 // alive, pred: *for_exit_81 // LiveIn: sum#15@89 [17] sum#15@89.Return()
となっていて、これまた見にくいですが、変数 i に相当する変数
[4] _tmp@26 = AssignConstant() Value=0 // _tmp@26 = constant 0, coalescable to id 0x01EA92C0 i#24@39 = i@118 // i#24@39 = constant type integer [15] _tmp@75 = i#24@39.Add(_tmp@69) // _tmp@75 = constant type integer, coalescable to id 0x01EA92C0
がすべて整数だとなっていて、
最後に [17] で return されている sum の型も
sum#15@89 = sum@116 // sum#15@89 = constant type integer
ということで必ず整数になることになっています。
型の推測ができると変数の型チェックをすっ飛ばすことができるのでそれなりにパフォーマンスがあがるかも、、、と思っていましたがもしかしたらあんまり効果無いかもしれません。。。
外部から来た変数や関数の戻り値、子関数と共有している変数などは型が分からないので、それを起源に持つ変数はすべて「型不明」として扱われてしまいます。明示的に途中でキャストして、あるいは変数に型を指定すれば(? そういう構文を用意するかどうかは決めてませんが)すこしは足しになるかもしれません。
とりあえず、どういう型と型を演算させたらどういう型になるというルールをコンパイラにたたき込まなければなりません。結構面倒ですがしばらくはその作業になりそう。
Risaは一休みして、ここしばらくは Risse のコード最適化周りを弄っていました。
ちなみに9月の初め頃のRisseが吐き出した、
{
var a = 0;
var b = false;
if(b) a = 1;
return a;
}
このスクリプトに対するVMコードは以下の通りでした。
#(1) {
00000 proxy %0
00002 copy %1, %0
00005 const %0, *0 // *0=void
00008 copy %1, %0
#(2) var a = 0;
00011 const %0, *1 // *1=0
00014 copy %1, %0
00017 copy %2, %0
#(3) var b = false;
00020 const %0, *2 // *2=false
00023 copy %2, %0
00026 copy %3, %0
#(4) if(b) a = 1;
00029 copy %0, %2
00032 copy %2, %1
00035 branch %0, 00039, 00056
00039 const %0, *3 // *3=1
00042 copy %1, %0
00045 copy %3, %0
00048 copy %3, %1
00051 copy %1, %0
00054 jump 00067
00056 const %0, *0 // *0=void
00059 copy %3, %2
00062 copy %1, %0
00065 jump 00067
00067 copy %0, %1
#(5) return a;
00070 copy %1, %3
00073 return %1
どえらい copy 文の量。ASTからSSA形式に変換する部分が馬鹿で大量のコピー文を挿入するうえに、最適化という最適化をしていなかったからです。
今では、これをゴリゴリと削ることができて、以下のようになっています。
#(1) {
00000 proxy %0
00002 const %0, *0 // *0=void
#(2) var a = 0;
00005 const %0, *1 // *1=0
#(3) var b = false;
00008 const %1, *2 // *2=false
#(4) if(b) a = 1;
00011 branch %1, 00015, 00023
00015 const %0, *3 // *3=1
00018 copy %1, %0
00021 jump 00028
00023 const %1, *0 // *0=void
00026 jump 00028
#(5) return a;
00028 return %0
コピー文がほとんど消えているのはφ関数に関する変数を合併するようになったのと、コピー伝播を行うようになったからです。ここらへんの実装が一番辛かった。
まだ上記のコードを見ると、使われないレジスタに値を代入していたり、次の命令にジャンプするだけの無駄なジャンプ命令があったり、定数条件のくせに無駄な条件分岐をやってしまっていますが、そこら辺の死亡コード除去だの定数伝播だの基本ブロックの連結だのはこれから実装します。
最終的にはこのスクリプトは 0 しか返さないことが明らかなので、
const %0, *0 // *0=0 return %0 // always constant 0, type=vtInteger
のような2文にまで縮まってほしいなーと思いつつ、いろいろ実装中です。
また、文脈からどのレジスタがどの型をとりうるかが分かるはずなので、レジスタに対する型チェックをすっ飛ばすようように指示するようなコードも将来的には吐ける可能性があります。型の推測が行えるというのは、タイプルーズなスクリプト言語としてはパフォーマンス的にはそこそこよい結果をもたらすのではないかと考えています。
吉里吉里2 2.29-dev.20070908でもお願いします
吉里吉里2 2.29-dev.20070901 を公開しました。今回から、ダブルバッファリングの方式として、従来の GDI と DirectDraw にくわえ、Direct3D でのダブルバッファリングが可能になっています。
Direct3Dを使うというと3Dバリバリな印象かもしれませんが、そんなことはなく、今回は単に画像の拡大のためだけに使っています。昨今の3Dゲーム用にチューニングされたチップでは3Dの描画機能を流用した方が遙かに高速に動作できる場合が多いためです。
ダブルバッファリングはフルスクリーン時のアスペクト比保持拡大機能など、吉里吉里本体が画像を拡大して表示する必要がある場合にも使用されます。
Direct3DはDirectDrawに比べて環境依存の不具合が多いと考えられます。というか環境依存の不具合の固まりです。吉里吉里本体にもいろいろな問題に対する回避コードが多数入っています。お時間のある方は実際にダウンロードしてテストしてくださると幸いです。
特に以下の点がポイントになるかと思います。もしテストしてくださる際は、画面解像度によってはフルスクリーンにしてもダブルバッファリングが有効にならない場合があるので、強制的にダブルバッファリングを行うようにする -usedb オプションをつけてみてください (このオプションをつけるとフルスクリーンであってもなくてもダブルバッファリングが使われるようになります)。
「吉里吉里設定」や「エンジン設定」では、2.29-dev.20070901 の場合、オプション情報が間違っていて正常に usedb オプションの設定をすることができないので、コマンドラインから -usedb というオプションを指定するか、あるいは *.cfu ファイルに usedb="\x79\x65\x73" という行を追加してください
2.29-dev.20070908 では治ってます
もしなにかありましたら、ご報告の際は以下の点もご報告ください。
吉里吉里はダブルバッファリングを行う際、ベンチーマークを行って、もっともパフォーマンスがよい方式を使用するようになっています。これは、ログに
20:31:06 ! Passthrough: benchmark result: DirectDraw : 358.21 fps 20:31:07 ! Passthrough: benchmark result: GDI : 25.71 fps 20:31:07 ! Passthrough: benchmark result: Direct3D : 711.71 fps 20:31:07 ! Passthrough: Using passthrough draw device: Direct3D double buffering
のように記録されています (うちの開発マシンの例ですが、Direct3D が選択されています)。GDIでのベンチマーク結果も表示されますが、GDIが自動的に選択される事はありません (GDIがベンチマーク時には異常に高い結果を示すのに、実際に表示させてみてるときわめて低速である環境が報告されているためです)。
Direct3Dのテクスチャ描画時にバイリニアフィルタ(補間)が利用できない環境では
Passthrough: Drawer object Direct3D does not have smooth zooming functionality
というログが、ベンチマーク結果付近に残っている場合があります。
DirectDrawでの補間拡大が可能な環境では
Passthrough: IDirectDraw::Blt seems to filter by some kind of interpolation method.
という記述がログに見られます。DirectDrawでの補間拡大ができない環境では
IDirectDraw::Blt seems to filter by nearest neighbor method.
という記述になります。
環境によっては
Passthrough: Using non 32bit ARGB texture format
という記述がログに残ってる場合があります。吉里吉里は、吉里吉里内部の画像形式と同じ形式のテクスチャを確保できる場合は直接テクスチャに対して画像を転送するようになりますが、異なる形式のテクスチャしか選択できなかった場合はこのような記録がのこり、別の方式を使うようになっています。
よく分からない方はログを全部コピー&ペーストしてください。
もし実際にこれで配布した後に問題が起こってしまった場合は、-dbstyleオプション (ダブルバッファリング方式)でダブルバッファリングの方式を強制的に別の方式にすることや、-fsres オプション(フルスクリーン時の画面解像度)を「最も近い解像度」にしてエンジン側での拡大処理を行わないようにすることでダブルバッファリングを使用しないようにすることでも回避できると思います。
よろしくお願い致します。