10
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

avr-gcc のコード生成で switch ~ case の最適化によっては RAM を消費することがある

豊四季タイニー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"

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
10
Help us understand the problem. What are the problem?