XC8攻略記2

XC8攻略記。今回はもっとも基本的な算術処理についてです。

【注意点4】演算子の罠

もっとも単純な計算式を考えてみます。次の計算式はAとBを足してCに代入しています。

C = A + B;  // AとBを足してCに代入

次のように書けば、Cに8を加えることになります。

C = C + 8;  // Cと8を足してCに代入

ところで、C言語には同様の処理を行うために、他にも書き方があります。

C = 8 + C;  // 8とCを足してCに代入
C += 8;     // Cに8を加える

とくに下の書き方は、コーディング量が少なくなるため、多用される書き方です。この例では変数名が”C"と一文字しかないのでさほどコーディング量が変わりませんが、長い変数名だったり、構造体のメンバ名にアクセスするようなときは、下の書き方の方が楽なのです。

しかし、XC8ではここに大きな罠があります。3つの計算式を下のように1つの関数の中で使ってみました。

static uint8_t Counter = 0;
void CountUp8(void)
{
    Counter = Counter + 8;
    Counter = 8 + Counter;
    Counter += 8;
}

最終的にCounterは24増えることになるので、Counter+=24と書いた方が良いことはわかっています。ここでは計算式に対してどのようなアセンブルリストが生成されるのかを調べようとしています。

これをコンパイルした結果のアセンブルリストは下のようになりました。

140:           void CountUp8(void)
141:           {
142:               Counter = Counter + 8;
0709  087D     MOVF 0xFD, W
070A  3E08     ADDLW 0x8
070B  00FD     MOVWF 0xFD
143:               Counter = 8 + Counter;
070C  087D     MOVF 0xFD, W
070D  3E08     ADDLW 0x8
070E  00FD     MOVWF 0xFD
144:               Counter += 8;
070F  3008     MOVLW 0x8
0710  00F0     MOVWF 0xF0
0711  0870     MOVF 0xF0, W
0712  07FD     ADDWF 0xFD, F
145:           }

「Counter = Counter + 8;」と「Counter = 8 + Counter;」は同じ結果になっています。順番が違っていても同じ式であると解釈して同じようにコンパイルされているようです。

MOVF 0xFD, W      0xFD番地のメモリの値をWレジスタにセット
ADDLW 0x8         Wレジスタに8を加算する
MOVWF 0xFD        Wレジスタを0xFD番地のメモリに保存する。

0xFD番地が変数Counterに対応しているなら、Cのソースコード通りの結果になっているといってよいでしょう。

番地についての補足。

PICの命令ではメモリの番地を指定するとき、0~127の数字を指定できます。そのため0xFD番地というのは不正確で、厳密には0x7D番地が正しいです。

しかし今回の説明ではそこまでの正確性は不要なので、わかりやすさを重視してアセンブルリストの表示にしたがって0xFDとしていますし、この後もそのように記載しています。

問題は「Counter += 8;」の方です。アセンブラの動きを見ると、無駄なことをしています。

MOVLW 0x8         Wレジスタに8をセット
MOVWF 0xF0    0xF0番地のメモリにWレジスタの値を保存
MOVF 0xF0, W      0xF0番地のメモリの値をWレジスタにセット
ADDWF 0xFD, F     0XFD番地のメモリにWレジスタの値を加える。

0xF0という余計なメモリにアクセスするのは謎です。

しかも「Wレジスタ→0xF0に格納、0xF0→Wレジスタに戻す」というのはまったく無駄な処理です。その2つを省いて、下のようにしてもCounterの値は正しく計算されます。

MOVLW 0x8         Wレジスタに8をセット
ADDWF 0xFD, F     0xF3番地のメモリにWレジスタの値を加える。

つまり、「変数 += 定数」という書き方をすると無駄なアセンブルリストが生成されるのです。

変数を加算する場合

定数でない場合はどうなるでしょうか?

void CountUpValue(uint8_t value)
{
    Counter = Counter + value;
    Counter += value;
}

このアセンブルストは下のようになりました。

147:           void CountUpValue(uint8_t value)
06EA  00F1     MOVWF 0xF1
148:           {
149:               Counter = Counter + value;
06EB  087D     MOVF 0xFD, W
06EC  0771     ADDWF 0xF1, W
06ED  00FD     MOVWF 0xFD
150:               Counter += value;
06EE  0871     MOVF 0xF1, W
06EF  00F0     MOVWF 0xF0
06F0  0870     MOVF 0xF0, W
06F1  07FD     ADDWF 0xFD, F
151:           }
06F2  0008     RETURN

0xFD番地がCounter、0xF1番地が関数の引数valueを格納していると考えると、やはり0xF0に謎のアクセスがあります。定数の加算と同様で、"+="の方がアセンブルリストが長くなってしまいます。

式を加算する場合

1つの変数ではなく、複数の変数からなる式の場合はどうなるでしょうか?

void CountUp2Value(uint8_t value1, uint8_t value2)
{
    Counter = Counter + value1 + value2;
    Counter += (value1 + value2);
}
153:           void CountUp2Value(uint8_t value1, uint8_t value2)
06FE  00F2     MOVWF 0xF2
154:           {
155:               Counter = Counter + value1 + value2;
06FF  087D     MOVF 0xFD, W
0700  0772     ADDWF 0xF2, W
0701  0770     ADDWF 0xF0, W
0702  00FD     MOVWF 0xFD
156:               Counter += (value1 + value2);
0703  0872     MOVF 0xF2, W
0704  0770     ADDWF 0xF0, W
0705  00F1     MOVWF 0xF1
0706  0871     MOVF 0xF1, W
0707  07FD     ADDWF 0xFD, F
157:           }
0708  0008     RETURN

式の場合も結果は同様でした。なお、謎アクセスの番地は0xF1になっています。0xF0ではないのは関数の引数が変わったためで、0xF0番地はvalue2の値を格納しています。

減算する場合

ここまでの計算は全て加算でした。減算の場合はどうなるのでしょうか?

void CountDownAllCase(uint8_t value1, uint8_t value2)
{
    // 定数
    Counter = Counter - 8;
    Counter -= 8;

    // 変数
    Counter = Counter - value1;
    Counter -= value2;

    // 式
    Counter = Counter - (value1 + value2);
    Counter -= (value1 + value2);
}
159:           void CountDownAllCase(uint8_t value1, uint8_t value2)
0727  00F2     MOVWF 0xF2
160:           {
161:               // 定数
162:               Counter = Counter - 8;
0728  087D     MOVF 0xFD, W
0729  3EF8     ADDLW 0xF8
072A  00FD     MOVWF 0xFD
163:               Counter -= 8;
072B  3008     MOVLW 0x8
072C  02FD     SUBWF 0xFD, F
164:           
165:               // 変数
166:               Counter = Counter - value1;
072D  0872     MOVF 0xF2, W
072E  027D     SUBWF 0xFD, W
072F  00FD     MOVWF 0xFD
167:               Counter -= value2;
0730  0870     MOVF 0xF0, W
0731  02FD     SUBWF 0xFD, F
168:           
169:               // 式
170:               Counter = Counter - (value1 + value2);
0732  0372     DECF 0xF2, W
0733  3AFF     XORLW 0xFF
0734  00F1     MOVWF 0xF1
0735  0870     MOVF 0xF0, W
0736  0271     SUBWF 0xF1, W
0737  077D     ADDWF 0xFD, W
0738  00FD     MOVWF 0xFD
171:               Counter -= (value1 + value2);
0739  0870     MOVF 0xF0, W
073A  0772     ADDWF 0xF2, W
073B  00F1     MOVWF 0xF1
073C  0871     MOVF 0xF1, W
073D  02FD     SUBWF 0xFD, F
172:           }
073E  0008     RETURN

今度は、”変数 -= ???"の書き方の方がアセンブルリストが短くなるようです。とくに、"Counter -= 8;"と"Counter -= value2;"に対するアセンブルリストには全く無駄がなく、人手でアセンブラをコーディングした時とほぼ同じなるでしょう。

"Counter = Counter - (value1 + value2);”はCのソースコードとは大きく変わっていて、すぐには理解できませんでした。これなら式を2つ分けて、"Counter -= value1; Counter -=value2;"としたほうが、短くなりそうです。

乗算・除算の場合

PIC12FやPIC16Fなどは、乗算・除算はライブラリルーチンをコールする形になります。つまり、

C = A * B;

は、

C = __bmul(A,B); // _bmul()はXC8が用意している8bit乗算用ライブラリ関数

としているのと同じです。このため、加算・減算のような問題は発生しません。除算も同様です。

なお、2の指数の除算は最適化により右シフトと同じになります。(A/2はA>>1と同じ。B/8はB>>3と同じ。)

論理演算の場合

ANDやORのような論理演算の場合はどうなるでしょうか?

void LogicCase(uint8_t value1, uint8_t value2)
{
    // 定数
    Counter = Counter & 0x0F;
    Counter &= 0x0F;
    Counter = Counter | 0x55;
    Counter |= 0x55;

    // 変数
    Counter = Counter | value1;
    Counter &= value2;

    // 式
    Counter = (Counter & (value1 | value2));
    Counter |= (value1 & value2);
}
174:           void LogicCase(uint8_t value1, uint8_t value2)
0757  00F2     MOVWF 0xF2
175:           {
176:               // 定数
177:               Counter = Counter & 0x0F;
0758  087D     MOVF 0xFD, W
0759  390F     ANDLW 0xF
075A  00FD     MOVWF 0xFD
178:               Counter &= 0x0F;
075B  300F     MOVLW 0xF
075C  00F1     MOVWF 0xF1
075D  0871     MOVF 0xF1, W
075E  05FD     ANDWF 0xFD, F
179:               Counter = Counter | 0x55;
075F  087D     MOVF 0xFD, W
0760  3855     IORLW 0x55
0761  00FD     MOVWF 0xFD
180:               Counter |= 0x55;
0762  3055     MOVLW 0x55
0763  00F1     MOVWF 0xF1
0764  0871     MOVF 0xF1, W
0765  04FD     IORWF 0xFD, F
181:           
182:               // 変数
183:               Counter = Counter | value1;
0766  087D     MOVF 0xFD, W
0767  0472     IORWF 0xF2, W
0768  00FD     MOVWF 0xFD
184:               Counter &= value2;
0769  0870     MOVF 0xF0, W
076A  00F1     MOVWF 0xF1
076B  0871     MOVF 0xF1, W
076C  05FD     ANDWF 0xFD, F
185:           
186:               // 式
187:               Counter = (Counter & (value1 | value2));
076D  0872     MOVF 0xF2, W
076E  0470     IORWF 0xF0, W
076F  057D     ANDWF 0xFD, W
0770  00FD     MOVWF 0xFD
188:               Counter |= (value1 & value2);
0771  0872     MOVF 0xF2, W
0772  0570     ANDWF 0xF0, W
0773  00F1     MOVWF 0xF1
0774  0871     MOVF 0xF1, W
0775  04FD     IORWF 0xFD, F
189:           }
0776  0008     RETURN

ADD??という命令がAND??やIOR??命令に変わっているだけと考えれば、加算のときと同じと言ってよいでしょう。

試してはいませんが、XORも同じでしょう。

シフト演算の場合

シフト演算の場合はどうなるでしょうか?

void ShiftCase(void){
    Counter = Counter << 1;
    Counter = Counter >> 1;
    Counter >>= 1;
    Counter <<= 1;
    Counter = Counter << 3;
    Counter = Counter >> 2;
    Counter <<= 3;
    Counter >>= 2;
    Counter = Counter << 5;
    Counter <<= 5;
}
191:           void ShiftCase(void)
192:           {
193:               Counter = Counter << 1;
0777  357D     LSLF 0xFD, W
0778  00FD     MOVWF 0xFD
194:               Counter = Counter >> 1;
0779  367D     LSRF 0xFD, W
077A  00FD     MOVWF 0xFD
195:               Counter >>= 1;
077B  1003     BCF STATUS, 0x0
077C  0CFD     RRF 0xFD, F
196:               Counter <<= 1;
077D  1003     BCF STATUS, 0x0
077E  0DFD     RLF 0xFD, F
197:               Counter = Counter << 3;
077F  087D     MOVF 0xFD, W
0780  00F0     MOVWF 0xF0
0781  3002     MOVLW 0x2
0782  35F0     LSLF 0xF0, F
0783  3EFF     ADDLW 0xFF
0784  1D03     BTFSS STATUS, 0x2
0785  2F82     GOTO 0x782
0786  3570     LSLF 0xF0, W
0787  00FD     MOVWF 0xFD
198:               Counter = Counter >> 2;
0788  087D     MOVF 0xFD, W
0789  00F0     MOVWF 0xF0
078A  3002     MOVLW 0x2
078B  36F0     LSRF 0xF0, F
078C  0B89     DECFSZ WREG, F
078D  2F8B     GOTO 0x78B
078E  0870     MOVF 0xF0, W
078F  00FD     MOVWF 0xFD
199:               Counter <<= 3;
0790  1003     BCF STATUS, 0x0
0791  0DFD     RLF 0xFD, F
0792  1003     BCF STATUS, 0x0
0793  0DFD     RLF 0xFD, F
0794  1003     BCF STATUS, 0x0
0795  0DFD     RLF 0xFD, F
200:               Counter >>= 2;
0796  1003     BCF STATUS, 0x0
0797  0CFD     RRF 0xFD, F
0798  1003     BCF STATUS, 0x0
0799  0CFD     RRF 0xFD, F
201:               Counter = Counter << 5;
079A  087D     MOVF 0xFD, W
079B  00F0     MOVWF 0xF0
079C  3004     MOVLW 0x4
079D  35F0     LSLF 0xF0, F
079E  3EFF     ADDLW 0xFF
079F  1D03     BTFSS STATUS, 0x2
07A0  2F9D     GOTO 0x79D
07A1  3570     LSLF 0xF0, W
07A2  00FD     MOVWF 0xFD
202:               Counter <<= 5;
07A3  0EFD     SWAPF 0xFD, F
07A4  0DFD     RLF 0xFD, F
07A5  30E0     MOVLW 0xE0
07A6  05FD     ANDWF 0xFD, F
203:           }
07A7  0008     RETURN

PICには左シフトを1回だけ行う命令が複数あります。そして Counter = Counter << 1に対して、LSLFという命令を使い、Counter <<= 1にはRLFという命令を使うようにアセンブルリストが生成されます。この使い分けをしている理由は謎ですが、どちらのケースもアセンブルリストの長さは同じなるので、プログラムサイズと実行速度には影響がありません。右シフトの場合も同様です。

しかし、シフトを2回以上行う場合はアセンブルリストに違いが出てきます。Counter = Counter << 3 の場合、ループ処理でLSLFを3回行うようなアセンブルリストになり、Counter <<= 3に対しては、ループではなくRLFを3回実行するように展開したアセンブルリストになっています。右シフトのときも同様です。

そして展開するケースである <<= や >>=の方がアセンブルリストも実行速度も短くなっています。

なお、ループで処理するほうはシフト回数でアセンブラリストの長さは変化しませんが、展開するほうはシフト回数の数だけアセンブラリストが増えていきます。

ということは、シフト回数が5になったとき、展開(Counter <<= 5)とループ(Counter = Counter << 5)を比べると、展開の方がアセンブルリストが長くなってしまうのではないか?…と思っていたのですが、実際にコードを書いて試してみると、意外な結果になりました。

Counter <<= 5;に対して、次のようなアセンブルリストが作成されます。

SWAPF 0xFD, F    0xFD番地の上位4bitと下位4bitを入れ替える。(=4つシフトする)
RLF 0xFD, F      0xFD番地を左に1つシフトする。(SWAPFと合わせて5つシフトになる。)
MOVLW 0xE0       Wレジスタに 0xE0( 2進数で 11100000 )
ANDWF 0xFD, F    0xFD番地にWレジスタとANDした結果をセットする。(下位5bitが0になる。)

SWAP命令は、8bitの上位4bitと下位4bitを入れ替える命令です。これを使って4回シフト分を1命令で行っています。試していませんが6回や7回のシフトも同様にすると考えられますので、<<= の方がアセンブルリストが短くなります。

まとめ

ここまでの結果をまとめると次のようになります。

演算の種類アセンブルリストが短くなる書き方
加算A = A + B;
減算A -= B;
論理演算A = A & B;
A = A | B;
シフト演算A >>=定数;
A <<=定数;
ただし定数が1なら、A = A>>1もA>>=1も同じ。

プログラムサイズを抑えるには上の通りコーディングすることが望ましいですが、A = A + ???; と A-= ??? が混在するのは、綺麗な(読みやすい)コードではありません。プログラムサイズの差は1~2ぐらいなので気にしなくても良いかもしれません。どちらかに統一したほうが良いでしょう。

しかしシフト演算は注意が必要でしょう。命令数が増えるうえにループ処理をするアセンブルリストが生成されるので、大きな差が生じます。シフト演算だけは <<= としたほうが良いと思います。

Cのソースコードレベルでは一見すると冗長な一時変数を使うコード(下記の例ならSumHL2()の方)の方がプログラムサイズも実行速度も効率が良いのです。

// dataの上位4bitと下位4bitの合計を返す。
uint8_t SumHL(int8_t data){
    // 普通に記述する例
    return ((data & 0x0F) + (data >> 4));
}

// シフトを >>= にするために作業変数を使った例
uint8_t SumHL2(int8_t data){
    uint8_t tmp = data;
    tmp >>= 4;
    return ((data & 0x0F) + tmp);
}

これは知識として知っておいた方がよいでしょう。そして処理速度が重要な部分では積極的に使った方がよいでしょう。

【注意点5】ループ処理のカウント方法

算術系の演算子としてもう1つ忘れてはならないものに、インクリメント・デクリメントがあります。

void CountUpDown(void)
{
    Counter++;
    Counter--;
}

インクリメント・デクリメントの使用回数は非常に多いです。例えば、繰り返し処理を行うときに、

for ( uint8_t i = 0 ; i < 8 ; i++ ){
  // 繰り返したいコードをここに書く
}

のように、for文を使う事が多いでしょう。ここにもインクリメントが存在しています。

そして、このインクリメント・デクリメントに対するアセンブルリストは次のようになります。

205:           void CountUpDown(void)
206:           {
207:               Counter++;
06E3  3001     MOVLW 0x1
06E4  00F0     MOVWF 0xF0
06E5  0870     MOVF 0xF0, W
06E6  07FD     ADDWF 0xFD, F
208:               Counter--;
06E7  3001     MOVLW 0x1
06E8  02FD     SUBWF 0xFD, F
209:           }
06E9  0008     RETURN

どこかで見たことがないでしょうか? そうこれは、Counter += 8; や Counter -=8; の時と似ています。定数が8から1に変わっただけです。つまり、インクリメント・デクリメントは、+=1や-=1と同じなのです。

そしてこのことから次のことが想定できます。

void ForCase(void)
{
    uint8_t i;
    for (i = 0; i < 8; i++)
    {
        Counter--;
    }
    for (i = 8; i > 0 ; i--)
    {
        Counter--;
    }
    Counter = Counter + 16;
}

上のコードはどちらも8回ループを行います。カウンタとなる変数iがカウントアップするかカウントダウンするかの違いしかありません。しかしアセンブルリストを見ると大きな違いがあります。

211:           void ForCase(void)
212:           {
213:               uint8_t i;
214:               for (i = 0; i < 8; i++)
073F  01F1     CLRF 0xF1
215:               {
216:                   Counter--;
0740  3001     MOVLW 0x1
0741  02FD     SUBWF 0xFD, F
217:               }
0742  3001     MOVLW 0x1
0743  00F0     MOVWF 0xF0
0744  0870     MOVF 0xF0, W
0745  07F1     ADDWF 0xF1, F
0746  3008     MOVLW 0x8
0747  0271     SUBWF 0xF1, W
0748  1C03     BTFSS STATUS, 0x0
0749  2F40     GOTO 0x740
218:               for (i = 8; i > 0; i--)
074A  3008     MOVLW 0x8
074B  00F1     MOVWF 0xF1
219:               {
220:                   Counter--;
074C  3001     MOVLW 0x1
074D  02FD     SUBWF 0xFD, F
221:               }
074E  3001     MOVLW 0x1
074F  02F1     SUBWF 0xF1, F
0750  0871     MOVF 0xF1, W
0751  1D03     BTFSS STATUS, 0x2
0752  2F4C     GOTO 0x74C
222:               Counter = Counter + 16;
0753  087D     MOVF 0xFD, W
0754  3E10     ADDLW 0x10
0755  00FD     MOVWF 0xFD
223:           }
0756  0008     RETURN

色を付けている行が、for文のカウンタを更新し、ループ終了をチェックする部分になります。インクリメントとデクリメントの影響がそのまま出ています。(またループを抜けるチェックもカウントダウンの方が簡潔になっています。)

ループカウンタをループの内部で使わない場合は、カウントダウン方式の方が効率が良いことが分かります。(もちろんループカウンタを配列のインデックスなどに使うような場合は、この限りではありません。)

for以外でも、カウントアップとカウントダウンのどちらでもよいケースなら、カウントダウンを選ぶようにしたほうが良いでしょう。

なお、PICの命令には、INCF、DECFというインクリメント・デクリメントにふさわしい命令があるのですが、XC8はかたくなに使ってくれません。もしかするとPRO版では使ってくれるのかもしれませんが、これぐらいはフリー版でも使ってほしいところです。

補足1:謎についての推測

上の方では、「Wレジスタ→0xF0に格納、0xF0→Wレジスタに戻す」という、余計なメモリにアクセスするのは「謎」と書きましたが、おそらく「こう」ではないかと思うことがあります。

”Counter += (value1 + value2);”に対して生成されるアセンブルリストは次のようになっていました。

MOVF 0xF2, W    // 0xF2のメモリの値をWレジスタにセット
ADDWF 0xF0, W   // 0xF0のメモリの値をWレジスタに加算
MOVWF 0xF1      // 0xF1のメモリにWレジスタの値を保存
MOVF 0xF1, W    // 0xF1のメモリの値をWレジスタにセット
ADDWF 0xFD, F   // 0XFDのメモリにWレジスタの値を加える。

0xF1が「謎のメモリアクセス」なのですが、

  1. 前半の3行は、代入文の右側「Value1 + Value2」を計算して、0xF1にいったん格納する。
  2. 後半の2行は、0xF1の値を取り出してCounterに加算する。

と考えれば、それほどおかしくはありません。

そしてこの0xF1にいったん格納する処理は一見無駄に見えるのですが、これはuint8_tの計算をしているために無駄に見えているだけなのです。例えば、uint16_tの計算をすると次のようになります。

static uint16_t Counter16 = 0;
void Count16Up2Value(uint16_t value1, uint16_t value2)
{
    Counter16 += (value1 + value2);
}
225:           static uint16_t Counter16 = 0;
226:           void Count16Up2Value(uint16_t value1, uint16_t value2)
227:           {
228:               Counter16 += (value1 + value2);
06F3  0872     MOVF 0xF2, W    ; W <- value2の下位8bit
06F4  0770     ADDWF 0xF0, W   ; W <- W + value1の下位8bit
06F5  00F4     MOVWF 0xF4      ; 0xF4 <- W 下位8bitの計算結果を一時保存
06F6  0873     MOVF 0xF3, W    ; W <- value2の上位8bit
06F7  3D71     ADDWFC 0xF1, W  ; W <= W + value2の上位8bit + 下位8bit計算時の桁上がり
06F8  00F5     MOVWF 0xF5      ; 0xF5 <= W 上位8bitの計算結果を一時保存
06F9  0874     MOVF 0xF4, W    ; W <- 下位8bitの計算結果
06FA  07A0     ADDWF 0xA0, F   ; Counter16の下位8bit <- W + Counter16の下位8bit
06FB  0875     MOVF 0xF5, W    ; W <- 上位8bitの計算結果
06FC  3DA1     ADDWFC 0xA1, F  ; Counter16の上位8bit <- W + Counter16の上位8bit + 下位8bit計算時の桁上がり
229:           }
06FD  0008     RETURN

16bitの加算を行う場合、Wレジスタには下位8bitの計算値が入ったり、上位8bitの計算値が入ったりと忙しいです。そしてそれぞれの計算結果をどこかに保存しなければ適切に処理できません。16bitの計算をする場合一時的なメモリに保存することは必要なのです。

そして、下位8bitの加算と上位8bitの加算が混ざって行われているのでややこしく見えますが、下位8bitに関連する部分に色を付けてみました。色を付けた部分を注意深く観察すると、8bitの”Counter += (value1 + value2);”のアセンブルリストと(メモリ番地の違いを無視すれば)まったく同じです。(そしてCソースコードの引数や変数が異なるためなので、メモリ番地が異なるのは当然です。)

つまり、8bitの計算も16bitの計算も同じようなアルゴリズムを使って楽をしようとした結果、8bitだけを処理する場合は一時的なメモリに保存することが無駄になってしまっている…と考えると謎がスッキリします。

試していませんが、論理演算も同様でしょう。また減算処理が -= の方が短くなることも16bit計算で考えると理解できるかもしれません。

補足2 シフト演算のループを無理やり回避する

シフト演算は「変数 >>= 定数」のコードの方が良いので一時変数に格納してでも使うようにしたほうが良いという結論になりました。

// 普通の記述例
C = A >> 4;

// 命令数や処理時間が短くなる例
uint8_t tmp1 = A;
tmp1 >>= 4;
C = tmp1;

しかしこの場合、Cのソースコードは汚くなりますが作業変数を使わずに無理やりループを回避する方法があります。それは次のようなコードです。

C = (((A >> 1) >> 1) >> 1) >> 1;

一時変数を使わないため、この方法はメモリ使用量も抑えることができます。

しかし、やはりこのコードは汚すぎると思います。シフト回数が長くなると読みづらく保守性も悪いと思います。半年後に見返したときに、「何をやっているのだろう?」となるでしょう。もしこのようなコーディングをするなら、コメントなどに適切な説明を入れるようにしましょう。

コメント