攻略記2の【注意点5】ループ処理カウント方法の話の中で、「ループを抜けるチェックもカウントダウンの方が簡潔になっています。」ということを書きました。(該当箇所へのリンク)そこで条件式について調べてみたいと考えました。
【注意点6】条件式は0を意識する
まず、2つの変数を比較する条件式です。例として、次のようなCソースを用意しました。
static uint8_t IF_UpCounter = 0;
static uint8_t IF_DownCounter = 0;
uint8_t ifcase1()
{
uint8_t retval = 0;
if (IF_DownCounter == IF_UpCounter)
{
retval |= (1 << 0);
}
if (IF_DownCounter != IF_UpCounter)
{
retval |= (1 << 1);
}
if (IF_DownCounter < IF_UpCounter)
{
retval |= (1 << 2);
}
if (IF_DownCounter > IF_UpCounter)
{
retval |= (1 << 3);
}
if (IF_DownCounter <= IF_UpCounter)
{
retval |= (1 << 4);
}
if (IF_DownCounter >= IF_UpCounter)
{
retval |= (1 << 5);
}
return retval;
}
この関数は、IF_DownCounterとIF_UpCounterを比較して、条件に応じてretvalの各ビットを1にして、返しています。IF_DownCounterとIF_UpCounterはこの関数の外でそれぞれカウントダウンしたりカウントアップされているものとします。
もちろん、下のように書いた方が効率が良いことはわかっていますが、条件式に対するアセンブルリストを見るために、無駄なコードになっています。
if (IF_DownCounter == IF_UpCounter)
{
retval |= (1 << 0);
}
else
{
retval |= (1 << 1);
}
// '<'と'>='も同様
// '>'と'<='も同様
これに対するアセンブルリストは次のようになります。
先に結論を書くと、このケースではおかしなアセンブルリストにはなっていません。おそらく人の手でアセンブラコードを作ってもだいたい同じになると思います。
37: uint8_t ifcase1()
38: {
39: uint8_t retval = 0;
0746 01F7 CLRF __pcstackCOMMON
40: if (IF_DownCounter == IF_UpCounter)
0747 0872 MOVF IF_DownCounter, W
0748 0673 XORWF IF_UpCounter, W
0749 1903 BTFSC STATUS, 0x2
41: {
42: retval |= (1 << 0);
074A 1477 BSF __pcstackCOMMON, 0x0
43: }
44: if (IF_DownCounter != IF_UpCounter)
074B 0872 MOVF IF_DownCounter, W
074C 0673 XORWF IF_UpCounter, W
074D 1D03 BTFSS STATUS, 0x2
45: {
46: retval |= (1 << 1);
074E 14F7 BSF __pcstackCOMMON, 0x1
47: }
48: if (IF_DownCounter < IF_UpCounter)
074F 0873 MOVF IF_UpCounter, W
0750 0272 SUBWF IF_DownCounter, W
0751 1C03 BTFSS STATUS, 0x0
49: {
50: retval |= (1 << 2);
0752 1577 BSF __pcstackCOMMON, 0x2
51: }
52: if (IF_DownCounter > IF_UpCounter)
0753 0872 MOVF IF_DownCounter, W
0754 0273 SUBWF IF_UpCounter, W
0755 1C03 BTFSS STATUS, 0x0
53: {
54: retval |= (1 << 3);
0756 15F7 BSF __pcstackCOMMON, 0x3
55: }
56: if (IF_DownCounter <= IF_UpCounter)
0757 0872 MOVF IF_DownCounter, W
0758 0273 SUBWF IF_UpCounter, W
0759 1803 BTFSC STATUS, 0x0
57: {
58: retval |= (1 << 4);
075A 1677 BSF __pcstackCOMMON, 0x4
59: }
60: if (IF_DownCounter >= IF_UpCounter)
075B 0873 MOVF IF_UpCounter, W
075C 0272 SUBWF IF_DownCounter, W
075D 1803 BTFSC STATUS, 0x0
61: {
62: retval |= (1 << 5);
075E 16F7 BSF __pcstackCOMMON, 0x5
63: }
64: return retval;
075F 0877 MOVF __pcstackCOMMON, W
65: }
0760 0008 RETURN
等価演算子(==および!=)
まず”==”演算子の部分です。
40: if (IF_DownCounter == IF_UpCounter)
0747 0872 MOVF IF_DownCounter, W
0748 0673 XORWF IF_UpCounter, W
0749 1903 BTFSC STATUS, 0x2
41: {
42: retval |= (1 << 0);
074A 1477 BSF __pcstackCOMMON, 0x0
43: }
4つのアセンブルリストが生成されてますが、最初の2つがif文の条件式に対するものです。
MOVF IF_DownCounter, W ; IF_DownCounterのメモリの値をWレジスタにセット。
XORWF IF_UpCounter, W ; W = W XOR IF_UpCounterのメモリの値になる。
2つの変数の値が同じ→Wレジスタは0になる→STATUSレジスタのZビットが1になる。
2つの変数の値が違う→Wレジスタは0以外の値になる→STATUSレジスタのZビットが0になる。
という動作をするので、
BTFSC STATUS, 0x2 ; STATUS.Zが1なら次の命令を実行する。0ならスキップ。
BSF __pcstackCOMMON, 0x0 ; __pcstackCOMMONの0ビット目を1にセット。
としています。__pcstackCOMMONは変数retvalに割り当てられているメモリです。(関数内のローカル変数なので、IF_DownCounterのように名前が一致していません。)
”!=”演算子もほとんど同じですが、”==”と逆の論理になるためBTFSCではなくBTFSSを使っています。
BTFSS STATUS, 0x2 ; STATUS.Zが0なら次の命令を実行する。1ならスキップ。
BSF __pcstackCOMMON, 0x1 ; __pcstackCOMMONの1ビット目を1にセット。
比較演算子1(”<”および">")
”<”演算子の部分です。
48: if (IF_DownCounter < IF_UpCounter)
074F 0873 MOVF IF_UpCounter, W
0750 0272 SUBWF IF_DownCounter, W
0751 1C03 BTFSS STATUS, 0x0
49: {
50: retval |= (1 << 2);
0752 1577 BSF __pcstackCOMMON, 0x2
ここではSUBWF名を使って、2つの変数の引き算を計算しています。その結果、STATUSレジスタのCビットが変化します。
以下は、PIC12F1822の日本語版DATASHEETからの抜粋です。
Wの値(上のアセンブルリストではIF_UpCounterが入っている)とレジスタf(IF_DownCounter)を使ってSUBWF命令を実行するとそれぞれの値に応じて、STATUS.Cビットが変化します。つまり、 "W > f" は、"IF_UpCounter > IF_DownCounter"と同じなので、このときに"C=0"なら条件式は真となります。
3行目が”BTFSS STATUS, 0x0”となっているので、STATUS.Cビットが0なら次の命令を実行し、1ならスキップします。
”>”演算子もほとんど同じですが、”<”と逆の論理になるため、IF_UpCounterの扱いとIF_DownCounterの扱いが逆になっています。
52: if (IF_DownCounter > IF_UpCounter)
0753 0872 MOVF IF_DownCounter, W
0754 0273 SUBWF IF_UpCounter, W
比較演算子2("<="および”>=”)
"<="演算子を見てみます。
56: if (IF_DownCounter <= IF_UpCounter)
0757 0872 MOVF IF_DownCounter, W
0758 0273 SUBWF IF_UpCounter, W
0759 1803 BTFSC STATUS, 0x0
これは、MOVFとSUBWFの部分は">"と同じです。上に載せたSUBWFの説明にあるように"W ≦ f"のときSTATUS.Cは1になります。つまり、"IF_DownCounter ≦ IF_UpCounter"のとき"C=1"なら条件式は真となります。
Cの値が”>”のときと逆になるので、">"のときにBTFSSだった命令が、">="のときはBTFSCになっています。
">="演算子の場合は、IF_UpCounterの扱いとF_DownCounterの扱いが逆になっているだけで、他の部分は"<="と同じです。
2つの変数を比較する条件式はこれで全てです。全て無駄がないといってよいでしょう。
変数と定数を比較する条件式
PICでいろいろプログラムを作ってきた個人的な経験から言えば、2つの変数の比較することはどちらかと言えば少ないケースだと思います。
それよりも変数と定数を比較することのほうが多いでしょう。例えば、XC8攻略1でも、74hc595に8ビットデータを送信するために、8をカウントするための「定数との比較」をすることが多かったです。逆に変数同士の比較はあまりありませんでした。
変数と定数を比較するケースのアセンブルリストを見てみましょう。
67: uint8_t ifcase2()
68: {
69: uint8_t retval = 0;
0761 01F7 CLRF __pcstackCOMMON
70: if (IF_DownCounter == 32)
0762 3020 MOVLW 0x20
0763 0672 XORWF IF_DownCounter, W
0764 1903 BTFSC STATUS, 0x2
71: {
72: retval |= (1 << 0);
0765 1477 BSF __pcstackCOMMON, 0x0
73: }
74: if (IF_DownCounter != 32)
0766 3020 MOVLW 0x20
0767 0672 XORWF IF_DownCounter, W
0768 1D03 BTFSS STATUS, 0x2
75: {
76: retval |= (1 << 1);
0769 14F7 BSF __pcstackCOMMON, 0x1
77: }
78: if (IF_DownCounter < 32)
076A 3020 MOVLW 0x20
076B 0272 SUBWF IF_DownCounter, W
076C 1C03 BTFSS STATUS, 0x0
79: {
80: retval |= (1 << 2);
076D 1577 BSF __pcstackCOMMON, 0x2
81: }
82: if (IF_DownCounter > 32)
076E 3021 MOVLW 0x21
076F 0272 SUBWF IF_DownCounter, W
0770 1803 BTFSC STATUS, 0x0
83: {
84: retval |= (1 << 3);
0771 15F7 BSF __pcstackCOMMON, 0x3
85: }
86: if (IF_DownCounter <= 32)
0772 3021 MOVLW 0x21
0773 0272 SUBWF IF_DownCounter, W
0774 1C03 BTFSS STATUS, 0x0
87: {
88: retval |= (1 << 4);
0775 1677 BSF __pcstackCOMMON, 0x4
89: }
90: if (IF_DownCounter >= 32)
0776 3020 MOVLW 0x20
0777 0272 SUBWF IF_DownCounter, W
0778 1803 BTFSC STATUS, 0x0
91: {
92: retval |= (1 << 5);
0779 16F7 BSF __pcstackCOMMON, 0x5
93: }
94: return retval;
077A 0877 MOVF __pcstackCOMMON, W
95: }
077B 0008 RETURN
Cのソースコードは書きません(読みにくいですが、アセンブルリスト中にも出ているので端折りました。)が、IF_DownCounterを定数32と比較するコードになっています。
"=="と"!="、"<"、”>="は2つの変数を比較した時とほとんど同じです。違いは、IR_UpCounterをWレジスタにセットするために、"MOVF IF_UpCounter, W"だった命令が、定数32をWレジスタにセットするために、”MOVLW 0x20”に変わっているだけです。
しかし、">"、"<="はちょっと変化しています。
まず、">"演算子の場合を見てみます。
82: if (IF_DownCounter > 32)
076E 3021 MOVLW 0x21
076F 0272 SUBWF IF_DownCounter, W
0770 1803 BTFSC STATUS, 0x0
0x21なので、最初に33をWレジスタにセットし、SUBWFで比較を行っています。
これだと、IF_DownCounterと33の比較をしていることになります。IF_DownCounterが8bit整数であることを利用して、"IF_DownCounter >= 33"の比較を行うことで、"IF_DownCounter > 32"の比較を行っているようです。
” SUBWF IF_DownCounter, W”で比較を行うために、Wに入れる値を工夫しているのでしょう。私は初見ではこのことが理解できなくて、「33はどこから来たのか?」と悩んでしまいました。
ただ理屈が分かれば簡単です。"<="も同じ理屈にしたがったアセンブルリストができています。
定数と変数の比較も無駄がないといってよいでしょう。
変数と0を比較する条件式
0も定数です。0と比較するケースは、また少し違ってきます。
97: uint8_t ifcase3()
98: {
99: uint8_t retval = 0;
0723 01F7 CLRF __pcstackCOMMON
100: if (IF_DownCounter == 0)
0724 0872 MOVF IF_DownCounter, W
0725 1903 BTFSC STATUS, 0x2
101: {
102: retval |= (1 << 0);
0726 1477 BSF __pcstackCOMMON, 0x0
103: }
104: if (IF_DownCounter != 0)
0727 0872 MOVF IF_DownCounter, W
0728 1D03 BTFSS STATUS, 0x2
105: {
106: retval |= (1 << 1);
0729 14F7 BSF __pcstackCOMMON, 0x1
107: }
108: if (IF_DownCounter < 0)
109: {
110: retval |= (1 << 2);
111: }
112: if (IF_DownCounter > 0)
072A 0872 MOVF IF_DownCounter, W
072B 1D03 BTFSS STATUS, 0x2
113: {
114: retval |= (1 << 3);
072C 15F7 BSF __pcstackCOMMON, 0x3
115: }
116: if (IF_DownCounter <= 0)
072D 0872 MOVF IF_DownCounter, W
072E 1903 BTFSC STATUS, 0x2
117: {
118: retval |= (1 << 4);
072F 1677 BSF __pcstackCOMMON, 0x4
119: }
120: if (IF_DownCounter >= 0)
121: {
122: retval |= (1 << 5);
0730 16F7 BSF __pcstackCOMMON, 0x5
123: }
124: return retval;
0731 0877 MOVF __pcstackCOMMON, W
125: }
0732 0008 RETURN
32と比較する時とは全く違うアセンブルリストになっています。
まず、SUBWFを使っていません。” MOVF IF_DownCounter, W”でWレジスタにIF_DownCounterの値をセットしたとき、それが0か否かでSTATUS.Zが変化するので、定数0と比較する必要がないのです。
また、IF_DownCounterはuint8_tであり、0~255の整数値を取ります。そのためマイナスにはなりません。
ということは、"IF_DownCounter >= 0"は常に真ですし、"IF_DownCounter < 0"は常に偽です。
それらに対応するアセンブルリストは生成されていません。また、"IF_DownCounter < 0"が常に偽なので、”retval |= (1 << 2);”に対するアセンブルリストも生成されていません。
"IF_DownCounter > 0"は実質的には"IF_DownCounter != 0"と同じなので、アセンブルリストも同じになっています。同様に"IF_DownCounter <= 0"は"IF_DownCounter == 0"と同じになっています。
というわけで、0との比較も無駄は無さそうです。
条件式で注意するべきこと
ここまで見てきた限りでは、XC8は条件式に対しては最適なアセンブルリストを生成しているように見えます。以上、おわり。
…としたいところですが、定数の比較には注意が必要でしょう。
結論から言えば、「可能な限り、0と比較する条件を使うべき」です。
次のCソースを見てください。
IF_UpCounter++;
if (IF_UpCounter == 100)
{
IF_UpCounter = 0;
}
IF_DownCounter--;
if (IF_DownCounter == 0)
{
IF_DownCounter = 100;
}
IF_UpCounterは100回カウントアップすると0に戻り、IF_DownCounterは100回カウントダウンすると100に戻ります。
どちらも「100回をカウント」としていますが、ifの条件式で0と比較するカウントダウンの方がアセンブルリストが短くなり有利です。
また、下のようなコードは条件式的には無駄があります。
typedef enum {
XXX_Null, // 初期値
XXX_WAIT_START, // 開始待ちフェイズ
XXX_START, // 信号送信開始フェイズ
XXX_SEND, // 送信処理フェイズ
XXX_WAIT_RETURN, // 相手からの返信待ちフェイズ
XXX_STOP, // 送信処理終了フェイズ
} XXX_Phase_t;
static XXX_Phase_t XXX_Phase = XXX_Null;
void main(void){
// 様々な初期化処理
XXX_Phase = XXX_WAIT_START;
while (1){
if ( RA2 == 0 && XXX_Phase == XXX_WAIT_START ){
// RA2のスイッチが押されて0になったので、処理を開始する。
// ただし、既に処理中にXXX_STARTに戻ると動作がおかしくなるので、
// XXX_WAIT_STARTのときのみ、受け付ける。
XXX_Phase = XXX_START;
}
if ( XXX_Phase != XXX_WAIT_START ){
// 開始待ちでないなら、XXXの処理を行う。
XXX_Run();
}
// その他の処理
}
}
enumの最初(コンパイルされると通常は整数0が割り当てられる)に、初期値(Null)値を置くことは、通常のCのソースコードではよくあることです。しかし、まったく使用しないenum値にせっかくの0を使ってしまうのは、XC8ではもったいないといってよいでしょう。
while(1)ループで中で何度も比較することになる、XXX_WAIT_STARTが0になるようにしたほうが良いと思います。
初期値(Null)についての補足。
enumの最初に初期値(Null)値を置くことがよくあることだと上では書いています。これは大量に生成されるデータの初期値として0にしたほうが都合のよいことがあるからです。例えば、次のようなプログラムです。
typedef enum {
NodeNull, // 初期値
NodeAdd, // '+'
NodeSub, // '-'
// 以下省略
} NodeType;
// 計算式を構文木にするときのノード
typedef struct {
NodeType type;
int value;
Node_t* left; // 左側のノード
Node_t* right; // 右側のノード
Node_t* next;
} Node_t;
// Node_tの全リスト
Node_t* AllNodeList = NULL;
// Node_tを生成し、管理リストに入れる。
Node_t* NodeAlloc(void){
Node_t* node = (Node_t*)calloc(1,sizeof(Node_t));
node->next = AllNodeList;
AllNodeList = node;
return node;
}
構造体のTypeとしてenum型を定義している時に、構造体をcallocでメモリ確保すると自然に、node->typeに初期値が入ります。そしてnodeを使う部分で、assert(node->type != NodeNull); などとすることで、バグ(typeの設定漏れ)を検出しやすくできます。
Nodeが大量に作られるようなプログラムならこういう使い方をするために enumの最初がNullの方が良いこともあります。しかし、XC8のプログラムでは、こういった使い方をするstructやenum型を定義することはないはずです。
【注意点7】計算式を比較する条件式
条件式に計算式が含まている場合はどうでしょうか?
例えば次のようなif文です。
if ((IF_DownCounter >> 4) == (uint8_t)(IF_UpCounter + 3))
{
retval |= (1 << 0);
}
これに対して生成されるアセンブルリストが次のようになりました。
130: if ((IF_DownCounter >> 4) == (uint8_t)(IF_UpCounter + 3))
0799 0873 MOVF IF_DownCounter, W
079A 00F8 MOVWF __pcstackCOMMON
079B 3004 MOVLW 0x4
079C 36F8 LSRF __pcstackCOMMON, F
079D 0B89 DECFSZ WREG, F
079E 2F9C GOTO 0x79C
079F 0874 MOVF IF_UpCounter, W
07A0 3E03 ADDLW 0x3
07A1 0678 XORWF __pcstackCOMMON, W
07A2 1903 BTFSC STATUS, 0x2
2~7行目(色を付けて強調しています)の部分は、”(IF_DownCounter >> 4)”を計算していて、結果は__pcstackCOMMONに入るようになっています。そのあと”IF_UpCounter + 3”を計算して、XORWFで"=="の比較を行いっています。
これもCのソースコード通りのアセンブルリストになっているといってよいでしょう。条件式としては無駄なことはしていません。
ただし、”(IF_DownCounter >> 4)”を計算するアセンブルリストがけっこう効率の悪いものになっていますので、こういった計算は別途行って一時的な変数に格納し、変数同士の比較にしておいた方がよいでしょう。
”IF_UpCounter + 3”の方はそれほど重たくないのでこのままでもよいと思いますが、やはりシフト演算は鬼門と言ってよいでしょう。
上の例では演算子の右と左の両方を計算することになるので、仕方のない部分もありますが、次のようなソースは見直しの余地があると思います。
if ((IF_DownCounter >> 4) > 5)
{
retval |= (1 << 1);
}
XC8はこれもCのソース通りにアセンブルリストを作成するため、”(IF_DownCounter >> 4)”を計算し、定数5と比較しようとします。
しかし、この条件式は、次のように変形することができます。
// (IF_DownCounter >> 4) > 5
// -> (IF_DownCounter >> 4) >= 6
// -> ((IF_DownCounter >> 4) << 4) >= (6 << 4)
// -> IF_DownCounter >= (6 << 4) と変形している。
// 注意:IF_DownCounter > (5 << 4) にすると正しくないので、注意
if (IF_DownCounter >= (6 << 4))
{
retval |= (1 << 2);
}
このソースの場合、(6<<4)はコンパイル時に0x60に変換されるので、"if (IF_DownCounter >= 0x60)"と同じです。これは変数と定数の単純な比較になるので、シフト演算のためのアセンブルリストが生成されなくなります。revalに値を格納するところまで含めても、たった4行のアセンブルリストしか生成されません。
したがって、「計算式の比較は、可能な限り避けるべき」です。式を変形することで計算回数を減らすことができないか検討する癖をつけた方が良いでしょう。
ただし、「注意:IF_DownCounter > (5 << 4) にすると正しくない」とコメントしてあるように、間違った変形をするとバグを作りこんでしまいます。
また、「どうして、こんな条件式になったのか、わかりにくい」となってしまい、メンテナンス性が落ちることもあります。適切なコメントを残して、対処しましょう。
コメント