XC8攻略記3

攻略記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) にすると正しくない」とコメントしてあるように、間違った変形をするとバグを作りこんでしまいます。

また、「どうして、こんな条件式になったのか、わかりにくい」となってしまい、メンテナンス性が落ちることもあります。適切なコメントを残して、対処しましょう。

コメント