新版FM音源プログラム (19)
今回からは実行サイクル数の検討に入ります。
まずは、ARMv6-M アーキテクチャの Cortex-M0 である NXP LPC1114、Cypress PSoC4 から始めます。
アセンブリ言語で書いたスロット (オペレータ) 計算プログラムをコンパイルして得られたオブジェクトを逆アセンブルしたものを下に示します。
(2018 年 3 月 3 日追記: 変更後のブログラムの逆アセンブル・リストに差し替えました)
00000000 <calc_slot>: 0: b530 push {r4, r5, lr} 2: 6802 ldr r2, [r0, #0] 4: 6843 ldr r3, [r0, #4] 6: 6884 ldr r4, [r0, #8] 8: 68c5 ldr r5, [r0, #12] a: 18d2 adds r2, r2, r3 c: 8a03 ldrh r3, [r0, #16] e: 6042 str r2, [r0, #4] 10: 18a4 adds r4, r4, r2 12: 0a24 lsrs r4, r4, #8 14: 4023 ands r3, r4 16: 6942 ldr r2, [r0, #20] 18: 5eeb ldrsh r3, [r5, r3] 1a: 8a45 ldrh r5, [r0, #18] 1c: b22d sxth r5, r5 1e: 435a muls r2, r3 20: 13d4 asrs r4, r2, #15 22: 8b03 ldrh r3, [r0, #24] 24: 8244 strh r4, [r0, #18] 26: 192d adds r5, r5, r4 28: 436b muls r3, r5 2a: 6083 str r3, [r0, #8] 2c: 69c2 ldr r2, [r0, #28] 2e: 6a03 ldr r3, [r0, #32] 30: 8e85 ldrh r5, [r0, #52] ; 0x34 32: 18d2 adds r2, r2, r3 34: 6202 str r2, [r0, #32] 36: 436c muls r4, r5 38: 18a4 adds r4, r4, r2 3a: 8d83 ldrh r3, [r0, #44] ; 0x2c 3c: 6a85 ldr r5, [r0, #40] ; 0x28 3e: 0a24 lsrs r4, r4, #8 40: 4023 ands r3, r4 42: 6b02 ldr r2, [r0, #48] ; 0x30 44: 5eeb ldrsh r3, [r5, r3] 46: 435a muls r2, r3 48: 13d2 asrs r2, r2, #15 4a: 85c2 strh r2, [r0, #46] ; 0x2e 4c: 3038 adds r0, #56 ; 0x38 4e: 3902 subs r1, #2 50: dcd7 bgt.n 2 <calc_slot+0x2> 52: bd30 pop {r4, r5, pc}
これは PSoC4 用に gcc を使ってコンパイルした結果です。 当然ですが、16 ビット Thumb 命令のみを使用しています。
ソースはアセンブリ言語で書いてあるので、処理系によらずコンパイル (アセンブル) 結果は同じになるはずですが、ARM/Keil の armcc を使った場合とでは、「subs r1, #2」命令だけが異なります。
LPC1114 用に armcc でコンパイルした結果では下のようになります。
1be: 1e89 subs r1, r1, #2
これは、16 ビット Thumb 命令の範囲でも、イミディエイト定数値のビット幅によって 2 つのエンコーディングが存在しているためです。
デスティネーション・レジスタとソース・レジスタが同一で、イミディエイト定数が 8 ビット幅のもの
SUBS <Rdn>, #<imm8>
と、デスティネーションとソースに別のレジスタを指定できるけれど、イミディエイト定数が 3 ビット幅に限られるもの
SUBS <Rd>, <Rn>, #<imm3>
とがあります。
「subs r1, r1, #2」と書いた場合に、「デスティネーションとソースが同じ」、「イミディエイト定数値が 3 ビット以下」という両方の条件が当てはまるため、gcc と armcc とでは解釈が分かれるようです。
スロット計算プログラムのループ 1 回あたりで、
- ブランチ命令 (3 サイクル) — 1 個
- ロード/ストア命令 (2 サイクル) — 21 個
- その他の命令 (1 サイクル) — 18 個
の命令があるので、単純にサイクル数を合計すると、
3 + (2 × 21) + 18 = 63
となります。 (2018 年 3 月 3 日追記: 条件ブランチ命令の実行サイクル数を「2」と誤っていたのを「3」に訂正しました)
Cortex-M0 ではロード/ストア命令を連続させることによるサイクル数の削減効果はないので、フラッシュ・レイテンシがゼロの場合には、この計算上の「63」サイクルという値に近くなることが期待されます。
LPC1114 と PSoC4 の場合の結果を再掲します。
(2018 年 3 月 3 日追記: 変更後のプログラムでの測定結果に差し替えました)
μVision V5.21.1.0
(MDK-Lite V5.21a, armcc.exe v5.06 update 3)
cycle per 2slot |
評価ボード名 | 搭載チップ名 | アーキテクチャ | clock [MHz] |
flash latency |
---|---|---|---|---|---|
64 | (チップ単体) | LPC1114 | ARMv6-M Cortex-M0 |
20 | 0 |
84 | (チップ単体) | LPC1114 | ARMv6-M Cortex-M0 |
48 | 2 |
PSoC Creator 4.0 Update 1 (gcc 4.9.3)
Inline Assembly Language で記述
cycle per 2slot |
評価ボード名 | 搭載チップ名 | アーキテクチャ | clock [MHz] |
flash latency |
---|---|---|---|---|---|
64 | PSoC 4200 Prototyping Kit |
CY8C4245 | ARMv6-M Cortex-M0 |
24 | 0 |
67 | PSoC 4200 Prototyping Kit |
CY8C4245 | ARMv6-M Cortex-M0 |
48 | 1 |
フラッシュ・レイテンシがゼロの場合のサイクル数は LPC1114、PSoC4 ともに「64」となっています。 単純な計算値よりも 1 サイクル多くなっていますが、ほぼ同じと言えます。
LPC1114 の場合、フラッシュ・レイテンシが「2」でサイクル数が「86」と大きく悪化しています。
これには LPC1114 フラッシュ・コントローラ機構が大きく関わっています。
CPU 側からの命令フェッチ要求に応じて、フラッシュ側では、そのアドレスを含む 128 ビット幅 (16 バイト幅) のコードをバッファに読み出します。
続く命令フェッチ要求に対して、そのアドレスに対するコードがフラッシュ・バッファ上にすでに存在している場合には、新たにフラッシュを読み出しすことなく、そのバッファ内のコードを読み出します。
バッファ上にないアドレスのコードのフェッチが要求された場合には、そのアドレスを含む 128 ビット幅で新たにフラッシュから読み出し、バッファの内容を置き換えます。
これは「プリフェッチ」ではなく、フェッチ要求が生じてからフラッシュを読み込むので、レイテンシが隠蔽されることなく、レイテンシがもろに表面に現れます。
2012 年 12 月 10 日の LPC1114 を使った SD カード LPCM プレイヤーの記事 (→こちら) でこのレイテンシ (ウェイト・サイクル) の影響をとらえた波形写真を示しています。
この記事では、コアクロックを 36 MHz に選んでいるので、レイテンシは 1 で、8 命令 (16 バイト) 実行するごとにレイテンシにより GPIO を使ったソフト SPI のクロック波形が 1 クロック分延びています。
コアクロック周波数が 48 MHz の場合にはレイテンシは 2 にする必要があります。
armcc ではアセンブラ記述で「align 16」として、gcc では関数のアトリビュートとして 16 バイト・アラインを指定しているので、calc_slot() 関数は 16 バイト境界から始まります。
関数の先頭アドレスから、オフセット 0x02 〜 0x51 の範囲がループ内部となっており、その中には 6 個の 16 バイト・ブロックが含まれています。
つまり、48 MHz クロックでオフセット 0x02 〜 0x51 の範囲を 1 回実行すると、フラッシュから 6 回読み出され、ウェイト・サイクルとしては 6 × 2 = 12 サイクルが消費されることになります。
さらには、フラッシュ・メモリ上のサイン波テーブルの読み出しを 2 スロット分で 2 回実行しているので、その分の影響があります。
LPC1114 の資料には明確に書いていないので、はっきりしないのですが、フラッシュ・バッファの振る舞いがフラッシュ・メモリ上の命令 (I-Code) だけではなく、フラッシュ・メモリ上のデータ (D-Code) にも適用されると仮定します。
そうすると、フラッシュ中のサイン波テーブル・データをアクセスするために生じるウェイト・サイクル 2 × 2 = 4 サイクルと、テーブル・データを読み出したために破棄された命令コードの再読み込みのためのウェイトサイクル 2 × 2 = 4 サイクルとが加算され、合計すると 8 サイクル余分にウェイト・サイクルが消費されます。
命令フェッチの分と合算すると、12 + 8 = 20 サイクルとなり、ウェイトサイクルなしの「64」に加算すると 84 となり、実測値と一致します。
PSoC 4200 のデータシートには、「Flash Accelerator」を内蔵し、フラッシュはコアクロック 24 MHz まではウェイト・サイクル 0、コアクロック周波数 48 MHz まではウェイト・サイクル 1 で動作すると記述されていますが、それ以上の詳しい説明はありません。
PSoC 4200 の実測値では、0 ウェイトで 64 クロック、1 ウェイトで 67 クロックなので、命令コード・フェッチについては「キャッシュ」が効いて、ウェイト・タイムがほぼ隠蔽されていると思われ、サイン波テーブル・アクセス 2 回分の 2 ウェイト・サイクル程度が上乗せされているだけのように見えます。