まずは、最新のv0.38の性能を確認しておきます。性能測定についてはNP2 for PSP高速化実験(その1)を参照してください。
ver0.38 | No Wait off | No Wait on |
---|---|---|
1回目 | 89949 | 83686 |
2回目 | 89552 | 83942 |
あれ、v0.37より微妙に遅くなっていますね。遅くなるような処理いれていないはずだけど。
とりあえず、これを基準値とします。1tickで1/1000秒なので、1000tickで1秒です。
No Wait Offで計測し、PC-98実機で動かしたときと同じ60秒が目標です。
それでは、いってみましょう。まずはgprofでプロファイリングしたところ、時間がかかっているのは、メモリのread/write、calcratechannel()とかsound_pcmunlock()はサウンド関係、ea_disp16()とかjnz_short()とかはCPUコア関係。
グラフィック関係は、以下のsdraw16p_2がワーストだが、全体の1.53%で大したことはなさそう。
Each sample counts as 0.001 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
17.89 46.45 46.45 54203281 0.00 0.00 memp_read16
9.66 71.53 25.08 212057550 0.00 0.00 memp_read8
8.37 93.25 21.73 12691803 0.00 0.00 calcratechannel
3.97 103.57 10.31 3699772 0.00 0.00 memp_write16
3.75 113.30 9.73 14843519 0.00 0.00 ea_disp16
3.18 121.56 8.26 9207 0.00 0.00 sound_pcmunlock
2.91 129.12 7.56 _jnz_short
2.66 136.03 6.92 _calc_ea16_i8
:
1.53 155.36 3.97 497 0.01 0.01 sdraw16p_2
メモリ関連を含め、CPUコアあたりが処理の大半を占めていて、ここが高速化のカギとなりそうです。
ところでざっくりとだが、np2を含め多くのエミュレータは、以下の様にメモリ上から命令コードを読み込んで、関数のポインタ配列から飛び先を選んで関数コールしている。
void( *func[])() = {op1, op2, op3};
void op1() {
//命令その1
}
void op2() {
//命令その2
}
void op3() {
//命令その3
}
while () {
func[*pc++]();
}
これを、labelの飛び先アドレスを変数にして、gotoジャンプができるというgccの拡張機能を使って書き換えてみる。
void *label[] = {&&op1, &&op2, &&op3};
while () {
ret:
retlabel = &&ret;
goto *label[*pc++];
}
op1:
//命令1
goto *retlabel
op2:
//命令2
goto *retlabel
op3:
//命令3
goto *retlabel
こうすると、関数コールに伴うオーバーヘッド(スタックの確保やレジスタの値の保存、復帰等)を回避でき、
高速化が期待できます。
で、実装して計測してみた。コードの変更量が半端なく、苦労した割には効果はいまいち。
あ、いじったのはi286のみ。v30はとりあえず切り捨て。i386のNP21はまったくノータッチ。NP21についてはお察しください。
i286命令実行部のラベルジャンプ化 | No Wait off | No Wait on |
---|---|---|
1回目 | 85884 | 79709 |
2回目 | 85959 | 80532 |
ラベルジャンプ化後のプロファイル。CPUの各命令処理は関数ではなくなったので、プロファイラからは見えなくなる。
Each sample counts as 0.001 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
27.02 37.83 37.83 45161057 0.00 0.00 memp_read16
12.55 55.41 17.57 10172873 0.00 0.00 calcratechannel
10.74 70.44 15.03 19059542 0.00 0.00 calc_ea_dst
8.50 82.34 11.90 184453464 0.00 0.00 memp_read8
6.64 91.64 9.30 10707068 0.00 0.00 memp_write16
5.70 99.62 7.98 6900 0.00 0.00 sound_pcmunlock
3.83 104.99 5.37 6 0.89 0.89 memf800_rd8
2.98 109.16 4.17 554 0.01 0.01 sdraw16p_2
2.74 112.99 3.83 6555078 0.00 0.00 memp_write8
memp_read16がワースト。これのcall graphを見てみると、i286c()からの呼び出しと、calc_ea_dst()からの呼び出しが大半をしめている。
[4] 62.2 0.21 86.88 280881 i286c [4]
15.03 12.40 19059542/19059542 calc_ea_dst [6]
25.57 0.00 30525473/45161057 memp_read16 [5]
[6] 19.6 15.03 12.40 19059542 calc_ea_dst [6]
12.24 0.00 14609446/45161057 memp_read16 [5]
0.16 0.00 2466354/184453464 memp_read8 [9]
memp_read16()は16ビットの読み込みに以下の様なマクロを使っている。
#define LOADINTELWORD(a) (((UINT16)(a)[0]) | ((UINT16)(a)[1] << 8))
これを、以下の様に書き換えてみる。
#define LOADINTELWORD(a) (((unsigned int)(a) & 1)? \
((UINT16)(a)[0]) | ((UINT16)(a)[1] << 8) : *(UINT16 *)(a))
結果失敗。これは元に戻した。
LOADINTELWORDの書き換え | No Wait off | No Wait on |
---|---|---|
1回目 | 87915 | 82310 |
i286c()のGETPCBYTE()を展開してみる
i286c()のGETPCBYTE()を展開 | No Wait off | No Wait on |
---|---|---|
1回目 | 85078 | 79216 |
ただし、適当に修正したので、プログラムコードがHIMEM領域とかに置かれていると、オーバーヘッドが増える。
そして、グラフィック周りで上位に来ていたsdraw16p_系の関数の無駄を見直し。
sdraw16p_系の関数見直し | No Wait off | No Wait on |
---|---|---|
1回目 | 83804 | 77336 |
とりあえず今回はおよそ6.5%の高速化となった。微妙。
今回の成果はv0.39として後ほどリリースします。
タグ:NP2 for PSP
高速化についてちょっと思ったのですが、PSP はリトルエンディアンらしい? ので common.h に定義されているマクロはリトルエンディアンに特化して良いのでは?
(元のソースはプラットフォームがビッグエンディアンでも動作可能な様な汎用コードになっているようですが・・・)
だから psp 専用ということで以下の様なコードでも良くないかと・・・ちょろっと思いました。(過ちを犯しているかも知れませんが、その時は笑って下さい。)
#define LOADINTELDWORD(a) (*(UINT32 *)(a))
#define LOADINTELWORD(a) (*(UINT16 *)(a))
#define STOREINTELDWORD(a, b) *(UINT32 *)(a) = (UINT32)(b)
#define STOREINTELWORD(a, b) *(UINT16 *)(a) = (UINT16)(b)