いきなり結論
AVR 向けのコンパイラ avr-gcc において,bool 型は uint8_t 型 (unsigned char 型) として扱われているようです。
知りたかったこと
AVR のためのライブラリを作っている中で,uint8_t 型変数の任意のビットを置換する,以下のような関数を書きました。 置換対象の変数 src の offset で指定されたビットが bit で指定した状態に置換されて return されます。
uint8_t ReplaceSingleBit(uint8_t src, int8_t offset, bool bit) {
assert((0 <= offset) && (offset <= 7));
return (src & ~(1 << offset)) | (bit << offset);
}
さてこの関数,とりわけ bool 型の変数 bit はどのようにコンパイルされるでしょうか? もしかして,int8_t とか uint8_t などの整数型を使ったほうが効率よいコードが吐き出されたりする,なーんてことはあるでしょうか?
そんな疑問が湧いてしまったので,実際どうなるのか調べてみました。
実験
実験に使ったソースコード
以下のようなソースコードを用意しました。
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
uint8_t TestA(uint8_t src, int8_t offset, bool bit) {
return (src & ~(1 << offset)) | (bit << offset);
}
uint8_t TestB(uint8_t src, int8_t offset, uint8_t bit) {
return (src & ~(1 << offset)) | (bit << offset);
}
uint8_t TestC(uint8_t src, int8_t offset, int8_t bit) {
return (src & ~(1 << offset)) | (bit << offset);
}
uint8_t TestD(uint8_t src, int8_t offset, uint16_t bit) {
return (src & ~(1 << offset)) | (bit << offset);
}
uint8_t TestE(uint8_t src, int8_t offset, int16_t bit) {
return (src & ~(1 << offset)) | (bit << offset);
}
TestA は,オリジナルの ReplaceSingleBit 関数と同様の構成です。 その他の関数は引数 bit の型をそれぞれ uint8_t,int8_t,uint16_t,int16_t に変えたものです。 いずれも assert は省略しました。
で,このコードを,以下のように最適化付きでコンパイルしました。
avr-gcc -S -O2 test.c
得られたアセンブリコード
結果出力されたのが,以下のアセンブリコードです。
.file "test.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.text
.global TestA
.type TestA, @function
TestA:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r18,lo8(1)
ldi r19,0
mov r0,r22
rjmp 2f
1:
lsl r18
2:
dec r0
brpl 1b
com r18
and r18,r24
mov r24,r20
mov r0,r22
rjmp 2f
1:
lsl r24
2:
dec r0
brpl 1b
or r24,r18
ret
.size TestA, .-TestA
.global TestB
.type TestB, @function
TestB:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r18,lo8(1)
ldi r19,0
mov r0,r22
rjmp 2f
1:
lsl r18
2:
dec r0
brpl 1b
com r18
and r18,r24
mov r24,r20
mov r0,r22
rjmp 2f
1:
lsl r24
2:
dec r0
brpl 1b
or r24,r18
ret
.size TestB, .-TestB
.global TestC
.type TestC, @function
TestC:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r18,lo8(1)
ldi r19,0
mov r0,r22
rjmp 2f
1:
lsl r18
2:
dec r0
brpl 1b
com r18
and r18,r24
mov r24,r20
clr r25
sbrc r24,7
com r25
mov r0,r22
rjmp 2f
1:
lsl r24
2:
dec r0
brpl 1b
or r24,r18
ret
.size TestC, .-TestC
.global TestD
.type TestD, @function
TestD:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r18,lo8(1)
ldi r19,0
mov r0,r22
rjmp 2f
1:
lsl r18
2:
dec r0
brpl 1b
com r18
and r18,r24
mov r0,r22
rjmp 2f
1:
lsl r20
2:
dec r0
brpl 1b
mov r24,r18
or r24,r20
ret
.size TestD, .-TestD
.global TestE
.type TestE, @function
TestE:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
ldi r18,lo8(1)
ldi r19,0
mov r0,r22
rjmp 2f
1:
lsl r18
2:
dec r0
brpl 1b
com r18
and r18,r24
mov r0,r22
rjmp 2f
1:
lsl r20
2:
dec r0
brpl 1b
mov r24,r18
or r24,r20
ret
.size TestE, .-TestE
.ident "GCC: (AVR_8_bit_GNU_Toolchain_3.4.5_1522) 4.8.1"
アセンブリコードの比較
それぞれのアセンブリコードを比べてみます。
bool | uint8_t | int8_t | uint16_t | int16_t |
---|---|---|---|---|
TestA: | TestB: | TestC: | TestD: | TestE: |
.L__stack_usage = 0 | .L__stack_usage = 0 | .L__stack_usage = 0 | .L__stack_usage = 0 | .L__stack_usage = 0 |
ldi r18,lo8(1) | ldi r18,lo8(1) | ldi r18,lo8(1) | ldi r18,lo8(1) | ldi r18,lo8(1) |
ldi r19,0 | ldi r19,0 | ldi r19,0 | ldi r19,0 | ldi r19,0 |
mov r0,r22 | mov r0,r22 | mov r0,r22 | mov r0,r22 | mov r0,r22 |
rjmp 2f | rjmp 2f | rjmp 2f | rjmp 2f | rjmp 2f |
1: | 1: | 1: | 1: | 1: |
lsl r18 | lsl r18 | lsl r18 | lsl r18 | lsl r18 |
2: | 2: | 2: | 2: | 2: |
dec r0 | dec r0 | dec r0 | dec r0 | dec r0 |
brpl 1b | brpl 1b | brpl 1b | brpl 1b | brpl 1b |
com r18 | com r18 | com r18 | com r18 | com r18 |
and r18,r24 | and r18,r24 | and r18,r24 | and r18,r24 | and r18,r24 |
mov r24,r20 | mov r24,r20 | mov r24,r20 | ||
clr r25 | ||||
sbrc r24,7 | ||||
com r25 | ||||
mov r0,r22 | mov r0,r22 | mov r0,r22 | mov r0,r22 | mov r0,r22 |
rjmp 2f | rjmp 2f | rjmp 2f | rjmp 2f | rjmp 2f |
1: | 1: | 1: | 1: | 1: |
lsl r24 | lsl r24 | lsl r24 | lsl r20 | lsl r20 |
2: | 2: | 2: | 2: | 2: |
dec r0 | dec r0 | dec r0 | dec r0 | dec r0 |
brpl 1b | brpl 1b | brpl 1b | brpl 1b | brpl 1b |
or r24,r18 | or r24,r18 | or r24,r18 | or r24,r18 | or r24,r18 |
ret | ret | ret | ret | ret |
.size TestA, .-TestA | .size TestB, .-TestB | .size TestC, .-TestC | .size TestD, .-TestD | .size TestE, .-TestE |
結果からすると,引数の型を bool としたときに出力されるコードは,引数の型を uint8_t とまったく同じになりました。 というわけで,冒頭の結論に至ったわけです。
ちょっと意外だったのが,int8_t 型の時の挙動。 引数の型を int8_t にした時だけ,r20 (引数 bit の値) を r24 にコピーした後でご丁寧に r25 の符号処理を行っています。 後の処理では使われないのに……?
また,16 bit 整数型を使うと,bool 型や 8 bit 整数型を使った場合と違って,r20 (引数 bit の値) を r24 へコピーする処理がありません。 結果,関数内の命令数が一番少ないのは 16 bit 整数型を使った場合となりました。 しかしながら,この関数は bit の値が 0 か 1 でないと期待した動作をしてくれません。 ので,引数 bit の型を bool 以外にした場合,値が 0 または 1 であることのアサーションが必要になります。 こういう点まで考えると,引数 bit の型には bool を選択するのがよさそうです。
とりあえずの興味は満たされたので,今のところはこれ以上深追いはしない予定です。