ESP32 の命令実行サイクル数 (3)
Xtensa LX プロセッサの MAC16 拡張 (16 ビット整数乗算オプション含む) では、「ハードウェア」的には 16 ビット × 16 ビット = 32 ビットの乗算器と 40 ビット・アキュムレータ、および 4 つの 32 ビット・レジスタ (MAC16 レジスタ) が追加されます。 そして、関連する命令が 72 個追加されます。
アキュムレータおよび MAC16 レジスタ (アセンブラ・ニモニックでは m0, m1, m2, m3 と表記される) は「スペシャル・レジスタ」として定義され、汎用レジスタを介してアクセスできます。
積和演算命令には、次のようなバリエーションがあります。
MULA.AA.** as, at MULA.AD.** as, my MULA.DA.** mx, at MULA.DD.** mx, my
命令のステム部分の「MULA」は、(signed) MULtiply and Add accumulator を意味しています。
最初のサフィックスの .AA / .AD / .DA / .DD は、「A」がアドレス・レジスタ (汎用レジスタ)、「D」が MAC16 レジスタ (m0 〜 m3) を表していて、最初の文字が第一オペランド、2 番目の文字が第二オペランドを表しています。
乗算は可換演算なので、.DA サフィックスと .AD サフィックスとで重複することになりますが、実際には、第一オペランドとして指定できるのは m0, m1 のみに限られ、第二オペランドとしては m2, m3 のみに限られるので、.DA / .AD サフィックスの両者を合わせて初めて m0 〜 m3 までのすべての MAC16 レジスタをオペランドとして指定することができます。
「.**」として表記してある 2 番目のサフィックス部分は、 .LL / .LH / .HL / .HH の 4 つのうちのいずれかで、「L」は 32 ビット・レジスタの下位ハーフワード、「H」は 32 ビット・レジスタの上位ハーフワードを示しています。
そして、最初の文字が第一オペランド、2 番目の文字が第二オペランドを示しています。
32 ビット・レジスタにパックされている 2 つの 16 ビット・ハーフワードのうち、どちら側を使うのかを、32 ビット・レジスタに置いたままパッキングを解かずに指定することができます。
40 ビット・アキュムレータと MAC16 レジスタとは「スペシャル・レジスタ」として実装されており、通常はアドレス・レジスタを仲介としてアクセスします。
そういうわけで、直接アクセス可能なアドレス・レジスタのみをオペランドとして持つ MULA.AA.** 系統の命令を Cortex-M シリーズの CMSIS 風に gcc 用に定義したものを下に示します。
#include <stdint.h> #if defined ( __GNUC__ ) #define __ASM __asm /*!< asm keyword for GNU Compiler */ #define __INLINE inline /*!< inline keyword for GNU Compiler */ #define __STATIC_INLINE static inline #endif __attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __RSR(uint32_t op1) { uint32_t result; __ASM volatile ("rsr %0, %1" : "=a" (result) : "I" (op1)); return(result); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __WSR(uint32_t op1, uint32_t op2) { __ASM volatile ("wsr %0, %1" : : "a" (op1), "I" (op2)); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __NOP( void ) { __ASM volatile ("nop" : : ); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __MULA_AA_LL(uint32_t op1, uint32_t op2) { __ASM volatile ("mula.aa.ll %0, %1" : : "a" (op1), "a" (op2)); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __MULA_AA_LH(uint32_t op1, uint32_t op2) { __ASM volatile ("mula.aa.lh %0, %1" : : "a" (op1), "a" (op2)); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __MULA_AA_HL(uint32_t op1, uint32_t op2) { __ASM volatile ("mula.aa.hl %0, %1" : : "a" (op1), "a" (op2)); } __attribute__( ( always_inline ) ) __STATIC_INLINE void __MULA_AA_HH(uint32_t op1, uint32_t op2) { __ASM volatile ("mula.aa.hh %0, %1" : : "a" (op1), "a" (op2)); }
以前に触れたように、ループ内にインライン・アセンブラ記述を含むと、そのループはハードウェア・ループにはならないようです。
for (i = 0; i < n_iter; i++) { __NOP(); } // for
6c: f03d nop.n 6e: 880b addi.n a8, a8, -1 70: ff8856 bnez a8, 6c
となり、実行結果は「cycle = 6.00281」となるので、nop 命令の実行サイクル数「1」を引くと、ループのオーバーヘッドは 5 サイクルということになります。
アドレス・レジスタを永続的なアキュムレータとして使用し、積和演算を行う場面だけで 40 ビット・アキュムレータへの値の出し入れを行う方針のプログラム例を下に示します。
register int32_t acc_lo = 0; for (i = 0; i < n_iter; i++) { __WSR(acc_lo, ACCLO); __MULA_AA_LL(a0, a1); __MULA_AA_LH(a0, a1); acc_lo = __RSR(ACCLO); } // for
レジスタ変数「acc_lo」が永続的なアキュムレータとして使うアドレスレジスタです。
「ACCLO」は 40 ビット・アキュムレータの下位 32 ビットのスペシャル・レジスタ番号 (32) を定義しているシンボルです。
「WSR」(Write to System Register) 命令でアドレス・レジスタからスペシャル・レジスタへ値を書き込みます。
MULA.AA.LL / MULA.AA.LH 命令で積和演算を実行した後、「RSR」(Read from Special Register) 命令でアキュムレータ下位 32 ビットから値をアドレス・レジスタに読み込みます。
積和演算結果が 32 ビット以内におさまり、上位 8 ビットの結果が必要ない場合には、40 ビット・アキュムレータの上位 8 ビットの値については触れる必要はありません。
上のプログラムのコンパイル結果の逆アセンブル・リストを下に示します。
71: 131090 wsr.acclo a9 74: 780344 mula.aa.ll a3, a4 77: 7a0344 mula.aa.lh a3, a4 7a: 031090 rsr.acclo a9 7d: ffc882 addi a8, a8, -1 80: fed856 bnez a8, 71
実行結果は「cycle = 11.005」で、ループ・オーバーヘッドの「5」を差し引くとループ 1 回あたり 6 サイクルかかっています。 ループ内の命令数は「4」なので、パイプライン・ストールが生じていると思われます。
NOP を挿入して、どこでストールしているかを調べると、
for (i = 0; i < n_iter; i++) { __WSR(acc_lo, ACCLO); __NOP(); __NOP(); __MULA_AA_LL(a0, a1); __MULA_AA_LH(a0, a1); acc_lo = __RSR(ACCLO); } // for
という記述でもループ内サイクル数は「6」となり、WSR 命令によるアキュムレータ値の更新と、積和演算命令との間が 2 サイクル以上ないとストールすることが分かりました。