本篇文章講述在學習CSAPP位運算LAB時的一些心得。
移位運算的小技巧
C/C++對於移位運算具有不同的策略,對於無符號數,左右移位為邏輯移位,也就是直接移位;對於有符號數,采用算術移位的方式,即左移仍為直接移位,右移時新產生的位用符號位補足。這種設計的目的是保證右移永遠代表除以二,在不考慮溢出的情況下,左移永遠代表乘以二;這里涉及到的一個規律是,二進制負數的左側實際上有無數個1;二進制正數的左側實際上有無數個0;
此外,當移位的長度大於等於數據的位數時,如果使用一個變量來代表移動的位數,則編譯器會放棄這一移位操作;如果使用一個立即數作為移動的位數,則編譯器會移動到底。
利用算術移位的特性,可以非常簡單的實現一些功能,例如,檢測一個數字是否可以被n位二進制表示成補碼(這不僅僅要求長度要小於n,更要求表示之后的數字大小不能發生變化,如果原本是一個正數,表示之后的符號位必須仍然保持是0):
例一:檢測一個整型數是否能夠被n位二進制數表示
-
int fitsBits(int x, int n) {
-
int r, c;
-
c = 33 + ~n; //c = 33 + ~n = 33 + (-n - 1) = 32 - n
-
r = !(((x << c) >> c) ^ x);
-
return r;
-
}
通過左移相應的位數再移回來看是否發生改變,就能很容易的知道這個數字能否被n位二進制補碼表示。這里涉及到的一個規律是,二進制負數的左側實際上有無數個1。簡單考慮一個四位數字0011,顯然其不能被2位數字表示但能被3位數字表示,當左移兩位再移回來時,由於第二位的1左移后變成了符號位,結果應該是1111,而左移一位在移回來時仍然是0011。
這種方案也可以應用在右移當中用於查看是否發生了舍棄末尾。
例二:位運算實現除以2的n次冪:
-
int divpwr2(int x, int n) {
-
return ((x >> 31) & !!(x ^ ((x >> n) << n))) + (x >> n);
-
}
對於正數,位運算實現除以二的n次冪非常簡單,只需要單純的移位即可;但是對於負數情況則略有不同,例如對於-7 = 1001b,其右移一位的結果是-4 = 1100b;然而實際上結果應該是-3;這是由於移位時末尾被舍去了導致的。也就是說,通過位運算實現的除法永遠是向下取整,然而除法的規則應該是對於正數向下取整而對於負數則要向上取整,因此這里采用(x ^ ((x >> n) << n))的方式檢測是否有某些數字在移位的過程中被忽略,也就是是否不能整除,如果不能整除則進行加一操作;這里還使用了兩次!!操作,是利用了邏輯非的縮位特性實現的。
此外,當我們無法得到0xFFFFFFFF時,也可以利用移位運算生成一個全1數字,((1 << 31) >> 31);
2. !運算的縮位特性
!x = 1當且僅當x=0;否則x = 1;因此可以使用!!x直接實現縮位或的效果。
-
int bang(int x) {
-
return (~((x >> 31) | (((~x) + 1) >> 31))) & 1;
-
}
bang函數通過不含!的運算實現了!操作,從中可以更好的體現出它的縮位特性。
3. 分組運算的技巧(二分——組合)
對於每個數據類型進行的操作是和數據的長度相關的。然而,當我們了解了很多關於數據類型的知識后,我們可以更好的利用運算的特性來加速,例如下面這一問題——不使用循環和條件語句統計一個二進制數中1的個數:
最直觀的想法其實就是,左移一位后檢查末尾是否為1,如果是的話總數就加一,重復31次;然而由於題目中限制不能用循環,如果只是簡單的拆成很多條語句顯然是很滑稽的。這時候可以考慮分組運算,例如下面的算法以四個為一組,分別統計出來個數,再進行加和。
-
int bitCount(int x) {
-
int a = 0x11 | (0x11 << 8);
-
int b = a | (a << 16);
-
int sum = x & b;
-
sum = sum + ((x >> 1) & b);
-
sum = sum + ((x >> 2) & b);
-
sum = sum + ((x >> 3) & b);
-
sum = sum + (sum >> 16);
-
a = 0xF | (0xF << 8);
-
sum = (sum & a) + ((sum >> 4) & a);
-
return ((sum + (sum >> 8)) & 0x3F);
-
}
在加和時實際上也采用了分組的技巧,這種類似於分治的思想成功地把一個O(n)的問題簡化為了O(logn)的問題。
此外還有一例,用單純的邏輯運算和+實現logn:
實際上我們是要找到最高位的1所在的位置即可,所以這是一個搜索問題,對於位運算,除了最簡單的線性搜索之外,比較容易實現的方法就是二分搜索。把二分搜索的方法應用到位運算中,並且注意這里邊始終是優先取高位(因為qr是使用高位得到的),經過5次一定能找到結果。
-
int ilog2(int x) {
-
int exp = 0;
-
int cx = x;
-
int rx = x >> 16;
-
int qr = (!!rx);
-
qr = (qr << 31) >> 31;
-
rx = (qr & rx) | ((~qr) & cx);
-
exp = exp + ((qr & 16) | (~qr & 0));
-
-
cx = rx;
-
rx = rx >> 8;
-
qr = (!!rx);
-
qr = (qr << 31) >> 31;
-
rx = (qr & rx) | (~qr & cx);
-
exp = exp + ((qr & 8) | (~qr & 0));
-
-
cx = rx;
-
rx = rx >> 4;
-
qr = (!!rx);
-
qr = (qr << 31) >> 31;
-
rx = (qr & rx) | (~qr & cx);
-
exp = exp + ((qr & 4) | (~qr & 0));
-
-
cx = rx;
-
rx = rx >> 2;
-
qr = (!!rx);
-
qr = (qr << 31) >> 31;
-
rx = (qr & rx) | (~qr & cx);
-
exp = exp + ((qr & 2) | (~qr & 0));
-
-
cx = rx;
-
rx = rx >> 1;
-
qr = (!!rx);
-
qr = (qr << 31) >> 31;
-
rx = (qr & rx >> 1) | (~qr & cx);
-
exp = exp + ((qr & 1) | (~qr & 0));
-
return exp;
-
}