FM音源プログラム (21) -- オペレータ (14)

オペレータについては、ほぼ説明しつくしたと思いますので、最後にプログラムの最適化について検討してみます。
オペレータは、フェーズジェネレータ+EG+サイン波テーブルがセットになった、モジュール性の高い要素ですから、C++ などのオブジェクト指向言語で書くのに適しています。
しかし、FM音源プログラムを多様なマイコン上で実現したいということから、C++ ではなく、C 言語で書いてあります。
実際に、ADuC7026 版、V850 版で使っているコードを下に示します。

#define SIGNBIT 0x1000
#define OSIGN   (SIGNBIT<<1)

int16_t op_ws0(op_prop_t *o_p) 
{
  uint32_t phh;
  int   s0;
  phh = ((o_p->mod_in + (o_p->phacc)) >> 16); 
  o_p->phacc += o_p->ph_inc; // update phase
  s0  = stab[phh & (OSIGN-1)]; 
  if (phh & OSIGN) s0 = -s0;   
  o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15);
  return( o_p->opout );
} // int16_t op_ws0()

WS=0 つまり、普通のサイン波出力のオペレータ関数です。
「op_prop_t」は、まだ説明していませんが、「operator property type」の意味で、フェーズアキュムレータや音色パラメータなどのオペレータ関連の変数を集めた構造体の型名です。
オペレータ関数は、引数に渡された op_prop_t 構造体へのポインタを使って、各変数にアクセスします。
EG は、別の 1ms ごとに起動されるプログラムで処理しています。
サンプリング周期で計算されるオペレータ関数は、処理時間を短くすることが重要です。
2オペレータ構成では、1サンプリング周期内に同時発音数に2をかけた回数だけオペレータ関数が呼ばれます。 たとえば、同時発音数 6 なら、12 回オペレータ関数が呼ばれますから、オペレータ関数で1サイクル短縮できれば、1サンプリング周期の合計では 12 サイクルの短縮になります。
V850 版のコンパイル結果のアセンブリ・コードを下に示します。

_op_ws0:
#  41:   uint32_t phh;
#  42:   int   s0;
#  43:   phh = ((o_p->mod_in + (o_p->phacc)) >> 16); // high word of phase acc

	ld.w	[r6], r12
	ld.w	8[r6], r18
	add	r12, r18
	shr	16, r18

#  44:   o_p->phacc += o_p->ph_inc; // update phase

	ld.w	4[r6], r15
	add	r12, r15
	st.w	r15, [r6]

#  45:   s0  = stab[phh & (OSIGN-1)]; // look up sine table 

	andi	0x1fff, r18, r16
	shl	1, r16
	mov	#_stab, r11
	add	r16, r11
	ld.h	[r11], r11

#  46:   if (phh & OSIGN) s0 = -s0;   // if b29 == 1, change sign 

	and	0x2000, r18
	je	.L222
	not	r11, r11
	add	1, r11
.L222:

#  47:   o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15);

	ld.w	28[r6], r10
	sar	16, r10
	mul	r11, r10, r0
	shr	15, r10
	st.h	r10, 58[r6]

#  48:   return( o_p->opout );

	sxh	r10
	jmp	[lp]	--1

#  49: } // int16_t op_ws0()

このコンパイル結果は、ほぼ期待通りです。
ADuD7026 版のコンパイル結果を下に示します。(ARM7TDMI の THUMB モード)

 0008A4E0          op_ws0?T:  ; FUNCTION START
   39: int16_t op_ws0(op_prop_t *o_p) 
 0008A4E0  B410      PUSH        {R4}
 0008A4E2  1C01      MOV         R1,R0 ; o_p
 0008A4E4  ---- Variable 'o_p' assigned to Register 'R1' ----
   40: {
 0008A4E4            ; SCOPE-START
   43:   phh = ((o_p->mod_in + (o_p->phacc)) >> 16); // high word of phase acc
 0008A4E4  1C08      MOV         R0,R1 ; o_p
 0008A4E6  6884      LDR         R4,[R0,#0x8]
 0008A4E8  1C08      MOV         R0,R1 ; o_p
 0008A4EA  6802      LDR         R2,[R0,#0x0] ; o_p
 0008A4EC  18A4      ADD         R4,R2
 0008A4EE  0C24      LSR         R4,R4,#0x10
 0008A4F0  ---- Variable 'phh' assigned to Register 'R4' ----
   44:   o_p->phacc += o_p->ph_inc; // update phase
 0008A4F0  1C08      MOV         R0,R1 ; o_p
 0008A4F2  6840      LDR         R0,[R0,#0x4]
 0008A4F4  1812      ADD         R2,R0
 0008A4F6  1C08      MOV         R0,R1 ; o_p
 0008A4F8  6002      STR         R2,[R0,#0x0] ; o_p
   45:   s0  = stab[phh & (OSIGN-1)]; // look up sine table 
 0008A4FA  1C22      MOV         R2,R4 ; phh
 0008A4FC  480E      LDR         R0,[R15,#56] ; PoolRef @0x8A538
 0008A4FE  4002      AND         R2,R0
 0008A500  0052      LSL         R2,R2,#0x1
 0008A502  480E      LDR         R0,[R15,#56] ; PoolRef @0x8A53C ; stab
 0008A504  5A80      LDRH        R0,[R0,R2]
 0008A506  0400      LSL         R0,R0,#0x10
 0008A508  1400      ASR         R0,R0,#0x10
 0008A50A  1C03      MOV         R3,R0
 0008A50C  ---- Variable 's0' assigned to Register 'R3' ----
   46:   if (phh & OSIGN) s0 = -s0;   // if b29 == 1, change sign 
 0008A50C  1C20      MOV         R0,R4 ; phh
 0008A50E  4A0C      LDR         R2,[R15,#48] ; PoolRef @0x8A540
 0008A510  4210      TST         R0,R2 ; phh
 0008A512  D000      BEQ         L_1  ; T=0x0008A516
 0008A514  425B      NEG         R3,R3 ; s0
 0008A516          L_1:
   47:   o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15);
 0008A516  1C08      MOV         R0,R1 ; o_p
 0008A518  69C0      LDR         R0,[R0,#0x1C]
 0008A51A  1400      ASR         R0,R0,#0x10
 0008A51C  1C1A      MOV         R2,R3 ; s0
 0008A51E  4342      MUL         R2,R0
 0008A520  13D2      ASR         R2,R2,#0xF
 0008A522  0412      LSL         R2,R2,#0x10
 0008A524  0C12      LSR         R2,R2,#0x10
 0008A526  1C08      MOV         R0,R1 ; o_p
 0008A528  8742      STRH        R2,[R0,#0x3A]
   48:   return( o_p->opout );
 0008A52A  1C08      MOV         R0,R1 ; o_p
 0008A52C  8F40      LDRH        R0,[R0,#0x3A]
 0008A52E  0400      LSL         R0,R0,#0x10
 0008A530  1400      ASR         R0,R0,#0x10
 0008A532            ; SCOPE-END
   49: } // int16_t op_ws0()
 0008A532  BC10      POP         {R4}
 0008A534  4770      BX          R14
 0008A536            ; END 'op_ws0?T'

一応、最適化レベルは最高の 8 に設定しているのですが、これは、ちょっと残念なコードです。 
特に、C ソースの 47 行目に対するコードで、

 0008A522  0412      LSL         R2,R2,#0x10
 0008A524  0C12      LSR         R2,R2,#0x10

によってゼロ拡張しているのは、続く「STRH」命令で下位 16 ビット分しかストアしていないので、全く無駄です。
また、「o_p->opout」にストアした結果が R2 に残っているのに、

 0008A52A  1C08      MOV         R0,R1 ; o_p
 0008A52C  8F40      LDRH        R0,[R0,#0x3A]

によって、再びロードしているのは、これまた無駄です。
ATmega 版では、C で書いた場合には、効率的には受け入れられないものだったので、オペレータ関数はアセンブラで書きました。
ATmega の場合は、スピード、RAM 容量、ROM 容量、いずれも制約となりますから、当初は 9 ビット PWM 出力だったこともあり、サイン波テーブルは 1/4 周期タイプの 129 エントリ、8 ビットデータのものを使っています。
データ幅が小さいので、現在のリニア値の代わりに、lb 値、つまり対数値を使うタイプにすることを検討しています。