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が「謎のメモリアクセス」なのですが、
- 前半の3行は、代入文の右側「Value1 + Value2」を計算して、0xF1にいったん格納する。
- 後半の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;
一時変数を使わないため、この方法はメモリ使用量も抑えることができます。
しかし、やはりこのコードは汚すぎると思います。シフト回数が長くなると読みづらく保守性も悪いと思います。半年後に見返したときに、「何をやっているのだろう?」となるでしょう。もしこのようなコーディングをするなら、コメントなどに適切な説明を入れるようにしましょう。
コメント