+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2005年 ==================== * Wed Apr 06 00:00:00 JST 2005 Naoyuki Sawa
- ストリーム再生のまとめ#2 今回もHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/stream/stream2.html * Sat Mar 26 19:35:00 JST 2005 Naoyuki Sawa - ストリーム再生のまとめ#1 今回はHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/stream/stream1.html * Fri Feb 25 06:00:00 JST 2005 Naoyuki Sawa - 続・ディレイド分岐命令の誤動作〜"jp.d %rb"命令は使用不可 2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」で、"jp.d %rb"命令の直前にメモリアクセス命令を置くとディレイド命令が実行されない、 という不具合について記しました。 ●ディレイド命令が正しく実行される例 ld.b %r4, %r7 ; メモリアクセス以外の命令 jp.d %r6 ; %r6レジスタの指すアドレスへジャンプ… add %r5, 1 ; …する前に、このディレイド命令を実行します。 ●ディレイド命令が正しく実行される例 ld.b [%r4], %r7 ; メモリアクセス命令 jp.d %r6 ; %r6レジスタの指すアドレスへジャンプ… add %r5, 1 ; …する前に、このディレイド命令を実行しなければいけないのですが、実行されません!! 詳しくは、2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」を参照してください。 ----------------------------------------------------------------------------- "jp.d %rb"命令の直前にメモリアクセス命令を置くことさえしなければ正しく動作するのならば、 プログラミング時に気をつけてさえいれば、"jp.d %rb"命令の誤動作を確実に回避することができます。 しかし実際には、誤動作が発生する条件はそれだけではなく、事実上"jp.d %rb"命令は使用不可能であることがわかりました。 その条件とは、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 というものです。 DMA転送によるメモリアクセスのタイミングを、プログラミング時にサイクル単位で完全に把握することは、まず不可能です。 "jp.d %rb"命令の誤動作を確実に回避することはできず、従って、"jp.d %rb"命令は使用不可、ということになります。 さて、それでは、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 ことを確認してみましょう。 実験プログラムはこちら: http://www.piece-me.org/archive/jpdbug-20050225.zip 実験プログラムtest1〜test4が、以下の説明の� 銑い紡弍�しています。 ----------------------------------------------------------------------------- �,泙困蓮�"jp.d %rb"命令が正しく動作する例を確認します。 アセンブラで書かれた次のようなテストルーチンをC言語から呼び出して、変数RESULTに格納された値を画面に表示します。 test1: xld.w %r4, 1000000 ; 1000000回ループします。 xld.w %r5, 0 ; 1回毎に1づつカウントアップします。 xld.w %r6, skip ; jp.d命令のジャンプ先アドレスです。 loop: jp.d %r6 ; jp.d命令を使って以下の区間を飛び越えます。 add %r5, 1 ; jp.d命令のディレイスロットでカウントアップを行います。 ; ;{{-------------------------------------------------------- ; この区間は飛び越えるので、実行されません。 ; 本当に飛び越えていることを確認するために、 ; 万一、この区間を飛び越えずに実行されたら、 ; アドレスエラーが発生するようにしておきます。 xld.w %r15, 1 ld.w %r15, [%r15] ;}}-------------------------------------------------------- ; skip: xsub %r4, %r4, 1 ; ループ処理を行います。 xjrne loop xld.w [RESULT], %r5 ; カウントアップの結果を格納します。 ret テストルーチンの内容は、次のとおりです。 1000000回のループを行い、ループが1回まわるたびにカウンタを1づつ増やします。 このとき、「カウンタを1づつ増やす」処理("add %5,1")を、"jp.d %rb"命令のディレイスロットで行っています。 1000000回のループが終了したら、カウンタの値を、グローバル変数RESULTに格納します。 正しく実行されたならば、グローバル変数RESULTには1000000が格納されているはずです。 それでは実行してみます。 結果: 1000000 期待どおり、グローバル変数RESULTには1000000が格納されました。 このテストルーチンでは、"jp.d %rb"命令の直前にメモリアクセス命令が無いので、"jp.d %rb"命令が正しく動作し、 ディレイスロットに置かれたディレイド命令がきちんと実行されていることがわかります。 ----------------------------------------------------------------------------- �⊆,法�"jp.d %rb"命令が誤動作する例を確認します。 2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」で確認したのと同じ条件ですけれど、 念のために、今回も、もういちど確認しておくことにしました。 テストルーチンを次のように変更します。 test2: xld.w %r4, 1000000 ; 1000000回ループします。 xld.w %r5, 0 ; 1回毎に1づつカウントアップします。 xld.w %r6, skip ; jp.d命令のジャンプ先アドレスです。 xld.w %r7, DUMMY ; jp.dの誤動作を再現するためのメモリ書き込みアドレスです。 loop: ld.b [%r7], %r7 ; jp.d命令の直前でメモリアクセスすると、jp.dが誤動作します。 ; (高速RAM上で実行する場合は読み書き両方、SRAMの場合は書き込みのみ) jp.d %r6 ; jp.d命令を使って以下の区間を飛び越えます。 add %r5, 1 ; jp.d命令の遅延スロットでカウントアップを行います。 ; ;{{-------------------------------------------------------- ; この区間は飛び越えるので、実行されません。 ; 本当に飛び越えていることを確認するために、 ; 万一、この区間を飛び越えずに実行されたら、 ; アドレスエラーが発生するようにしておきます。 xld.w %r15, 1 ld.w %r15, [%r15] ;}}-------------------------------------------------------- ; skip: xsub %r4, %r4, 1 ; ループ処理を行います。 xjrne loop xld.w [RESULT], %r5 ; カウントアップの結果を格納します。 ret �,箸琉磴い蓮�"jp.d %rb"命令の直前にダミーのメモリアクセス命令を置いたことだけです。 実行してみると、 結果: 84 となりました。(実行するたびに、数字は少し変化します) 本当は1000000となるはずのところ84ですから、ほとんどの"jp.d %rb"命令は誤動作していることになりますが、わずかに正常動作の回があります。 これは何かと言うと、"jp.d %rb"命令の直前で割り込みが発生し、メモリアクセス命令と"jp.d %rb"命令の間で割り込みルーチンが実行された場合に、 メモリアクセス命令と"jp.d %rb"命令が連続で実行されないので、"jp.d %rb"命令が正しく動作するのです。 ためしに、割り込みを完全に禁止して、同じテストルーチンを実行してみると、 結果: 0 となり、"jp.d %rb"命令がすべて異常動作することが確認できます。 なお、この異常動作を確認するテストプログラムは、ときどきハングアップすることがあります。 メモリアクセス命令と"jp.d %rb"命令が連続すると、ディレイド命令が実行されないだけでなく、予測できない動作となることもあるみたいです。 ----------------------------------------------------------------------------- ��さて、ここからが本題です。 まず、テストルーチンの内容を�,汎韻犬發里北瓩靴泙后� "jp.d %rb"命令が正しく動作して、“結果: 1000000”と表示されるはずのテストルーチンです。 ただし、�,箸琉磴い蓮▲丱奪�グラウンドでサウンドを再生しながらテストルーチンを実行する、という点です。 サウンド再生には、通常のP/ECEサウンドAPI pceWaveDataOut() を使います。 実行してみると、 結果: 994836 1%未満ですけれど、"jp.d %rb"命令が誤動作しています。(実行するたびに、数字は少し変化します) �,箸琉磴い蓮▲丱奪�グラウンドでサウンドを再生していることだけですから、サウンド再生が"jp.d %rb"の誤動作の原因になっていることは確実です。 サウンド再生処理の内容は、主に“DMAによるメモリ転送”と“DMA完了割り込み”のふたつです。 このうち、“DMA完了割り込み”が誤動作の原因になっているとは考えづらいです。 なぜなら、サウンド再生を行っていなくても、“DMA完了割り込み”以外の割り込みは頻繁に発生しているからです。 “DMA完了割り込み”以外の割り込みが"jp.d %rb"の誤動作を引き起こさず、“DMA完了割り込み”だけが誤動作を引き起こすとは思えないからです。 そんなわけで、原因は“DMAによるメモリ転送”にあるとアタリを付けてみることにしました。 ----------------------------------------------------------------------------- �ぁ�DMAによるメモリ転送”が"jp.d %rb"命令の誤動作の原因であることを確認しましょう。 サウンド再生処理のような複雑な処理ではなく、純粋にDMA転送だけを行ってみます。 �,筬�と同じテストルーチンのバックグラウンドで単純なDMA転送を行って、"jp.d %rb"命令の誤動作が発生するかどうかを確認します。 テストルーチンを呼び出す前に、次のような手順でDMA転送を開始します。 DISABLE; /*---------- DMAトリガ用タイマの設定 ----------*/ /* クロック選択 = θ/1 (タイマカウントダウン周期 = 24MHz) */ bCLKSEL_T8_P8TPCK3 = 1; /* クロック制御 = On */ bCLKCTL_T8_23_P8TON3 = 1; /* リロードデータ = 240-1 (DMAトリガ周期 = 100KHz) */ pT8_RLD3 = 240-1; /* プリセット、Run。 */ pT8_CTL3 = (1<<1) | (1<<0); /*---------- DMAの設定 ----------*/ /* コントロール情報を設定する前に、高速DMA Ch.3を確実に停止します。 */ HS3_DISABLE; /* トリガ要因 = 8bitタイマCh.3アンダーフロー */ bHSDMA_HTGR2_HSD3S = HS3_8T3; /* アドレスモード = デュアル * 転送カウンタ = 0xffffff回 (このプログラムでは、明示的に停止されるまで) */ pHS3_CNT = (1<<31) | (1<<28)-1; /* 転送データサイズ = バイト * 転送元アドレス制御 = 固定 * 転送元アドレス = &DUMMY1 */ pHS3_SADR = (int)&DUMMY1; /* 転送モード = シングル転送 * 転送先アドレス制御 = 固定 * 転送先アドレス = &DUMMY2 */ pHS3_DADR = (int)&DUMMY2; /* 高速DMA Ch.3を有効にする前に、確実にトリガフラグをクリアします。 */ pHS3_TF = 1; /* 高速DMA Ch.3を有効にします。 */ HS3_ENABLE; ENABLE; DMA転送の内容は、ダミー変数領域から別のダミー変数領域へ、100KHzの頻度でメモリ転送を行う、というものです。 サウンド再生と同様に、DMA転送とテストルーチンは並行して実行されます。 サウンド再生の場合と違って、DMA転送にかかわる割り込みは発生しません。 それでは、実行してみます。 結果: 974426 予想どおり、"jp.d %rb"が誤動作しました。(実行するたびに、数字は少し変化します) サウンド再生の場合よりも、少し誤動作の頻度が多いですね。 その理由は、サウンド再生のDMA転送レートは32KHzであるのに対して、�い離謄好肇廛蹈哀薀爐療樵�レートは100KHzだからです。 DMA転送の発生する頻度が多い分、"jp.d %rb"と重なる確率が高く、結果、"jp.d %rb"が誤動作する頻度も多くなるわけです。 ためしに、DMA転送レートを10倍に増してみると、 /* リロードデータ = 24-1 (DMAトリガ周期 = 1MHz) */ pT8_RLD3 = 24-1; "jp.d %rb"誤動作の頻度がもっと高くなることが確認できます。 結果: 699436 ----------------------------------------------------------------------------- 以上の実験で、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 ことが確認できました。 先にも述べたように、DMA転送のタイミングに依存する誤動作は、回避不可能です。 P/ECEの場合、サウンド再生と併用しなければ良いのですけれど、それは現実的ではありません。 結論は、 ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃"jp.d %rb"命令は、いかなる場合においても★絶対に★使ってはいけない┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ となります。 なお、P/ECE開発環境の標準Cコンパイラは最適化機能が弱く、"jp.d %rb"命令を生成しませんので、C言語だけを使ってプログラミングしている限りは安全です。 アセンブラを使って、手作業で最適化する場合には、要注意です。 ----------------------------------------------------------------------------- さて、以下は余談です。 "jp.d %rb"命令とDMA転送の併用による誤動作に気付いた経緯について、記しておこうと思います。 現在、「ゲームボーイ/EMU」の作成中なのですけれど、サウンドを追加したとたんに、CPUエミュレーションが正しく実行されなくなるという問題が発生しました。 最初、サウンドエミュレーションルーチンにバグがあって、不正なメモリを破壊しているのかと考えて調べてみたのですが、いくら調べても間違っていません。 原因を突き詰めていくと、サウンドエミュレーションルーチンを呼ばなくても、単にpceWaveDataOut()を呼ぶだけで、問題が発生することがわかりました。 そこで、pceWaveDataOut()を呼んだ場合と呼ばない場合とで、CPUエミュレーションのトレースログを比較してみました。 すると、pceWaveDataOut()を呼んだ場合に、ときどき、残りCPU実行サイクルのカウントダウンが行われていないことがわかりました。 こんなかんじです。 pceWaveDataOut()を呼ばない場合 pceWaveDataOut()を呼んだ場合 ------------------------------ ------------------------------ PC=1000: 残り実行サイクル=440 PC=1000: 残り実行サイクル=440 PC=1001: 残り実行サイクル=436 PC=1001: 残り実行サイクル=436 PC=1002: 残り実行サイクル=432 PC=1002: 残り実行サイクル=436 <- 減ってない!! PC=1003: 残り実行サイクル=428 PC=1003: 残り実行サイクル=432 PC=1004: 残り実行サイクル=424 PC=1004: 残り実行サイクル=428 PC=1005: 残り実行サイクル=420 PC=1005: 残り実行サイクル=424 PC=1006: 残り実行サイクル=416 PC=1006: 残り実行サイクル=420 上の例では、1002番地の命令のエミュレーションで誤動作が発生していますが、実行するたびに誤動作が発生するアドレスが変化するのです。 何度か試してみて、誤動作が発生する命令のエミュレーションルーチンの共通性に気が付きました。 それらのエミュレーションルーチンはすべて、次のようなコード並びを使っていたからです。 ld.w %r0, %r4 ; (メモリアクセス以外の命令。"jp.d %r11"は誤動作しないはず、だったのですけれど…) jp.d %r11 ; メモリ書き込みルーチンへジャンプ sub %r7, 4 ; "jp.d %rb"命令のディレイストットで、残り実行サイクルを減らす つまり、ときどき"sub %r7,4"が実行されていないことになります。 以前のP/ECE研究記録「ディレイド分岐命令の誤動作」で、"jp.d %rb"とメモリアクセス命令の相性が悪いことは既にわかっていました。 サウンドを追加した途端に発生した誤動作、メモリアクセス、、、となると、サウンド再生のためのDMA転送しか考えられません。 そこで今回のような調査を行ってみたところ、"jp.d %rb"命令とDMA転送の併用で異常動作が発生することがわかった、というわけです。 今回は、単純で定型的な処理を繰り返すCPUエミュレーションルーチンの中での誤動作だったために、比較的簡単に原因が推測できました。 もしも、通常のゲームプログラムの入り組んだルーチンの中で、ごく稀に発生するような誤動作だったならば、原因究明にはもっと時間がかかっていたと思います。 ある意味、幸運だったかも知れません。 * Fri Feb 11 04:56:00 JST 2005 Naoyuki Sawa - 続・実はディレイド命令 今回のテーマは、2003年4月12日の記録「実はディレイド命令」の続きです。 ====================================================================================================================== まずは、前回のおさらいから。 P/ECEのCPU S1C33209は、RISC CPUの特徴のひとつ、「ディレイド分岐機能」を持っています。 ディレイド分岐機能とは、分岐命令の実行より先に分岐命令の直後の命令を実行し、実行効率を稼ぐ機能です。 ディレイド分岐機能を使う分岐命令を、「ディレイド分岐命令」と呼びます。 また、ディレイド分岐命令の直後に置かれる命令を、「ディレイド命令」と呼びます。 ディレイド分岐命令直後の、ディレイド命令を置くことができる位置を、「ディレイドスロット」と呼びます。 すべての命令が、ディレイド命令として使用できるわけではありません。 ある命令がディレイド命令として使用できるか否かについて、基本的な判断基準は次の通りです。 ディレイド命令は以下の条件をすべて満たしていることが必要です。 ・1サイクル命令 ・メモリをアクセスしない ・ext命令による拡張なし 『S1C33209コアCPUマニュアル』(C:\usr\PIECE\docs\datasheet\EPSON\33000Core-J.pdf) p.30「ディレイド分岐機能」より 同ページには、ディレイド命令として使用できる命令の一覧が示されています。 さらにその下に、次のような注意が記されています。 注:上記の条件を満たさない命令は動作が不定となるため、ディレイド命令として使用することは禁止します。 ところが、EPSON社自身による標準ライブラリの実装が、このルールを破っていました。 ディレイド命令として使用できないはずの"ld.w %rd,%ss"命令を、ディレイド命令として使用していたのです。 何通りかの命令並びを試してみたところ、"ld.w %rd,%ss"命令は、ディレイド命令として問題なく使用できることがわかりました。 EPSON社も使っているのですから、たぶん大丈夫なのでしょう。 以上、前回のおさらいでした。 ====================================================================================================================== その後、自作プログラムで"ld.w %rd,%ss"命令をディレイド命令として使ってきましたが、これまでのところ問題は出ていません。 やっぱり、大丈夫だったようです。 さて、ディレイド命令として使用不可とされている命令のなかに、"ld.w %rd,%ss"命令よりもっと使用頻度の高いものがあります。 char⇔int変換やshort⇔int変換を行う、以下の命令群です。 ld.b %rd,%rs char⇔int変換 ld.ub %rd,%rs unsigned char⇔int変換、(value&0xff)の代用 ld.h %rd,%rs short⇔int変換 ld.uh %rd,%rs unsigned short⇔int変換、(value&0xffff)の代用 以降の文では、これらの命令を“型変換命令”と呼ぶことにします。 型変換命令はすべて「1サイクル命令」で「メモリをアクセスしない」命令で「ext命令による拡張なし」の条件を満たしています。 にもかかわらず、『S1C33209コアCPUマニュアル』では、ディレイド命令として使用可能な命令に挙げられていません。 実際にプログラムを組んでいると、型変換命令がディレイド命令として使えないために、悔しい思いをすることがよくあります。 add %r12, %r13     ; a = a + b ld.ub %r10, %r12    ; return (unsigned char)a ret ↓"ld.ub %r10,%r12"がディレイド命令として使用可能だったならば、次のように並び替えて高速化できるのですが・・・ add %r12, %r13     ; a = a + b ret.d          ; return (unsigned char)a ld.ub %r10, %r12    ; *delay* 実際には、"ld.ub %r10,%r12"がディレイド命令として使用可能でないために、並び替え“不可”です!! 一見、ディレイド命令として使用可能な"and"命令で代用してしまえばいいようにも見えますけれど、それはできません。 add %r12, %r13     ; a = a + b ret.d          ; return a & 0xff xand %r10, %r12, 0xff  ; *delay* ↓"xand %r10,%r12,0xff"は、コンパイラによって次のように展開されます。 add %r12, %r13     ; a = a + b ret.d          ; return a & 0xff ext 0xff        ; ┐ and %r10, %r12     ; ┴ 2命令に展開されてしまい、ディレイドスロットに収まりません!! 型変換命令を使うときは、ディレイド分岐機能を使った高速化をあきらめるしかないのでしょうか? ====================================================================================================================== 「1サイクル命令」「メモリをアクセスしない」「ext命令による拡張なし」の条件を満たしているにもかかわらず、 ディレイド命令として使用可能でない命令としては、型変換命令の他に、"div1"や"halt"命令などがあります。 "div1"や"halt"命令はやや特殊な部類の命令ですから、ディレイド命令として使用可能でなくても、なんとなく納得できます。 しかし、型変換命令のような単純で使用頻度の高い命令がディレイド命令として使用可能でないというのは、納得できません。 そこでふと考えたのが、「もしかして実際には型変換命令もディレイド命令として使用可能なのではないか?」 思えば"ld.w %rd,%ss"命令だって、マニュアルには記載されていないけれど、暗黙公然のディレイド命令だったわけですから。 それではさっそく試してみましょう。 --------------------------------------------------------- foo: add %r12, %r13     ; a = a + b ret.d          ; return (unsigned char)a ld.ub %r10, %r12    ; *delay* --------------------------------------------------------- このアセンブラルーチンを、C言語プログラムから呼び出してみます。 --------------------------------------------------------- extern int foo(int a, int b); n = foo(0x12345678, 0x9abcdef0); pceFontPrintf("%08x", n); --------------------------------------------------------- 結果は、「00000068」と表示されました。 0x12345678 + 0x9abcdef0 = 0xacf13568 (unsigned char)0xacf13568 = 0x68 ですから、正しい結果です。 実行時間を計測してみると、ディレイド分岐機能を使ったことによる高速化の効果が確認できました。 前後数命令の並びを何通りかに変えてみたり、プログラムやデータをSRAMや高速RAMに配置しても、すべて正しい結果となります。 "ld.b %rd,%rs"、"ld.h %rd,%rs"、"ld.uh %rd,%rs"命令についても同様に試してみたところ、すべて正しい結果が得られました。 三年間悩んでいたのが可笑しくなってくるぐらい、あっけない幕切れです。 ┏━━━━━━━━━━━━━━━━━━━━┓ ┃┏━━━━━━━━━━━━━━━━━━┓┃ ┃┃ 型変換命令もディレイド命令でした ┃┃ ┃┗━━━━━━━━━━━━━━━━━━┛┃ ┗━━━━━━━━━━━━━━━━━━━━┛ もっとも、テストプログラムで正しい結果が得られただけでは、あらゆる場面で正しい結果が得られるとは限りません。 マニュアルにも「上記の条件を満たさない命令は動作が★不定★となるため〜」と記されているように、 テストプログラムではたまたま正しい結果が得られただけかも知れないからです。 しかし、"ld.w %rd,%ss"の前例からすると、たぶんどんな場面でも大丈夫な気がします。僕のカンですけれど。。。(^^; 型変換命令がディレイド命令として使えることのメリットは、もしかしたら不安定かも…というリスクを上回ります。 なにより、使ってみなければ不安定かどうかもわかりませんから、今後は積極的に使ってみることにしようと思います。 ====================================================================================================================== そんなわけで、型変換命令もディレイド命令として(たぶん)使用可能であることがわかりました。 もっと早くに試しておけばよかった。。。 さて、こうなってくると、「型変換命令以外にも、実はディレイド命令として使用可能な命令があるのでは?」と思えてきます。 ちょっと試してみたところ、 ・1サイクル命令 ・メモリをアクセスしない ・ext命令による拡張なし の条件をまったく無視して、ほとんどの命令がディレイド命令として(一応は)動作することがわかりました。 xjp.d bar mlt.w %r12, %r13    ; 5サイクル命令!! とか、 xjp.d bar ld.w %r10, [%r12]    ; メモリをアクセスする!! とか、 xjp.d bar ext 0xff        ; 分岐先の命令のext拡張を先取り!! . . . bar: and %r10, %r12     ; "xand %r10, %r12, 0xff"になります とか、軽く試した限りでは、すべて正しい結果が得られました。 実行時間を計測してみると、ディレイド分岐機能を使ったことによる高速化の効果が確認できます。 特に二つ目の、メモリアクセス命令をディレイドスロットに入れた例で、大幅な高速化の効果があるみたいです。 しかしながらさすがにこれらの複雑な命令になってくると、状況によっては結果が不安定になるのではないかという気もします。 とりあえず今回は「型変換命令もディレイド命令」という結果に満足し、その他の命令の実験はまた今後、としたいと思います。