アカウント名:
パスワード:
いつも思うんですがエミュって重いですよねLLVMみたいに動的コンパイルすれば超早くなるのでは?と妄想してしまいます
# 難しい(現実的に作るのは不可能に近い)のはわかってます
PCSX2のMIPSエミュレーションで利用されてるかどうかはしりませんが最近のエミュレーターはJIT使ってるのも結構あるみたいですね。
重いとっても最近のx86は速いので、昔のCPUのエミュレーションだけならそんなに重荷でもなくて、どちらかというとグラフィックスやサウンドといったサブシステムのエミュレーションのほうが荷が重いかな、というところじゃないでしょうか。
そうなんですか?コード部分が圧縮とか暗号化とかしてあってメモリ展開だとJITは不可能じゃないのか?と思ってました。(switchしか無理だろうなと)
訂正switchじゃなくて関数ポインタの配列ジャンプかな?BYTE nimoc[0xFF]={func_00, func_01, ...}みたいなswitchだと何倍も遅くなっちゃいますよね
コンパイラ次第ですが、switch文はcase値が連続値だと配列ジャンプにしてくれることが多いので、遅くなるとは限らないのです。また、関数ポインタだと関数呼び出し時にスタック操作が入りますから(リターンアドレスの格納、スタックフレーム絡みの処理など)、switchよりも不利になる部分もあります。
下でもそういったコメントがあってなるほど参考になります。__inline void func_0x00(DWORD param1...)みたいに__inlineつければswitchのほうが早くなるのかな??(まさかswitch内に処理べた書きではないでしょうから)# 個人的な趣味的には関数ポインタのほうが好きかな?^^;
そゆことです。
void (*func[])() = { func0, func1, func2, ...};(*func[index])(param);
みたいな関数ポインタコードの場合は、関数をインライン展開することは不可能ですが、
switch (index) {case 0: func0(param); break;case 1: func1(param); break;case 2: func2(param); break;…}
とした場合には、関数のインライン展開が可能になります。インライン展開されなかったとしても関数ポインタ配列と同等。
あとはコンパイラ次第ですが、今時のコンパイラなら、関数の宣言側でinline を付けなくても自動で最適化で勝手にインライン展開してくれたりしますな。
で、その結果として関数呼び出しがなくなった場合、・引数のスタックへのプッシュが不要・関数入り口でのレジスタ退避が不要になるほか、・変数のライフタイム管理がより正確に行えるようになるというメリットも。
例えば、エミュレータの場合なら、エミュ対象CPUのレジスタなどの状態情報を変数に保持することになるかと思いますが、そのデータ内容は呼び出した関数でも書き換えられる可能性があります。となると、呼び出し元では、関数呼び出し前に変数内容をメモリからレジスタに読み込んでいたとしても、関数呼び出し後にはレジスタ内容は捨てて再度メモリから変数内容を読み込み直す必要があります。それが、インライン展開されるなどして関数呼び出しがなくなってしまえば、「書き換えられていない」ことが確定しているデータについては、無駄なメモリアクセスをなくすことができます。
まあ、これは突き詰めて言えば「C言語のポインタは、最適化を妨げる諸悪の根源だ」なんて考え方にもなっちゃうんですけどね。
#私が昔Z80のエミュレータを書いた時は、そこまで性能を気にしなかったので、関数ポインタの配列を使ったかな。#オペコード1バイト目と、CB/EDのオペコード2バイト目の、計3テーブル。DD/FDはフラグを立てたんだっけか。#今にして思えば、オペコードを8進数3桁に分離してからswitch-caseを使った方が、シンプルで保守性のよいコードになっただろうな、と思う…
Z80というかDMG-CPUのエミュを書いたが、コードは手書きせずに生成プログラムを書いたほうがいろいろよかった
なるほど。目から鱗が落ちました。ここら辺の最適化はエミュの速度を大幅に決めてしまいますから重要ですよね。参考になります。
# コンパイラの最適化のわかりやすい一覧があるといいですよね。ブラックボックス化(わたしが無知なので)してしまっています。そういったページが見当たらないのが残念
有名どころではQEMU [qemu.org]なんかそうですね。Wikipedia [wikipedia.org]より。
QEMUの特徴として、中間コードを介して動的コンパイルを行うことにより、x86、PowerPC、SPARCなど多くのホストCPUに対して多くのターゲットCPUを高速にエミュレーション可能である事が挙げられる。
さっそく調べてみたんですが私の脳みそではちょっとよく理解できませんでした・・・http://www.usenix.org/publications/library/proceedings/usenix05/tech/f... [usenix.org]3.6を一生懸命読んでみると(英語読めませんが・・・)コードっぽい部分、読み取り専用部分を一度実行すれば動的コンパイルしてキャッシュするみたいなことなのでしょうか?
switchの羅列ばかりで驚いた事があります。(略) 少なくとも関数ポインタの配列位は使ってるんじゃないかと思っていたので。
関数ポインタの配列って可読性はよくなりますが、オーバーヘッドが大きいですよ。それよりもswitchの方が効率が良いです。switch-caseと見ると、内部的にif-elseif-elseif…みたいな処理になって効率が悪い、と思われるのかもしれませんが、賢いコンパイラ(gccとか)で、caseの指定値が連続した数値だった場合、ジャンプテーブルに展開されます。そういう場合のswitch-caseはそんなオーバーヘッドはありません。むしろ、関数ポインタ配列に較べて、関数呼び出しではない(引数のスタック積み込み操作がない)分、switch-caseの方がオーバーヘッドが少なくなります。
#私はコンパイラのそういう賢さを、Duff's device [wikipedia.org]のコンパイル出力で知りました。#下手にアセンブラでごりごり書くより速いコードをコンパイラが出力するのでorzになった…
10年くらい前には、定型文・式みたいなものはそういうのやってましたよ
なんでしてないと思ったの?
してるんですか?
自分で調べろよ
無理でしょ# 1レス目に書いてありますが
×動的コンパイル○静的コンパイルの間違いでした
より多くのコメントがこの議論にあるかもしれませんが、JavaScriptが有効ではない環境を使用している場合、クラシックなコメントシステム(D1)に設定を変更する必要があります。
私は悩みをリストアップし始めたが、そのあまりの長さにいやけがさし、何も考えないことにした。-- Robert C. Pike
エミュ (スコア:0)
いつも思うんですがエミュって重いですよね
LLVMみたいに動的コンパイルすれば超早くなるのでは?と妄想してしまいます
# 難しい(現実的に作るのは不可能に近い)のはわかってます
Re:エミュ (スコア:2)
PCSX2のMIPSエミュレーションで利用されてるかどうかはしりませんが最近の
エミュレーターはJIT使ってるのも結構あるみたいですね。
重いとっても最近のx86は速いので、昔のCPUのエミュレーションだけならそんなに重荷
でもなくて、どちらかというとグラフィックスやサウンドといったサブシステムの
エミュレーションのほうが荷が重いかな、というところじゃないでしょうか。
Re: (スコア:0)
そうなんですか?
コード部分が圧縮とか暗号化とかしてあってメモリ展開だとJITは不可能じゃないのか?と思ってました。(switchしか無理だろうなと)
Re: (スコア:0)
訂正
switchじゃなくて関数ポインタの配列ジャンプかな?
BYTE nimoc[0xFF]={func_00, func_01, ...}みたいな
switchだと何倍も遅くなっちゃいますよね
Re: (スコア:0)
コンパイラ次第ですが、switch文はcase値が連続値だと配列ジャンプにしてくれることが多いので、遅くなるとは限らないのです。
また、関数ポインタだと関数呼び出し時にスタック操作が入りますから(リターンアドレスの格納、スタックフレーム絡みの処理など)、switchよりも不利になる部分もあります。
Re: (スコア:0)
下でもそういったコメントがあってなるほど参考になります。
__inline void func_0x00(DWORD param1...)みたいに__inlineつければswitchのほうが早くなるのかな??(まさかswitch内に処理べた書きではないでしょうから)
# 個人的な趣味的には関数ポインタのほうが好きかな?^^;
Re:エミュ (スコア:1)
そゆことです。
みたいな関数ポインタコードの場合は、関数をインライン展開することは不可能ですが、
とした場合には、関数のインライン展開が可能になります。
インライン展開されなかったとしても関数ポインタ配列と同等。
あとはコンパイラ次第ですが、今時のコンパイラなら、関数の宣言側でinline を付けなくても
自動で最適化で勝手にインライン展開してくれたりしますな。
で、その結果として関数呼び出しがなくなった場合、
・引数のスタックへのプッシュが不要
・関数入り口でのレジスタ退避が不要
になるほか、
・変数のライフタイム管理がより正確に行えるようになる
というメリットも。
例えば、エミュレータの場合なら、エミュ対象CPUのレジスタなどの状態情報を変数に保持することになるかと思いますが、そのデータ内容は呼び出した関数でも書き換えられる可能性があります。
となると、呼び出し元では、関数呼び出し前に変数内容をメモリからレジスタに読み込んでいたとしても、関数呼び出し後にはレジスタ内容は捨てて再度メモリから変数内容を読み込み直す必要があります。
それが、インライン展開されるなどして関数呼び出しがなくなってしまえば、「書き換えられていない」ことが確定しているデータについては、無駄なメモリアクセスをなくすことができます。
まあ、これは突き詰めて言えば「C言語のポインタは、最適化を妨げる諸悪の根源だ」なんて考え方にもなっちゃうんですけどね。
#私が昔Z80のエミュレータを書いた時は、そこまで性能を気にしなかったので、関数ポインタの配列を使ったかな。
#オペコード1バイト目と、CB/EDのオペコード2バイト目の、計3テーブル。DD/FDはフラグを立てたんだっけか。
#今にして思えば、オペコードを8進数3桁に分離してからswitch-caseを使った方が、シンプルで保守性のよいコードになっただろうな、と思う…
Re: (スコア:0)
Z80というかDMG-CPUのエミュを書いたが、コードは手書きせずに生成プログラムを書いたほうがいろいろよかった
Re: (スコア:0)
なるほど。目から鱗が落ちました。
ここら辺の最適化はエミュの速度を大幅に決めてしまいますから重要ですよね。参考になります。
# コンパイラの最適化のわかりやすい一覧があるといいですよね。ブラックボックス化(わたしが無知なので)してしまっています。そういったページが見当たらないのが残念
Re: (スコア:0)
有名どころではQEMU [qemu.org]なんかそうですね。
Wikipedia [wikipedia.org]より。
Re: (スコア:0)
さっそく調べてみたんですが私の脳みそではちょっとよく理解できませんでした・・・
http://www.usenix.org/publications/library/proceedings/usenix05/tech/f... [usenix.org]
3.6を一生懸命読んでみると(英語読めませんが・・・)コードっぽい部分、読み取り専用部分を一度実行すれば動的コンパイルしてキャッシュするみたいなことなのでしょうか?
Re:エミュ (スコア:1)
前どこかでソースコードを見た時に、switchの羅列ばかりで驚いた事があります。当時、なんとなくエミュって得体のしれない超技術で作られているようなイメージが有り、少なくとも関数ポインタの配列位は使ってるんじゃないかと思っていたので。
いつもの結論は、必要ないとか技術不足以外では、移植性の高さと再現性を重視しているんじゃないかってところです。
ゲーム機エミュレーターってのは、閉じたハードにサードパーティーソフトを動かせるようになった時に真っ先に移植されるソフトです。何しろ、初歩的なグラフィック・オーディオ命令だけ書き換えて移植すれば、何千何万という良質なゲームソフトが動くなるようになるわけですから。開発コミュニティの規模や技術から、いちいち移植に人員を割いてもいられませんし。
それに元のハードウェア仕様ががっちりきまってますから、タイミング含めてかなりきちんと再現しなくちゃいけません。ミスがあるとdesyncなんて言ってTASや普通のステートセーブなんかに影響が出たりします。深刻なら動かないゲームまで出てくるでしょう。
もちろん十分な技術や人員があれば何とかなるんでしょうけど、需要は多いとはいえ大っぴらに開発したり募集したりできない代物です。
AppleでさえLionでは、Rosettaのサポートを辞めたくらいですし。JavaScriptのJIT対応ブラウザは世界にいくつかしかありません。そういう意味では難しいんでしょう。
そういえば昔、ROMをアップロードすればiアプリに変換するサービスがあったような気がします。iアプリをiPhoneアプリに変換 [nikkeibp.co.jp]みたいなのもあります。ただ、実用的時間で動的となれば話は別でしょう。
Re:エミュ (スコア:3, すばらしい洞察)
関数ポインタの配列って可読性はよくなりますが、オーバーヘッドが大きいですよ。それよりもswitchの方が効率が良いです。
switch-caseと見ると、内部的にif-elseif-elseif…みたいな処理になって効率が悪い、と思われるのかもしれませんが、
賢いコンパイラ(gccとか)で、caseの指定値が連続した数値だった場合、ジャンプテーブルに展開されます。そういう場合のswitch-caseはそんなオーバーヘッドはありません。むしろ、関数ポインタ配列に較べて、関数呼び出しではない(引数のスタック積み込み操作がない)分、switch-caseの方がオーバーヘッドが少なくなります。
#私はコンパイラのそういう賢さを、Duff's device [wikipedia.org]のコンパイル出力で知りました。
#下手にアセンブラでごりごり書くより速いコードをコンパイラが出力するのでorzになった…
Re:エミュ (スコア:1)
いくら最適化が賢くても関数ポインタの配列よりか速くはならないだろうと思ってましたが、関数呼び出しかどうかで差が出るんですか。びっくりしました。
ただそのコードは余り最適化を期待できない環境で、逆に関数ポインタの意味も無さそうな環境で…。まぁ、私はコードを見ただけで別に関わったりは…。とにかく当時はそう思っちゃいました。
ところで昔から思うんですが、高速化する場合Cとアセンブラ両方で書いて、コンパイラが等価性を保証して、速度比較して、さらにそこから学習してくれる、みたいなのはできないんでしょうかね。
Re:エミュ (スコア:2)
10年くらい前には、定型文・式みたいなものはそういうのやってましたよ
Re:エミュ (スコア:1)
他に、昔のゲームで多用される関数ポインタへの対応の困難・メモリとデータの混在の厄介さ・セキュリティー問題などがあるでしょうね。
例えば、RPGの戦闘で多様なエフェクトを出す、あるいはアイテムで特殊な振る舞いをさせる、といった場面を考えると、簡単なスクリプトを使うか関数ポインタを使うしか思い浮かびません。メモリ制限が厳しいROM時代なら後者しかないでしょう。関数ポインタの対応もやろうとすればできなくはないでしょうが、ハードウェア・コンパイラ回りの知識が必要で、バグの温床・移植の妨げです。
単に静的コンパイルするだけなら、外部のコンパイラかアセンブラにソースコードを食わせればいいように思えますが、こういう理由でうまくいかないんでしょうね。
というより、昔のゲームなら必要なくて、新しいゲームなら再現性ある移植で精いっぱいというのが実情でしょうか。
Re: (スコア:0)
なんでしてないと思ったの?
Re: (スコア:0)
してるんですか?
Re: (スコア:0)
自分で調べろよ
Re: (スコア:0)
無理でしょ
# 1レス目に書いてありますが
Re: (スコア:0)
×動的コンパイル
○静的コンパイル
の間違いでした