割込処理の高速化(案)
割込処理(exception.xの_int0〜_int3とintc.cの_get_interrupt_handler)を見てスピードの点で不満を持ち、もっと高速化できないものか考えてみた。実際に問題があって高速化するのではないが、「割込ルーチンは出来るだけ短く(早く)」が基本なので。
例としていつものAT32UC3B0256を取り上げるが、レジスタのアドレスが変わるくらいでAVR32なら共通しているはずだ。
まずは現状から。INTCの割込ハンドラ部分_int0。Releaseでビルドした結果を以下に示す。_int1〜_int3もあるが、マクロで展開しているだけなので省略。
00000104 <_int0>: // CPU upon interrupt entry. No other register is saved by hardware. #elif __AVR32_AP__ // PC and SR are automatically saved in respectively RAR_INTx and RSR_INTx by // the CPU upon interrupt entry. No other register is saved by hardware. pushm r8-r12, lr #endif 104: 30 0c mov r12,0 106: f0 1f 00 12 mcall 14c10a: 58 0c cp.w r12,0 10c: f8 0f 17 10 movne pc,r12 110: d6 03 rete
_int1〜_int3のコードは、オフセット104の第2オペランドがINTに応じて1〜3へ変化するだけで、それ以外は共通である。使用される命令はすべて1cycleで実行され、オフセット10cで目的の割込要因本体の割込ハンドラへジャンプするため、4サイクルを消費する。割込ハンドラのアドレスが0(未登録)のときオフセット110へ到達してreteする(登録されているとき、割込ハンドラ本体からreteする)。
次はオフセット106で呼び出している_get_interrupt_handlerのコードである(Releaseモードで最適化した場合)。
<<C言語ソース>> __int_handler _get_interrupt_handler(unsigned int int_lev) { unsigned int int_grp = AVR32_INTC.icr[AVR32_INTC_INT3 - int_lev]; unsigned int int_req = AVR32_INTC.irr[int_grp]; return (int_req) ? _int_handler_table[int_grp]._int_line_handler_table[32 - clz(int_req) - 1] : NULL; } <<オブジェクトファイルの逆アセンブル・リスト>> 00000000 <_get_interrupt_handler>: 0: e0 68 00 83 mov r8,131 4: fe 79 08 00 mov r9,-63488 8: f0 0c 01 0c sub r12,r8,r12 c: f2 0c 03 2a ld.w r10,r9[r12<<0x2] 10: f4 c8 ff c0 sub r8,r10,-64 14: f2 08 03 2c ld.w r12,r9[r8<<0x2] 18: 58 0c cp.w r12,0 1a: 5e 0c reteq r12 1c: f8 08 12 00 clz r8,r12 20: 48 09 lddpc r9,20 <_get_interrupt_handler+0x20> 20: R_AVR32_9W_CP .text._get_interrupt_handler+0x34 22: f0 08 11 1f rsub r8,r8,31 26: f2 0a 00 39 add r9,r9,r10<<0x3 2a: 72 1a ld.w r10,r9[0x4] 2c: f4 08 03 2c ld.w r12,r10[r8<<0x2] 30: 5e fc retal r12
割込要因が見つからなければオフセット1aで返るが、通常処理は割込ハンドラが呼ばれるオフセット30であるから15サイクル。
合わせて19cycle掛かっている。
まず_get_interrupt_handlerの先頭で
intc.c, 113: unsigned int int_grp = AVR32_INTC.icr[AVR32_INTC_INT3 - int_lev];
とやっているが、「AVR32_INTC_INT3 - int_lev」などと引き算するのが回りくどい。この行は該当レジスタICRnの値を取得したいだけだ。なら引数で割込優先度(int_lev)でなく、レジスタICRnのポインタあるいはレジスタ値を渡せばよい。そうすればオフセット0, オフセット8の2cycle、8byteが省ける。
intc.c, 162: return (int_req) ? _int_handler_table[int_grp]._int_line_handler_table[32 - clz(int_req) - 1] : NULL;
clz命令は、msb側からビットをチェックして、0がいくつ連続して存在するか調べる。int_irqの元になるIRRnがビットフラグだからこのようになるが、問題は値がインデックス番号と逆になるため、32-1(=31)から引く必要がある。この手間を省くためには_int_line_handler_table配列の並び順を逆にする方法で対処する。ただし、いずれの配列(_int_line_handler_table[]に代入されている_int_line_handler_table_??のこと)も長さが32も無いので、予め_int_handler_table[int_grp]のポインタをマイナス方向へずらしておく。これで問題ないはずだ(他にもINTC_init_interruptsとINTC_register_interruptの対応が必要)。これで省けるのはオフセット22の僅か1cycle, 4byteだ。
1つ手前で、clz命令を使ったインデックス・アクセスで「予め_int_handler_table[int_grp]のポインタをずらす」と書いたが、更にこの配列を1つ余計に確保してしまうという方法もある。
この方法の利点は、int_req==0の対処が要らなくなることだ。int_req==0のときclz(int_req)は32となり、1つ余計に確保した末尾要素へアクセスするので、ここに何もせずreteする無効ハンドラを登録しておけばよい。
オフセット18, 1a、更にはオフセット10aが不要になり、3cycle, 6byte省略できる。
他には_get_interrupt_handlerの関数コールを止めて、コード展開する方法がある(C言語のinline展開に相当)。割込レベル毎に完全展開してしまえば、mcallとretalの2cycle, 6byteを省略できる(当然展開した分、使用メモリは増える)。
以上、すべて合計して19cycle→11cycle、57.9%の低減(高速化)である。
ICRnレジスタのアドレスかオフセットが必要なため、特定の製品で修正するのは難しくないが、汎用的に修正可能とするとちょっと厄介だ。何かツールを作るか、あるいはAtmelが対処してくれるまで待つか...対処する訳無いか。
しかし、高速化しても割込本体処理開始までに11cycleは掛かり過ぎ。
根本的にソフトウェアで割込要因を調べて該当ハンドラへ飛ぶだけなのだが、割込要因と無関係な割込レベルのレジスタを読み必要があること(多重割込サポート上仕方がない?)、その上割込グループと割込ラインの2段階で割込要因を特定するという2つの手間が掛かる点が問題だ。
割込グループ番号を、それより小さい番号の割込グループの割込ライン総計にすれば、割込ハンドラ・テーブルを1次元配列にでき、要素番号のアクセスが割込グループ番号+割込ラインで計算できて少し早くなるのだがどうだろう。