豊四季タイニーBASIC Arduino版のフリーエリアを増やす実験(4)で追加した関数 inumsize() について、当初は
unsigned char inumsize(unsigned char *lp) {
unsigned char i_num = *lp;
switch (i_num) {
case I_NUM_0 ... I_NUM_9:
return 1;
case I_NUM_B:
return 2;
case I_NUM_W:
return 3;
default:
return 0;
}
}
と書いていたのですが、これでビルドするとビルド時のメッセージが
最大2,048バイトのRAMのうち、グローバル変数が749バイト(36%)を使っていて、ローカル変数で1,299バイト使うことができます。
となりました。inumsize() を追加する以前では使用可能なローカル変数は 1311バイトだったので、12バイト減ったことになります。
この関数の他は大域変数等は増えておらず、RAM の消費の原因を探るためにビルド時に生成された .elf ファイルを逆アセンブルしてみたところ、
00000908 <_Z8inumsizePh>:
908: dc 01 movw r26, r24
90a: ec 91 ld r30, X
90c: e2 52 subi r30, 0x22 ; 34
90e: ec 30 cpi r30, 0x0C ; 12
910: 28 f4 brcc .+10 ; 0x91c <_Z8inumsizePh+0x14>
912: f0 e0 ldi r31, 0x00 ; 0
914: eb 5f subi r30, 0xFB ; 251
916: fe 4f sbci r31, 0xFE ; 254
918: 80 81 ld r24, Z
91a: 08 95 ret
91c: 80 e0 ldi r24, 0x00 ; 0
91e: 08 95 ret
00800105 <CSWTCH.240>:
800105: 01 01 movw r0, r2
800107: 01 01 movw r0, r2
800109: 01 01 movw r0, r2
80010b: 01 01 movw r0, r2
80010d: 01 01 movw r0, r2
80010f: 02 03 mulsu r16, r18
が原因とわかりました。
見易い様、逆アセンブルでなく、コンパイラの出力するアセンブルリストを示すと以下の通りとなります。
.global _Z8inumsizePh
.type _Z8inumsizePh, @function
_Z8inumsizePh:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
movw r26,r24
ld r30,X
subi r30,lo8(-(-34))
cpi r30,lo8(12)
brsh .L128
ldi r31,0
subi r30,lo8(-(CSWTCH.240))
sbci r31,hi8(-(CSWTCH.240))
ld r24,Z
ret
.L128:
ldi r24,0
ret
.size _Z8inumsizePh, .-_Z8inumsizePh
/* 略 */
.section .rodata
.type CSWTCH.240, @object
.size CSWTCH.240, 12
CSWTCH.240:
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 2
.byte 3
switch ~ case による条件分岐が最適化によりテーブル参照に変換されており、参照されるテーブルが .rodata に配置されており、ArduinoIDE が使用している avr-gcc では .rodata は RAM に配置するため、ビルド時のローカル変数の空きエリアが減る直接の原因となっています。
同等のコードを出力する C ソースの例としては以下の通りです。
unsigned char inumsize(unsigned char *lp) {
unsigned char i_num = *lp - I_NUM_0;
if (i_num <= I_NUM_W - I_NUM_0) {
static const unsigned char t[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3};
return t[i_num];
} else {
return 0;
}
}
これをビルドをビルドすると先の例とほぼ同等のコードが生成されます。コンパイラの出力は以下の通りとなります。
.global _Z8inumsizePh
.type _Z8inumsizePh, @function
_Z8inumsizePh:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
movw r26,r24
ld r30,X
subi r30,lo8(-(-34))
cpi r30,lo8(12)
brsh .L128
ldi r31,0
subi r30,lo8(-(_ZZ8inumsizePhE1t))
sbci r31,hi8(-(_ZZ8inumsizePhE1t))
ld r24,Z
ret
.L128:
ldi r24,0
ret
/* 略 */
.section .rodata
.type _ZZ8inumsizePhE1t, @object
.size _ZZ8inumsizePhE1t, 12
_ZZ8inumsizePhE1t:
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 2
.byte 3
もし、参照されるテーブルが PROGMEM に配置され、
unsigned char inumsize(unsigned char *lp) {
unsigned char i_num = *lp - I_NUM_0;
if (i_num <= I_NUM_W - I_NUM_0) {
static const unsigned char PROGMEM t[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3};
return pgm_read_byte_near(&t[i_num]);
} else {
return 0;
}
}
.global _Z8inumsizePh
.type _Z8inumsizePh, @function
_Z8inumsizePh:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
movw r26,r24
ld r30,X
subi r30,lo8(-(-34))
cpi r30,lo8(12)
brsh .L128
ldi r31,0
subi r30,lo8(-(_ZZ8inumsizePhE1t))
sbci r31,hi8(-(_ZZ8inumsizePhE1t))
/* #APP */
; 592 "basic.cpp" 1
lpm r24, Z
; 0 "" 2
/* #NOAPP */
ret
.L128:
ldi r24,0
ret
.size _Z8inumsizePh, .-_Z8inumsizePh
.type _ZZ8inumsizePhE1t, @object
.size _ZZ8inumsizePhE1t, 12
_ZZ8inumsizePhE1t:
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 1
.byte 2
.byte 3
以上のようなコードが生成されれば RAM の消費はなかったのですが、ArduinoIDE 1.6.8 が採用している avr-gcc 4.8.1 は
PROGMEM 領域を読み出す lpm 命令より RAM を読み出す ld 命令の方が実行に必要なサイクル数が 1 少ないことを優先してか switch ~ case をテーブル参照と最適化する場合にはテーブルを RAM に置くようです。
以上を踏まえて、『豊四季タイニーBASIC Arduino版のフリーエリアを増やす実験(4)』では、switch ~ case をテーブル参照とされる最適化を避けるために inumsize() を以下の内容としました。
unsigned char inumsize(unsigned char *lp) {
unsigned char i_num = *lp;
if (i_num >= I_NUM_0 && i_num <= I_NUM_9) {
return 1;
} else if (i_num == I_NUM_B) {
return 2;
} else if (i_num == I_NUM_W) {
return 3;
} else {
return 0;
}
}
これは以下のコードにコンパイルされ、RAM の消費はありません。
.global _Z8inumsizePh
.type _Z8inumsizePh, @function
_Z8inumsizePh:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
movw r30,r24
ld r24,Z
ldi r25,lo8(-34)
add r25,r24
cpi r25,lo8(10)
brlo .L128
cpi r24,lo8(44)
breq .L129
cpi r24,lo8(45)
brne .L130
ldi r24,lo8(3)
ret
.L128:
ldi r24,lo8(1)
ret
.L129:
ldi r24,lo8(2)
ret
.L130:
ldi r24,0
ret
.size _Z8inumsizePh, .-_Z8inumsizePh
以上のコードはフラッシュメモリを 36バイト使用し、実行には引数 lp の指す内容が I_NUM_0 から I_NUM_9 の場合は 13クロック、I_NUM_B で 15クロック、I_NUM_W で 16クロック、それ以外で17クロックを消費します。
最初の例ではフラッシュメモリを 36バイト、RAM を 12バイト、実行には引数 lp の指す内容が I_NUM_0 から I_NUM_W の場合は 15クロック、それ以外で 12クロックを消費します。
フラシュメモリの消費量は同等であり、実行クロック数はさほど差がなく、RAM の消費がないことで以上のコードを採用しています。
Arduino Uno に搭載されている ATMEGA328P は搭載されている RAM の容量が 2kBと少なく、それの消費を避けるためには以上のようにコンパイラの出力コードを評価してコードの書き方を検討することも時として有効な手段となります。
尚、以上の内容とは違いますが、switch ~ case による条件分岐が最適化によりテーブル参照に変換されない場合、ジャンプテーブルが PROGMEM 上のジャンプ命令として生成されるのみで RAM の消費はないようです
unsigned char hoge(unsigned char x, unsigned char y)
{
switch (x) {
case 0:
return x + y;
case 1:
return x - y;
case 2:
return x * y;
case 3:
return x / y;
case 4:
return x % y;
case 5:
return x & y;
case 6:
return x | y;
case 7:
return x ^ y;
default:
return 0;
}
}
.file "hoge.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.text
.global hoge
.type hoge, @function
hoge:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r25,0
cpi r24,8
cpc r25,__zero_reg__
brsh .L2
mov r30,r24
mov r31,r25
subi r30,lo8(-(gs(.L4)))
sbci r31,hi8(-(gs(.L4)))
ijmp
.section .progmem.gcc_sw_table,"ax",@progbits
.p2align 1
.L4:
rjmp .L12
rjmp .L5
rjmp .L6
rjmp .L7
rjmp .L8
rjmp .L9
rjmp .L10
rjmp .L11
.text
.L2:
ldi r24,0
ret
.L5:
ldi r24,lo8(1)
sub r24,r22
ret
.L6:
mov r24,r22
lsl r24
ret
.L7:
ldi r24,lo8(3)
rcall __udivmodqi4
ret
.L8:
ldi r24,lo8(4)
rcall __udivmodqi4
mov r24,r25
ret
.L9:
mov r24,r22
andi r24,lo8(5)
ret
.L10:
mov r24,r22
ori r24,lo8(6)
ret
.L11:
ldi r24,lo8(7)
eor r24,r22
ret
.L12:
mov r24,r22
ret
.size hoge, .-hoge
.ident "GCC: (GNU) 4.8.1"