在看 《Tonc: Whirlwind Tour of ARM Assembly》 的時候學到一個小技巧: 若 n 是有號 32bit 整數, 則 n>>31 相當於 n >= 0 ? 0 : -1。
看完 arithmetic shift 的定義後, 才發現我沒弄懂 arithmetic right shift, 原來是一直補進 signed bit (most significant bit), 當它是負數時, 等於一直補 1 進來。所以, -1 不管右移幾次, 結果仍是 -1, 不是 0。
將這訊息發到 G+ 和 plurk 後, 看到許多人提到 C、C++ 沒定義 signed shift 的行為, 所以用的時候要小心。jserv 提到 compiler 可以加參數改變位元運算的結果。
查看 gcc 的說明, 得知 gcc 有實作 arithmetic right shift, 但看不懂 left shift 的部份。Scott 提到這篇有解釋, 然後我明白了一件事: 關於 C99、C++0x draft 的定義, 我都看不太懂啊 ...。孩子的教育不能等, 要好好學英文。
btw, Louis 提到一個有趣的應用, ((n>>31)^n) - (n>>31) 等同於 abs(n)。拆解這個式子可得知, n 是正數時:
- n>>31 是 0
- n^0 仍是 n
- n - 0 仍是 n
n 是負數時:
- n>>31 是 -1
- -1 以 2 補數表示為 0xFFFFFFFF
- n^(-1) 等同於 1 補數運算
- 最後再減 -1, 得到 2 補數運算的值
順便試著寫了以下的 C code, 再編成 assembly code, 想說看看會差多少:
int abs1(int n) {
return n >= 0 ? n : -n;
}
int abs2(int n) {
if (n >= 0)
return n;
return -n;
}
int abs3(int n) {
return ((n>>31)^n) - (n>>31);
}
結果 abs1 和 abs3 差不多, 以下是 abs1 用 gcc 4.4.3 -O3 編後的結果:
abs1:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
popl %ebp
movl %eax, %edx
sarl $31, %edx
xorl %edx, %eax
subl %edx, %eax
ret
gcc 偷吃步用 arithmetic right shift 避免使用 branch。不知這是針對特例的最佳化, 還是用某種神祕的機制算出來的。附帶一提, 我用不同版本的 gcc 編出來的結果不同, 用 4.5.2 最佳化編譯後, abs1 和 abs3 的結果完全一樣, 只差在用的 register 不同。
另外, 若用 ARM assembly 的話, abs() 可以寫成:
@@ 參數由 r0 傳入, 結果寫到 r0
cmp r0, #0
sublt r0, 0, r0
而用 arithmetic right shift 的版本則是:
eor r1, r0, r0, asr #31
sub r0, r1, r0, asr #31
都是兩個算數指令, 速度應該一樣吧。我沒裝 ARM 開發環境, 應該沒寫錯吧。藉機練習寫看看, 體會指令附加 branch 以及 barrel shifter 的便利之處。指令詳細說明, 見這篇的 "23.3.2. Data instructions"。"r0, asr #31" 那部份是 Op2, 可在同一 cycle 內取得 shift 後的值。