+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2003年 ==================== * Tue Dec 23 08:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#4 (前回からの続き…) 引き続き、ディレイド分岐命令の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験します。 前々回は「jp.d %rb」、前回は「call.d %rb」「ret.d」について調べました。 今回は、残りの「jp.d sign8」「jr.d sign8」「call.d sign8」について調べます。     ○ディレイド分岐命令      1. jp.d sign8      2. jr.d sign8      3. call.d sign8     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元・分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 前回までと異なり、分岐元と分岐先のメモリの種類が違うパターンについては実験していません。 「jp.d sign8」「jr.d sign8」「call.d sign8」は前後256バイトまでの範囲にしか分岐できないため、 高速RAM(0x0000〜0x1fff)からSRAM(0x100000〜0x13ffff)へ、あるいはSRAMから高速RAMへは、遠すぎて分岐できないからです。 (「ext」命令と組み合わせれば前後256バイト以上の範囲に分岐できますが、メモリアクセス命令とディレイド分岐命令の間に「ext」命令が入ります。  ディレイド分岐命令の直前にメモリアクセス命令がある状況だけをテストしているので、「ext」命令を挟むケースは今回は除外することにしました。) 検証用プログラムはこちら:     「jp.d sign8」    http://www.piece-me.org/archive/delyerr4-20031223.zip     「jr.d sign8」 http://www.piece-me.org/archive/delyerr5-20031223.zip     「call.d sign8」   http://www.piece-me.org/archive/delyerr6-20031223.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     「jp.d sign8」    http://www.piece-me.org/piece-lab/delyerr4-20031223-result.csv     「jr.d sign8」 http://www.piece-me.org/piece-lab/delyerr5-20031223-result.csv     「call.d sign8」   http://www.piece-me.org/piece-lab/delyerr6-20031223-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     「jp.d sign8」    http://www.piece-me.org/piece-lab/delyerr4-20031223-kekka.csv     「jr.d sign8」 http://www.piece-me.org/piece-lab/delyerr5-20031223-kekka.csv     「call.d sign8」   http://www.piece-me.org/piece-lab/delyerr6-20031223-kekka.csv 結果、     --------------------------------     全ての組み合わせで、誤動作無し!     -------------------------------- 「jp.d sign8」「jr.d sign8」「call.d sign8」はいずれも非常に使用頻度の高いディレイド分岐命令で、これまでにも多用しています。 これまでの使用において特に問題は発生していなかったので、誤動作条件が存在しないのも予想通りでした。 -------------------------------------------------------------------------------------------------- 以上、四回に渡って、ディレイド分岐命令の誤動作条件を調べてきました。 まだ全てのメモリアクセス命令や全ての条件の組み合わせを試したわけではありませんが、とりあえずここで総括してみます。 結局、マニュアルに記載されていない誤動作条件が存在するディレイド分岐命令は、「jp.d %rb」だけでした。 ディレイド分岐命令を使ってプログラムを最適化する場合に、注意しなければいけないルールは次の通りです:     『「jp.d %rb」命令の直前にメモリアクセス命令を置いてはいけない』 とりあえずこのルールに従っておけば、誤動作することはないと思います。 より詳しい誤動作条件については、前々回の研究記録『ディレイド分岐命令の誤動作#2』をご参照ください。 このルール、S1C33209 CPUのマニュアルには載っていません。 マニュアルの記載漏れか、あるいは想定外の誤動作なのか・・・いずれにせよ、危険度「大」です。 * Mon Dec 22 22:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#3 (前回からの続き…) 前回は、ディレイド分岐命令「jp.d %rb」の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験しました。 その結果、いくつかの組み合わせで誤動作することがわかり、 『「jp.d %rb」命令の直前にメモリアクセス命令を置いてはいけない』 という結論が得られました。 今回は、ディレイド分岐命令「call.d %rb」と「ret.d」について、同様に実験してみることにします。 前回実験した「jp.d %rb」命令、実はディレイド分岐命令の中ではいちばん使用頻度が低いです。 「jp %rb」を使う場面はテーブルジャンプ処理ぐらいしかなく、C言語ではswitch文に相当します。 switch文の実行性能がプログラム全体の性能に大きく影響するようなケースは、比較的少ないと思います。 従って、「jp %rb」を「jp.d %rb」に置き換えてまで処理速度を稼ぐような場面も、あまりないと思うからです。 「jp.d %rb」に較べて、「call.d %rb」と「ret.d」の使用頻度ははるかに高いです。 「call.d %rb」は関数ポインタ呼び出しに限らず、頻繁に使う関数のアドレスをレジスタに保持するようなケースで頻出します。 「ret.d」は・・・実は僕はあまり使っていないのですが、EPSON社純正ライブラリの除算ルーチンがこれを有効活用しています。 以前の研究記録『実はディレイド命令』の回をご参照ください。 というわけで、「call.d %rb」「ret.d」の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験してみました。     ○ディレイド分岐命令      1. call.d %rb      2. ret.d     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 検証用プログラムはこちら:     「call.d %rb」 http://www.piece-me.org/archive/delyerr2-20031222.zip     「ret.d」   http://www.piece-me.org/archive/delyerr3-20031222.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     「call.d %rb」 http://www.piece-me.org/piece-lab/delyerr2-20031222-result.csv     「ret.d」   http://www.piece-me.org/piece-lab/delyerr3-20031222-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     「call.d %rb」 http://www.piece-me.org/piece-lab/delyerr2-20031222-kekka.csv     「ret.d」   http://www.piece-me.org/piece-lab/delyerr3-20031222-kekka.csv 結果、     --------------------------------     全ての組み合わせで、誤動作無し!     -------------------------------- (まだメモリアクセス命令「pushn %rs」「popn %rd」との組み合わせを試していませが、多分大丈夫じゃないかな?と思います。) これは助かりました! 前述の通り、手作業で最適化を行う場合に「call.d %rb」「ret.d」を使うケースは、「jp.d %rb」よりもずっと多いです。 「call.d %rb」と「ret.d」に誤動作条件があったら、かなりヤバイことになっているところでした。     『「call.d %rb」「ret.d」の使用においては、仕様外の誤動作を心配する必要はありません』 さて、残るディレイド分岐命令は「jp.d sign8」「jr.d sign8」「call.d sign8」です。 これらの命令も非常に使用頻度が高いですが、これまでにさんざん使って特に問題が出ていないので、多分大丈夫だと思います。 次回、実験予定です。 (続きます…) * Sat Dec 20 12:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#2 (前回からの続き…) 何通りかのパターンで検証を行ってみたところ、ディレイド分岐命令が誤動作する条件がおぼろげに見えてきました。 その条件とは、おおよそ次のようなものです。     『ディレイド分岐命令の直前にメモリアクセス命令があると、ディレイド命令が実行されないことがある』 前回はインターロック条件について詳しく触れましたが、どうやらインターロックの有無は関係ない感じです。 例えば、次のようなコードを高速RAMに置いて実行した場合にも誤動作となり、ディレイド命令が実行されません。     ; あらかじめ%r10に分岐先アドレスが入っているものとします。     xld.w %r4, 0x100000     xld.w %r5, [%r4+100] ; 直前にメモリアクセス命令があると…     jp.d %r10       ; インターロックしていなくても…     add %r6, 1      ; ★実行されない!!★ 予想以上に、誤動作の影響を受ける範囲が広いような気がしてきました。 これは詳細に検証してみる必要がありそうです。 それでは、どれぐらいのパターンを検証する必要があるのでしょうか? 少なく見積もっても、次に挙げるぐらいの検証は必要そうです。     ○ディレイド分岐命令      1. jp.d %rb      2. call.d %rb      3. ret.d      4. jp.d sign8      5. jr.d sign8      6. call.d sign8     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]      9. pushn %rs     10. popn %rd     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 6×10×2×2×2=480通り…ちょっと気が遠くなってきました(^^; さらに欲を言えば、     ・各命令がext命令を伴っているか否か     ・分岐先の最初にある命令の種類によって違いはあるか 等の条件も変えて試してみたいところです。 でも、いきなり全部は無理そうなので、とりあえず今回は次の組み合わせだけ検証してみることにしました。     ○ディレイド分岐命令      1. jp.d %rb     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 1×8×2×2×2=64通り。 これだけでも、ある程度の傾向はつかめるのではないかと思います。 検証用プログラムはこちら:     http://www.piece-me.org/archive/delyerr1-20031220.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     http://www.piece-me.org/piece-lab/delyerr1-20031220-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     http://www.piece-me.org/piece-lab/delyerr1-20031220-kekka.csv Microsoft Excel等のスプレッドシートソフトで読んでください。 出力結果から、いくつかのルールが見て取れます。 ---------------------------- 分岐先メモリの種類は関係ない ---------------------------- 分岐先メモリの種類(高速RAM/SRAM)だけが異なり、他の条件が全て同じケースでは、必ず同じ結果となっています。 分岐先メモリが高速RAMの場合に正常動作なら、分岐先メモリをSRAMに変えてみても正常動作。 分岐先メモリが高速RAMの場合に誤動作なら、分岐先メモリをSRAMに変えてみても誤動作。 このルールから推測すると、たぶん分岐先の最初にある命令の種類も関係ないんじゃないかな?と思います。 -------------------------------- メモリアクセスのサイズは関係ない -------------------------------- メモリアクセスのサイズ(BYTE/UNSIGNED BYTE/HALF/UNSIGNED HALF/WORD)だけが異なり、他の条件が全て同じケースでは、必ず同じ結果となっています。 SRAMへのメモリアクセスの場合、メモリアクセスのサイズによってタイミングが異なるのですが、それは誤動作の条件には関係ないようです。 ----------------------------------------- 分岐元メモリが高速RAMなら、必ず誤動作する ----------------------------------------- 今回の最初に提示した、誤動作するコードの条件がこれに相当します。 高速RAMに転送して使うプログラムを最適化する場合は、ディレイド分岐命令の直前にメモリアクセス命令を置かないよう、注意しなければいけません。 ---------------------------------------------------------------- 書き込みアクセス先メモリと分岐元メモリが共にSRAMなら、誤動作する ---------------------------------------------------------------- 今回の検証で新たに見つけた誤動作条件です。 例えば、次のようなコードをSRAM上で実行すると誤動作し、ディレイド命令が実行されません。     ; あらかじめ%r10に分岐先アドレスが入っているものとします。     xld.w %r4, 0x130000  ; アクセス先メモリ=SRAM     xld.w [%r4], %r5   ; メモリアクセス=書き込み     jp.d %r10     add %r6, 1      ; ★実行されない!!★ なお、このコードを高速RAM上で実行した場合は前出の条件に一致し、やはり誤動作します。 つまり、SRAMへの書き込みの直後にディレイド分岐命令を置いた場合は、高速RAMに転送して実行するプログラムでなくても誤動作する、ということです。 今回の検証のまとめです。 出力結果を見ると、ディレイド分岐命令の直前にメモリアクセス命令を置いても誤動作しないケースはいくつか存在します。 例えば、分岐元プログラムがSRAM上にあり、かつ、直前のメモリアクセスが読み込みならば、誤動作しません。 しかしながら、最後に挙げたような特殊条件下で誤動作するケースがあるため、安全のためには次に示す単純なルールに従っておくのが良さそうです。     『ディレイド分岐命令の直前にはメモリアクセス命令を置かない』 さて、今回はディレイド分岐命令「jp.d %d」についてだけ実験しましたが、他のディレイド分岐命令「call.d %rb」や「jr.d sign8」等ではどうでしょうか? これまでの経験から推測すると、「jr.d sign8」は直前にメモリアクセス命令があっても大丈夫そうな気がするのですが・・・ 次回は、ディレイド分岐命令の種類を変えて試してみようと思います。 (続きます…) * Wed Dec 17 12:55:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#1 「タンクバタリアン/EMU」にて、6502エミュレーション高速化のために手作業でアセンブラプログラムの最適化を行ったのですが、実はかなりハマリました。 問題の個所は比較的小さなサブルーチンで、何度見直しても間違っていないはずなのに、どうやっても期待通りの動作をしないのです。 使用頻度の高いサブルーチンだったので高速RAMに転送して実行していたところを、試しにSRAMに戻してみるとなぜか問題なく実行できてしまう・・・何故? 散々悩んだ末に、どうやら原因らしきものが掴めました。S1C33209 CPUコアの★誤動作★、さもなくばCPUマニュアルの★記載漏れ★っぽいです。 今回は、この件について記録しておこうと思います。 P/ECEのCPU S1C33209は、多くの一般的なRISCプロセッサと同じく、「ディレイド分岐機能」を備えています。 ディレイド分岐機能については、以前の研究記録『実はディレイド命令』でも触れましたので、そちらも併せてご参照ください。 今回問題となった個所は、次のような処理を行う部分でした。(※かなり簡略化しています)     『6502機械語命令を1バイト読み込み、その内容によって分岐する』 C言語で書くと、こんな感じの処理です。 --------------------------------------------------------------------------------------------------     unsigned char* pc = ...;     switch(*pc++) {     case 0: ...; break;     case 1: ...; break;         ...     case 9: ...; break;     } -------------------------------------------------------------------------------------------------- これをアセンブラで書くと、次のようになります。 -------------------------------------------------------------------------------------------------- 1|     ; 2|     ; %r0レジスタに変数pcが割り当てられているものとします。 3|     ; 4|     ld.ub %r1, [%r0]    ; code = *pc 5|     xld.w %r2, TABLE    ; addr = TABLE[code] 6|     sll %r1, 2 7|     add %r1, %r2 8|     ld.w %r3, [%r1] 9|     jp.d %r3        ; goto addr 10|     add %r0, 1       ; pc++ 11|      12| TABLE:             ; switch用ジャンプテーブル 13|     .word CASE0 14|     .word CASE1 15|     ... 16|     .word CASE9 17|     18| CASE0: 19|     ...           ; code=0の場合の処理 20| CASE1: 21|     ...           ; code=1の場合の処理 22|     ... 23| CASE9: 24|     ...           ; code=9の場合の処理 -------------------------------------------------------------------------------------------------- ※これ以降の説明で個々の命令について実行サイクル数を調べて行きますが、とりあえずメモリアクセス時のウェイトサイクルは除外して考えます。 9〜10行目にご注目ください。 ディレイド分岐機能を使って、ジャンプ命令を1サイクル高速化しています。     jp.d %r3    ; 1サイクル     add %r0, 1   ; 1サイクル もしディレイド分岐機能を使わずに普通に書けば、次のようになります。     add %r0, 1   ; 1サイクル     jp %r3     ; 2サイクル 従って、ディレイド分岐機能を使った方が1サイクル速くなります。 ・・・と言いたいところですが、実はこの場合に限って言えば、ディレイド分岐機能を使った場合と使わない場合のサイクル数は変わらないのです。 8〜9行目にご注目ください。     08|    ld.w %r3, [%r1]     09|    jp.d %r3 8行目でメモリから読み込んだ%r3の値を、直後に9行目のジャンプ命令で飛び先アドレスとして参照しています。 S1C33209 CPUではこのような処理を行った場合に「インターロック」と呼ばれる遅延が発生し、命令の実行時間が1サイクル長くなるのです。 詳しくは「S1C33000コアCPUマニュアル(33000Core-J.pdf)」34ページ『3.2 プログラム実行状態 (7) インターロックによる遅延』をご参照ください。 結局、9〜10行目の本当の実行サイクル数は次のようになります。     jp.d %r3    ; 1サイクル+1サイクル(インターロック)     add %r0, 1   ; 1サイクル ディレイド分岐命令を使わない場合は、ジャンプ命令の実行時間が1サイクル長い代わりにインターロックは発生せず、 ディレイド分岐命令を使った場合は、ジャンプ命令の実行時間が1サイクル短い代わりにインターロックが発生します。 従ってこの場合に限って言えば、どちらの方法を使っても全体のサイクル数は3サイクルとなり、実行速度は同じです。 ではなぜディレイド分岐命令を使う方のプログラムを採用したかと言うと、次のような判断によります。 ディレイド分岐命令を使わない方の合計実行サイクル数は、必ず3サイクルとなります。例外はありません。 それに対し、ディレイド分岐命令を使った場合の合計実行サイクル数は、非常に幸運な場合に2サイクルとなる可能性があります。 どんな幸運かと言いますと、8〜9行目の間に割り込みが発生した場合です。 8行目の命令実行直後にタイマやサウンド等の割り込みが発生すると、8行目と9行目の間で割り込み処理を行うことになります。 すると、8行目でメモリから読み込んだ%r3の値を、「直後に」9行目で参照するわけではなくなります。 従って9行目でインターロックは発生せず、9行目と10行目の合計実行サイクル数は2サイクルとなります。 ・・・とは言えこんな幸運は滅多にあるわけでなし、全体の速度から見れば 0.00...01% 程度の違いでしかないでしょう。 どちらの方法を使うかは全く“気分の問題”であり、重要なのは、どちらの方法でも正しく動く、ということです。 ■■■■■■■■■■ ここまでは問題ありません。仕様通りの動作です。問題はここから ■■■■■■■■■■ ところが実は、ある条件が重なった場合に、ディレイド分岐命令を使う方のプログラムは正しく動かなくなります。 ある条件とは、     このサブルーチンを高速RAM上で実行した場合 です。わざわざ手作業で最適化するからには高速性が必要なサブルーチンであり、もちろん高速RAMに転送して実行するでしょう。 今回が正にこのケースに相当しました。 ディレイド分岐命令を使う方のプログラムを高速RAMに置いて実行すると、次のような振る舞いとなります。(※サイクル数は推測です)      8|    ld.w %r3, [%r1]   ; 1サイクル      9|    jp.d %r3       ; 1サイクル+1サイクル(インターロック)      |               ; ★ここでジャンプしてしまう!!★     10|    add %r0, 1      ; ★実行されません!!★ すなわち、ディレイド命令であるはずの10行目「add %r0, 1」が実行されないのです。 C言語に例えると、     switch(*pc++) {         ...     } と書いたつもりが実際の動作は     switch(*pc) {         ...     } になってしまう、という事態。正しい結果が得られるはずもありません。 これは危険です!! もう少し詳しく、症状が発現する条件を調べておく必要がありそうです。 (続きます…) * Mon May 5 05:30:00 JST 2003 Naoyuki Sawa - 警告の範囲が違う 昨日書いた「距離を求める(誤差±約1%・30ビット版)」のアセンブラプログラム、実はかなりハマリました。 どうやっても結果が合わず、さんざん悩んだ末に見つけた原因は次のようなものでした。 正しくは     xld.w %r13, 55 と書くべきところを     ld.w %r13, 55 と書いてしまっていたのです。 P/ECEのCPU S1C33の命令セットは、たいていの場合、即値に6ビットまでの数値しか指定できません。 それ以上のビット数の即値を使いたい場合は、ext命令を使って桁を拡張する必要があります。 上の例に挙げた「ld」命令の場合、即値には符号付き6ビットまでしか指定できません。 符号付き6ビットで表現できる数値は-32〜31ですので、     ・     ・     ・     ld.w %r13, 34      ; 間違い!     ld.w %r13, 33      ; 間違い!     ld.w %r13, 32      ; 間違い!     ld.w %r13, 31      ; 正しい     ld.w %r13, 30      ; 正しい     ld.w %r13, 29      ; 正しい     ・     ・     ・     ld.w %r13, 1      ; 正しい     ld.w %r13, 0      ; 正しい     ld.w %r13, -1      ; 正しい     ・     ・     ・     ld.w %r13, -30     ; 正しい     ld.w %r13, -31     ; 正しい     ld.w %r13, -32     ; 正しい     ld.w %r13, -33     ; 間違い!     ld.w %r13, -34     ; 間違い!     ld.w %r13, -35     ; 間違い!     ・     ・     ・ となります。 また別の例として、「add」命令などは即値に符号なし6ビットまでの数値しか指定できません。 符号なし6ビットで表現できる数値は0〜63ですので、     ・     ・     ・     add %r13, 66      ; 間違い!     add %r13, 65      ; 間違い!     add %r13, 64      ; 間違い!     add %r13, 63      ; 正しい     add %r13, 62      ; 正しい     add %r13, 61      ; 正しい     ・     ・     ・     add %r13, 1       ; 正しい     add %r13, 0       ; 正しい     add %r13, -1      ; 間違い!     add %r13, -2      ; 間違い!     add %r13, -3      ; 間違い!     ・     ・     ・ となります。 さて、今回のミス     ld.w %r13, 55 は、「ld」命令が扱える符号付き6ビットの範囲(-32〜31)を外れた即値(55)を指定してしまったことです。 確かに当方のミスなのですが、本来、こういうミスに対してはアセンブラが警告を発することになっています。     「Warning: Numeric range.:インストラクションオペランドの数値が不正です。」     S1C33 Family Cコンパイラパッケージマニュアル Ver.4 (S5U1C33000C_J.pdf) 179ページより ところが、アセンブラは上のコードに対して警告を発しませんでした。 そして、55を無理やり符号付き6ビットと解釈した値で、命令コードを生成してしまっていたのです。     ld.w %r13, -9      ; -9 … 55を無理やり符号付き6ビットと解釈した値 なぜ警告が発せられなかったのでしょうか? いくつか試してみたところ、S1C33 CPUのアセンブラas33.exeは、警告とする数値の範囲を間違えているようです。 「ld」命令といろいろな数値を組み合わせて、警告が発せられるかどうかを試してみました。     ・     ・     ・     ld.w %r13, 66      ; 間違い!   ; 警告あり     ld.w %r13, 65      ; 間違い!   ; 警告あり     ld.w %r13, 64      ; 間違い!   ; 警告あり     ld.w %r13, 63      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 62      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 61      ; 間違い!   ; 警告なし!危険!     ・     ・     ・     ld.w %r13, 34      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 33      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 32      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 31      ; 正しい    ; 警告なし     ld.w %r13, 30      ; 正しい    ; 警告なし     ld.w %r13, 29      ; 正しい    ; 警告なし     ・     ・     ・     ld.w %r13, 1      ; 正しい    ; 警告なし     ld.w %r13, 0      ; 正しい    ; 警告なし     ld.w %r13, -1      ; 正しい    ; 警告なし     ・     ・     ・     ld.w %r13, -30     ; 正しい    ; 警告なし     ld.w %r13, -31     ; 正しい    ; 警告なし     ld.w %r13, -32     ; 正しい    ; 警告なし     ld.w %r13, -33     ; 間違い!   ; 警告なし     ld.w %r13, -34     ; 間違い!   ; 警告なし     ld.w %r13, -35     ; 間違い!   ; 警告なし     ・     ・     ・ 32〜63までの数値が、正しい命令コードが生成されないにも関わらず無警告となっています。 「add」命令についても同様に試してみます。     ・     ・     ・     add %r13, 66      ; 間違い!   ; 警告あり     add %r13, 65      ; 間違い!   ; 警告あり     add %r13, 64      ; 間違い!   ; 警告あり     add %r13, 63      ; 正しい    ; 警告なし     add %r13, 62      ; 正しい    ; 警告なし     add %r13, 61      ; 正しい    ; 警告なし     ・     ・     ・     add %r13, 1       ; 正しい    ; 警告なし     add %r13, 0       ; 正しい    ; 警告なし     add %r13, -1      ; 間違い!   ; 警告なし!危険!     add %r13, -2      ; 間違い!   ; 警告なし!危険!     add %r13, -3      ; 間違い!   ; 警告なし!危険!     ・     ・     ・     add %r13, -30      ; 間違い!   ; 警告なし!危険!     add %r13, -31      ; 間違い!   ; 警告なし!危険!     add %r13, -32      ; 間違い!   ; 警告なし!危険!     add %r13, -33      ; 間違い!   ; 警告あり     add %r13, -34      ; 間違い!   ; 警告あり     add %r13, -35      ; 間違い!   ; 警告あり     ・     ・     ・ -32〜-1までの数値が、正しい命令コードが生成されないにも関わらず無警告となっています。 どうやらas33.exeは、命令によって有効な数値の範囲が違うことを忘れ、常に-32〜63が正しい数値の範囲だと思ってしまっているようです。 今回は小さなプログラムだったので間違いに気付いたものの、大きなプログラムで間違ったコードが無警告で通ってしまっていたら・・・ この危険を回避するには、「ld」や「add」等の命令で即値を使う場合は、たとえ小さな値でも常に「xld」「xand」等と表記しましょう。 各命令の有効な数値範囲を超えると、コンパイラが自動的に「ext」命令を付加して、誤った命令コードの生成を防いでくれます。 * Sun May 4 15:00:00 JST 2003 Naoyuki Sawa - 距離を求める(誤差±約1%・30ビット版) P/wareさん(http://www.aw.wakwak.com/~hitode/piece/)のページの、 距離を求める(http://www.aw.wakwak.com/~hitode/piece/index.html#dist2)高速な方法が更新されています。 まず、前回と同じ誤差±約4%の方法で、より精度の高い係数が公開されました。 前回のプログラムを、新しい係数で作り直したものがこちら。     http://www.piece-me.org/archive/hypot2a-20030504.zip さらに、「誤差約1%」と「誤差約0.2%」の計算方法が追加されています。 誤差約1%の方法が、速度と精度のバランスが取れていて、良さそうです。 そこで今回は、誤差約1%の計算方法の30ビット版を作ってみました。     /* 原点と点 (x, y) の距離を求めます。      * [in]      *   x, y      距離を求める座標      * [out]      *   戻り値     原点と点 (x, y) の距離      * [note]      *   * 距離の近似をもとめる計算式:      *       |x|>|y|のとき、|x|+((|y|*|y|/|x|*55)>>7)      *       (誤差は±約1%)      */     int hypot4(int x, int y);         asm("         .code         .align 1         .global hypot4     hypot4:         ld.w %r10, %r12     ; if(x == 0 && y == 0) return 0         or %r10, %r13         jreq hypot4_exit         ;         cmp %r12, 0       ; %r12 = |x|         jrge 3          not %r12, %r12          add %r12, 1         cmp %r13, 0       ; %r13 = |y|         jrge 3          not %r13, %r13          add %r13, 1         cmp %r12, %r13      ; if(|x| < |y|) %r12 exch %r13         jrge 4          xor %r13, %r12          xor %r12, %r13          xor %r13, %r12         ;         mltu.w %r13, %r13    ; %r5:%r4 = |y|^2         ld.w %r4, %alr         ld.w %r5, %ahr         ;         xld.w %r13, 55      ; %r6:%r5:%r4 = |y|^2 * 55         mltu.w %r4, %r13         ld.w %r4, %alr         ld.w %r6, %ahr         mltu.w %r5, %r13         ld.w %r5, %alr         add %r5, %r6         ;{{         jruge.d 3        ; (!C)          ld.w %r6, %ahr     ; (undoc'd delay)          add %r6, 1         ;}}または{{         ;ld.w %r6, %ahr         ;adc %r6, %r8      ; -gp=0x0 を想定         ;}}         ;         ld.w %r13, %r5      ; %r5:%r4 = (|y|^2 * 55) >> 7         xsll %r13, 25         xsrl %r4, 7         or %r4, %r13         xsll %r6, 25         xsrl %r5, 7         or %r5, %r6         ;         ld.w %alr, %r4      ; %alr = ((|y|^2 * 55) >> 7) / |x|         div0u %r12         ld.w %ahr, %r5         div1 %r12        ; 1         div1 %r12        ; 2         div1 %r12        ; 3         div1 %r12        ; 4         div1 %r12        ; 5         div1 %r12        ; 6         div1 %r12        ; 7         div1 %r12        ; 8         div1 %r12        ; 9         div1 %r12        ; 10         div1 %r12        ; 11         div1 %r12        ; 12         div1 %r12        ; 13         div1 %r12        ; 14         div1 %r12        ; 15         div1 %r12        ; 16         div1 %r12        ; 17         div1 %r12        ; 18         div1 %r12        ; 19         div1 %r12        ; 20         div1 %r12        ; 21         div1 %r12        ; 22         div1 %r12        ; 23         div1 %r12        ; 24         div1 %r12        ; 25         div1 %r12        ; 26         div1 %r12        ; 27         div1 %r12        ; 28         div1 %r12        ; 29         div1 %r12        ; 30         div1 %r12        ; 31         div1 %r12        ; 32         ;         ld.w %r10, %alr     ; %r10 = ((|y|^2 * 55) >> 7) / |x| + |x|         add %r10, %r12     hypot4_exit:         ret         "); オリジナルの計算式は     |x|+((|y|*|y|/|x|*55)>>7) なのですが、|y|*|y| と |x| の値が近いときに誤差が大きくなり、例えば (-13, 8) で約7%の誤差が発生してしまいます。 そこで、ちょっと計算式をアレンジして、     |x|+((|y|*|y|*55>>7)/|x|) としてみました。 この関数を含む、動作検証用プログラムはこちら。     http://www.piece-me.org/archive/hypot4-20030504.zip 前回と同じく、ランダムな座標 (x, y) と原点 (0, 0) 間の距離を求め、本当の値との誤差を表示します。 10万回程度繰り返してみたところ、ほとんど±1%以内に収まり、最悪でも1.5%を超えることはありませんでした。 前回の4%・30ビット版は、一回あたり約30サイクル前後で完了していました。 今回の1%・30ビット版は、一回あたり約90サイクル前後かかってしまいます。 (いずれもソースコード上での合計値で、実測値ではありません。メモリウェイト等も考慮していません。) しかしながら、三倍程度の遅さで四倍の精度が得られるのなら、1%版の方を使おうかな?と思いました。 * Sat May 3 08:50:00 JST 2003 Naoyuki Sawa - 距離を求める(30ビット版) P/wareさん(http://www.aw.wakwak.com/~hitode/piece/)のページで、 距離を求める(http://www.aw.wakwak.com/~hitode/piece/index.html#dist)高速な方法が説明されています。 特に、誤差4%の近似式を使った方法 hypot2() が参考になります。 僕はこれまでマトモに平方根を計算してました・・・これからはこの方法を使わせて頂きます。 先のページにも説明されている通り、hypot2() は (x, y) が 16ビットまでの数値でないと正しく動きません。 固定小数点数などを使っていると、16ビット以上の座標値や距離が必要になってくることも結構あります。 そこで、誤差を増やさずに、30ビットまでの (x, y) に対応した版を作ってみました。 なぜ30ビットまでなのか(31ビットや32ビットの座標値には対応できないのか)というと、 31ビット以上の座標値では結果の距離そのものが32ビット符号付き整数をオーバーフローしてしまうからです。     /* 原点と点 (x, y) の距離を求めます。      * [in]      *  x, y      距離を求める座標      * [out]      *  戻り値     原点と点 (x, y) の距離      * [note]      *  * 距離の近似をもとめる計算式:      *   |x|>|y|のとき、|x|*0.9604+|y|*0.3978      *   (誤差は±約4%)      */     int hypot2(int x, int y);         asm("         .align 1         .global hypot2     hypot2:         cmp %r12, 0        ; %r12 = |x|         jrge 3          not %r12, %r12          add %r12, 1         cmp %r13, 0        ; %r13 = |y|         jrge 3          not %r13, %r13          add %r13, 1         cmp %r12, %r13      ; if(|x| < |y|) %r12 exch %r13         jrge 4          xor %r13, %r12          xor %r12, %r13          xor %r13, %r12         xld.w %r4, 0xf5dcc63f   ; %r10 = |x| * (0.9604 << 32)         mltu.w %r12, %r4         ld.w %r10, %ahr         xld.w %r4, 0x65d63886   ; %r10 += |y| * (0.3978 << 32)         mltu.w %r13, %r4         ld.w %r4, %ahr         add %r10, %r4         ret         "); 使い方はオリジナルの hypot2() と同じです。 この関数を含む、動作検証用プログラムはこちら。     http://www.piece-me.org/archive/hypot-20030503.zip ランダムな座標 (x, y) と原点 (0, 0) 間の距離を求め、本当の値との誤差を表示します。 10000回程度繰り返してみたところ、確かに誤差が±4%を超えることはありませんでした。 余談ながら、インラインアセンブラ asm("〜") を関数定義の外側で使っている点にご注目ください。 Cコンパイラが暗黙に生成するコードの心配をしなくていいので、僕は最近この方法を好んで使ってます。 (単独のアセンブラソースに分けるのが面倒なので・・・(^^;) * Fri Apr 12 20:30:00 JST 2003 Naoyuki Sawa - 実はディレイド命令 P/ECEのCPU S1C33209は、RISC CPUの特徴のひとつ、「ディレイド分岐機能」を持っています。 ディレイド分岐機能とは、分岐命令の実行より先に分岐命令の直後の命令を実行し、実行効率を稼ぐ機能です。 ディレイド分岐機能を使う分岐命令を、「ディレイド分岐命令」と呼びます。 また、ディレイド分岐命令の直後に置かれる命令を、「ディレイド命令」と呼びます。 ほとんどの分岐命令は、ディレイド分岐命令として利用可能です。 しかし、ディレイド命令として利用可能な命令はある程度限られています。 単純なレジスタ間ロード命令や加減算・論理演算命令は、だいたいディレイド命令として利用可能なのですが、 直感に反してディレイド不可な命令もいくつかあるので注意が必要です。 例えば、符号拡張・ゼロ拡張を伴うレジスタ間ロード命令がディレイド不可というのは、ちょっと戸惑います。     ld.b %rd, %rs     ×ディレイド不可     ld.ub %rd, %rs    ×ディレイド不可     ld.h %rd, %rs     ×ディレイド不可     ld.uh %rd, %rs    ×ディレイド不可     ld.w %rd, %rs     ○ディレイド可能 その他に、汎用レジスタと特殊レジスタ間のロード命令もディレイド不可となっています。     ld.w %rd, %ss     ×ディレイド不可     ld.w %sd, %rs     ×ディレイド不可 汎用レジスタと特殊レジスタ間のロード命令がディレイド不可というのは、感覚的に理解できなくもありません。 ・・・と思っていたのですが、実はディレイド可能でした。 今回は、この件について記録しておこうと思います。 S1C33000コアCPUマニュアルによると、「ld.w %rd,%ss」「ld.w %sd,%rs」はディレイド不可となっています。 (33000Core-J.pdf p.112「ld.w %rd,%ss」, p.117「ld.w %sd,%rs」, Appendix-1「Quick Reference」参照) しかし実際には、これらの命令もディレイド命令として利用できるようです。 なぜなら、EPSON社純正ライブラリの除算ルーチンが、これらをディレイド命令として使っているからです。 例えば、符号付き除算ルーチンは次のように実装されています。     __divsi3:         ld.w %alr, %r12         div0s %r13         ld.w %r4, 0x4         ld.w %r5, %psr     __divsi3_loop_start:         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         sub %r4, 0x1         jrne.d __divsi3_loop_start   <= 次の命令がディレイド命令になります         ld.w %psr, %r5         <= ld.w %sd(0),%rs(5) に相当します         div2s %r13         div3         ret.d              <= 次の命令がディレイド命令になります         ld.w %r10, %alr         <= ld.w %rd(10),%ss(2) に相当します S1C33000コアCPUマニュアルによると、規定のディレイド可能な命令以外をディレイド命令として用いた場合、 「動作が不安定となる」(p.30「ディレイド分岐機能」参照)とあります。 前述の除算ルーチンのケースはたまたま動いていて、別の命令並びでは「動作が不安定となる」可能性もあるのですが、 わざわざそんな危険な実装をするとは考えづらいので、たぶんいつでもディレイド可能なんじゃないかな、と思います。 * Fri Feb 14 12:30:00 JST 2003 Naoyuki Sawa - memcpy()でアドレス不整例外 『XMプレイヤー』作成において、なかなか消えない「アドレス不整例外」に悩まされました。 今回は、その顛末をまとめておこうと思います。 P/ECEのCPU・S1C33209は、「不整アドレス」からのデータ読み出しを行おうとすると、「アドレス不整例外」を発生します。 「不整アドレス」とは、読み出そうとしているデータ型のサイズの倍数になっていないアドレスのことです。 例えば、int型データを読み出す場合、intは4バイトですので、読み出すアドレスも4の倍数でなければいけません。 同様に、short型データを読み出す場合は、shortが2バイトですから、読み出すアドレスも2の倍数でなければいけません。 「アドレス不整例外」が発生すると、画面には「Address error exception」と表示され、リセットを押すしかなくなります。 以下に、「アドレス不整例外」を発生するコード例を、いくつか示します。     int *p, v; p = (int*)0x120000; v = *p;  /* 0x120000番地は4の倍数なので、「アドレス不整例外」は発生しません */     int *p, v; p = (int*)0x120001; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120002; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120003; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120004; v = *p;  /* 0x120004番地は4の倍数なので、「アドレス不整例外」は発生しません */     shrot *p, v; p = (shrot*)0x120000; v = *p;  /* 0x120000番地は2の倍数なので、「アドレス不整例外」は発生しません */     shrot *p, v; p = (shrot*)0x120001; v = *p;  /* 「アドレス不整例外」が発生します! */     shrot *p, v; p = (shrot*)0x120002; v = *p;  /* 0x120002番地は2の倍数なので、「アドレス不整例外」は発生しません */ 通常のプログラムでは、「アドレス不整例外」に注意する必要はありません。 intデータは4の倍数アドレスに、shortデータは2の倍数アドレスに、コンパイラが適切に配置してくれるからです。 注意が必要になるのは、あらかじめ決められた形式を持つデータ構造から、データを読み出す場合です。 今回問題となったXM形式のデータ構造がまさにそれです。 XM形式のデータ構造では、奇数個のchar型データの直後に、空白なしでint型データが配置されていたりします。 つまり、int型のデータが「不整アドレス」に配置されていることになります。 メモリ上に読み込んだXM形式のデータ構造から、このようなint型データを読み出そうとすると、「アドレス不整例外」が発生します。 =============================================================================================================================== 「不整アドレス」からの読み出しができない、という制約を持つのは、S1C33209だけが特別ではありません。 いちばんメジャーなPentiumシリーズCPUにはこの制約がないものの、他の多くのCPUは同様の制約を持っています。 ですから、「不整アドレス」にデータが配置されている可能性のあるようなデータ構造からデータ読み出しを行う場合は、 「アドレス不整例外」が発生しないよう、プログラム側で対処しなければいけません。 いちばんオーソドックスなのは、次のような方法です。 どんなアドレスからでも「アドレス不整例外」なしにint型データを読み出せるような、補助関数を用意します。     int read_int(const int* p) {         const char* pp = (const char*)p;         return (pp[0] << 24) | (pp[1] << 16) | (pp[2] << 8) | (pp[3]);     } 4バイトの読み出しを1バイトづつ行ってから、int型に組み立てて返します。 1バイトづつの読み出しならば、絶対に「アドレス不整例外」は発生しません。(1の倍数でないアドレスはないから) この補助関数を使えば、引数pにどんなアドレスが指定されたとしても、そこからint型のデータが読み出せるわけです。 それでは、この補助関数を使って、先ほど挙げた例を書き直してみます。     int *p, v; p = (int*)0x120000; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120001; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120002; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120003; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120004; v = read_int(p);  /* OK! */ =============================================================================================================================== 以上の方法が、オーソドックスかつ正しい方法です。 ・・・が、しかし。僕はズボラなので、read_int()を次のように定義してしまいます。     int read_int(const int* p) { int v; memcpy(&v, p, sizeof v); return v; } どこらへんがズボラかといいますと、いろいろな型のための補助関数を追加する場合、機械的な書き換えで対応できる点です。      int read_int (const int * p) { int v; memcpy(&v, p, sizeof v); return v; }      short read_short (const short * p) { short v; memcpy(&v, p, sizeof v); return v; }     unsigned int read_unsigned_int (const unsigned int * p) { unsigned int v; memcpy(&v, p, sizeof v); return v; }     unsigned short read_unsigned_short(const unsigned short * p) { unsigned short v; memcpy(&v, p, sizeof v); return v; }      float read_float (const float * p) { float v; memcpy(&v, p, sizeof v); return v; }      double read_double (const double* p) { double v; memcpy(&v, p, sizeof v); return v; } ちなみに、オーソドックスな方法でread_short()やread_double()を実装すると、こうなります。     short read_short(const short* p) {         const char* pp = (const char*)p;         return (pp[0] << 8) | (pp[3]);     }     float read_double(const double* p) {         const char* pp = (const char*)p;         char v[8];         v[0] = pp[0];         v[1] = pp[1];         v[2] = pp[2];         v[3] = pp[3];         v[4] = pp[4];         v[5] = pp[5];         v[6] = pp[6];         v[7] = pp[7];         return *(double*)v;     } まあこの程度ですので大した手間ではないのですが、この程度の手間も惜しむほどズボラだという…(^^; これまでP/ECE以外の環境でも、memcpy()を使ったズボラ版補助関数を使っていましたし、それで上手くいっていると思っていました。 ・・・が、それは間違いでした。 =============================================================================================================================== ズボラ版補助関数は、memcpy()が内部でデータを1バイトづつコピーしてくれることを前提としています。 いや、良く出来たmemcpy()の実装の場合、指定された転送元・転送先アドレスと転送サイズによっては、 数バイトづつのコピーを行って処理を高速化するとは聞いていましたが、 少なくとも「アドレス不整例外」が発生するような方法は採られないものと思い込んでいました。 ところが、コンパイラによる最適化が絡んでくると、必ずしもそうとは言えないようなのです。 それではまず、「アドレス不整例外」を発生するmemcpy()の使用例を示します。 -------------------------------------------------------------------------------------------------------------------------------     #include     #include          volatile int* ptr;     volatile int val;     volatile char data[] = { 0,1,2,3,4,5 };     /* ここを読みます ~~~~~~~ */          int read_int(const int* p) { int v; memcpy(&v, p, sizeof v); return v; }          void pceAppInit() { }     void pceAppExit() { }     void pceAppProc(int count) {         ptr = (int*)&data[1];         val = read_int(ptr);     } ------------------------------------------------------------------------------------------------------------------------------- 変数にvolatile指定を追加しているのは、最適化によって読み出し処理そのものが削られてしまわないようにするためです。 特に重要な意味はありません。 さて、このプログラムを最適化オプション「-O2」を指定してコンパイルします。 そして実行すると・・・「アドレス不整例外」が発生します! 「-ls」オプションを指定してシンボルファイルを作成してみると、data[]配列は0x100264番地に確保されていました。 プログラムでは&data[1]から読み出そうとしているので、つまり0x100265番地からint型データを読み出していることになります。 そのまま読み出したのでは「アドレス不整例外」が発生するのは必然ですが、今回はちゃんと補助関数を使っています。 にもかかわらず「アドレス不整例外」発生。なぜ? =============================================================================================================================== memcpy()のところで、コンパイラはどのようなコードを生成しているのでしょうか? 「-b」オプションを指定して、コンパイラが生成したアセンブラソースを確認してみます。 ※読みやすいように、少し編集してあります。 ----------------------------------------[S1C33用gccの生成コード]---------------------------------------------------------------     read_int:         xsub %sp, %sp, 4         xld.w %r10, [%r12]         xld.w [%sp], %r10         xadd %sp, %sp, 4         ret          pceAppProc:         xld.w %r12, data+1         xld.w [ptr], %r12         xcall read_int         xld.w [val], %r10         ret ------------------------------------------------------------------------------------------------------------------------------- なんと、単なるint型データの読み出し・格納になってしまっています! 「アドレス不整例外」が発生するわけです。 これは明らかに、コンパイラの最適化による副作用です。 では、このような最適化が行われるのは ・P/ECEのコンパイラ(S1C33用gcc)が特別なのでしょうか? ・それとも、gccならどれでもそうなのでしょうか? ・あるいは、どのコンパイラでもたいていこうなるのでしょうか? まずは、他のCPU用のgccで確認してみます。 SPARC用gccで同様のプログラムを試してみると、やはり「アドレス不整例外」になりました。 (Linux上で試したので、画面表示は「Bus Error」となります。) コンパイラが生成したアセンブラソースは、次のようになります。 ※読みやすいように、少し編集してあります。 ----------------------------------------[SPARC用gccの生成コード]---------------------------------------------------------------     read_int:         retl         ld [%o0], %o0          main:         save %sp, -104, %sp         sethi %hi(data+1), %o0         call read_int,0         or %o0, %lo(data+1), %o0         ret         restore %g0, %o0, %o0 ------------------------------------------------------------------------------------------------------------------------------- 僕は、SPARCプロセッサのアセンブラはぜんぜん読めませんが、read_int()が1バイトづつコピーしていないのはわかります。 (あと余談ですが、ディレイスロットによる最適化で命令の順番が入れ替わったりしているのが見えます。  また、P/ECEのS1C33用gccは、SPARC用gccに較べて、不要なローカル変数の削除による最適化がまだまだ弱いこともわかります。) どうやらgccを使って最適化コンパイルを行うと、だいたい同じような結果になるようです。 それでは、別のコンパイラではどうでしょうか? VisualC++6.0を使って、同様のプログラムをコンパイルしてみます。 最適化オプションは、Releaseビルドの初期設定のままとしました。 ※読みやすいように、少し編集してあります。 ----------------------------------------[Pentium用VisualC++6.0の生成コード]----------------------------------------------------     read_int:         mov eax, dword ptr [esp+4]         mov eax, dword ptr [eax]         ret          main:         push offset data+1         call read_int         add esp, 4         ret ------------------------------------------------------------------------------------------------------------------------------- 同じです。やはり、read_int()は1バイトづつコピーしていません。 (ただしPentiumの場合はそもそも「アドレス不整例外」の制約がありませんので、このプログラムを実行しても問題は出ません。) どうやらどのコンパイラでも、memcpy()が最適化されて不整アドレスからの読み出しが発生する可能性はあるようです。 しかし、こんな重大な問題にこれまで気が付かなかったとは・・・ 例えば、P/ECEで仮想VRAMの(0,1)〜(0,4)のピクセルを(1,1)〜(1,4)へコピーしようとしたら、「アドレス不整例外」になるのでしょうか? -------------------------------------------------------------------------------------------------------------------------------     #include          unsigned char vbuff[128 * 88];          void pceAppInit() {         pceLCDSetBuffer(vbuff);         pceLCDDispStart();     }     void pceAppExit() { }     void pceAppProc(int count) {         unsigned char *src, *dst;         src = &vbuff[ 1];         dst = &vbuff[129];         memcpy(dst, src, 4);         pceLCDTrans();     } ------------------------------------------------------------------------------------------------------------------------------- 試してみると・・・「アドレス不整例外」は発生しません。 そりゃあ 、これで「アドレス不整例外」が発生するのなら、とっくに気付いているはずですよね。 生成されるコードは次のようになります。 ※読みやすいように、少し編集してあります。 -------------------------------------------------------------------------------------------------------------------------------     pceAppProc:         xld.w %r13, vbuff+1         xadd %r12, %r13, 128         xld.w %r14, 0x00000004         xcall memcpy         xcall pceLCDTrans         ret ------------------------------------------------------------------------------------------------------------------------------- 同じ4バイトのコピーなのに、こちらはちゃんとmemcpy()関数が呼び出されています。 「アドレス不整例外」になる例との違いは・・・memcpy()に渡しているポインタの型でしょうか? それでは、プログラムを次のように変更して、もういちど試してみます。 -------------------------------------------------------------------------------------------------------------------------------     #include          unsigned char vbuff[128 * 88];          void pceAppInit() {         pceLCDSetBuffer(vbuff);         pceLCDDispStart();     }     void pceAppExit() { }     void pceAppProc(int count) {         int *src, *dst;         src = (int*)&vbuff[ 1];         dst = (int*)&vbuff[129];         memcpy(dst, src, 4);         pceLCDTrans();     } ------------------------------------------------------------------------------------------------------------------------------- で、実行してみると・・・「アドレス不整例外」が発生しました! 生成されるコードは次の通り。 ※読みやすいように、少し編集してあります。 -------------------------------------------------------------------------------------------------------------------------------     pceAppProc:         xld.w %r11, vbuff+1         xld.w %r10, [%r11]         xld.w [%r11+128], %r10         xcall pceLCDTrans         ret ------------------------------------------------------------------------------------------------------------------------------- vbuff[1]の位置からint型で読み出しを行い、vbuff[1+128]の位置に格納しようとしています。 なんとなく見えてきました。     memcpy()にint*型のポインタを渡すと、単なるint型データの読み出しとして最適化される可能性がある。     その際、ポインタが「不整アドレス」を指しているかどうかは調べられない。     結果、ポインタが「不整アドレス」を指していたなら、「アドレス不整例外」が発生する。 P/ECEの仮想VRAMを扱う場合は、たいていunsigned char*型のポインタを渡すので、問題にならなかったのですね。 =============================================================================================================================== 回避方法は次のとおりです。 1.オーソドックスなread_int()を使う。   つまり、memcpy()を使わず、明示的に1バイトづつ読み出して、組み立てる。 2.ズボラ版read_int()の引数型を変更する。   修正後のread_int()は、次のようになります。     int read_int(const void* p) { int v; memcpy(&v, p, sizeof v); return v; }   『XMプレイヤー』では、この方法で解決しました。 3.最適化しない。   「-O2」オプションを指定しません。しかしこれは、現実的な方法ではありません。   S1C33用gccには「-mno-memcpy」というオプションがあります。   gcc33コマンドの使い方表示には「inline functions 'strcpy' and 'memcpy'」と書かれており、   このコンパイルオプションを使ってmemcpy()の最適化だけを制御できそうに見えます。   が、実際にはこのオプションは、memcpy()の最適化には関係ないようです。     gcc33 -S -O sample.c     gcc33 -S -O -mmemcpy sample.c     gcc33 -S -O -mno-memcpy sample.c   いずれの方法でコンパイルしてもmemcpy()は最適化されてしまい、「アドレス不整例外」が発生します。   そもそもpcc33経由では、gcc33にこのオプションを渡せません。   それに、一部のmemcpy()最適化による問題を回避するためだけに、全部の最適化を抑制してしまうというのはやりすぎです。 =============================================================================================================================== さて、コンパイラにケチを付けるのも身の程知らずですが、しかしこれはちょっと納得いかないような気もします。 memcpy()を最適化してくれるのはありがたいのですけれど、memcpy()のプロトタイプはもともと次のようなものです。     void* memcpy(void* dst, const void* src, size_t len); memcpy()自身は、受け取ったポインタの正確な型を知っていてはいけないはずです。 int*型ポインタだから最適化するとか、char*型ポインタだから最適化しない、というやり方は不可だと思うのですが… C言語仕様的にはどうなのでしょうか? もしかしたら、int*型ポインタに「不整アドレス」を代入した時点で既にC言語仕様違反なのでしょうか? 要調査です。