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 サイクル以上ないとストールすることが分かりました。