關於C/C++中的位運算技巧


本篇文章講述在學習CSAPP位運算LAB時的一些心得。

  1. 移位運算的小技巧

C/C++對於移位運算具有不同的策略,對於無符號數,左右移位為邏輯移位,也就是直接移位;對於有符號數,采用算術移位的方式,即左移仍為直接移位,右移時新產生的位用符號位補足。這種設計的目的是保證右移永遠代表除以二,在不考慮溢出的情況下,左移永遠代表乘以二;這里涉及到的一個規律是,二進制負數的左側實際上有無數個1;二進制正數的左側實際上有無數個0;

此外,當移位的長度大於等於數據的位數時,如果使用一個變量來代表移動的位數,則編譯器會放棄這一移位操作;如果使用一個立即數作為移動的位數,則編譯器會移動到底。

利用算術移位的特性,可以非常簡單的實現一些功能,例如,檢測一個數字是否可以被n位二進制表示成補碼(這不僅僅要求長度要小於n,更要求表示之后的數字大小不能發生變化,如果原本是一個正數,表示之后的符號位必須仍然保持是0):

例一:檢測一個整型數是否能夠被n位二進制數表示

  1. int fitsBits(int x, int n) {  
  2.     int r, c;  
  3.     c = 33 + ~n; //c = 33 + ~n = 33 + (-n - 1) = 32 - n  
  4.     r = !(((x << c) >> c) ^ x);  
  5.     return r;  
  6. }  

    通過左移相應的位數再移回來看是否發生改變,就能很容易的知道這個數字能否被n位二進制補碼表示。這里涉及到的一個規律是,二進制負數的左側實際上有無數個1。簡單考慮一個四位數字0011,顯然其不能被2位數字表示但能被3位數字表示,當左移兩位再移回來時,由於第二位的1左移后變成了符號位,結果應該是1111,而左移一位在移回來時仍然是0011。

     

    這種方案也可以應用在右移當中用於查看是否發生了舍棄末尾。

    例二:位運算實現除以2的n次冪:

  7. int divpwr2(int x, int n) {  
  8.     return ((x >> 31) & !!(x ^ ((x >> n) << n))) + (x >> n);  
  9. }  

    對於正數,位運算實現除以二的n次冪非常簡單,只需要單純的移位即可;但是對於負數情況則略有不同,例如對於-7 = 1001b,其右移一位的結果是-4 = 1100b;然而實際上結果應該是-3;這是由於移位時末尾被舍去了導致的。也就是說,通過位運算實現的除法永遠是向下取整,然而除法的規則應該是對於正數向下取整而對於負數則要向上取整,因此這里采用(x ^ ((x >> n) << n))方式檢測是否有某些數字在移位的過程中被忽略,也就是是否不能整除,如果不能整除則進行加一操作;這里還使用了兩次!!操作,是利用了邏輯非的縮位特性實現的。

    此外,當我們無法得到0xFFFFFFFF時,也可以利用移位運算生成一個全1數字,((1 << 31) >> 31);

    2.    !運算的縮位特性

    !x = 1當且僅當x=0;否則x = 1;因此可以使用!!x直接實現縮位或的效果。

  10. int bang(int x) {  
  11.     return (~((x >> 31) | (((~x) + 1) >> 31))) & 1;  
  12. }  

    bang函數通過不含!的運算實現了!操作,從中可以更好的體現出它的縮位特性。

    3.    分組運算的技巧(二分——組合)

    對於每個數據類型進行的操作是和數據的長度相關的。然而,當我們了解了很多關於數據類型的知識后,我們可以更好的利用運算的特性來加速,例如下面這一問題——不使用循環和條件語句統計一個二進制數中1的個數:

    最直觀的想法其實就是,左移一位后檢查末尾是否為1,如果是的話總數就加一,重復31次;然而由於題目中限制不能用循環,如果只是簡單的拆成很多條語句顯然是很滑稽的。這時候可以考慮分組運算,例如下面的算法以四個為一組,分別統計出來個數,再進行加和。

  13. int bitCount(int x) {  
  14.     int a = 0x11 | (0x11 << 8);  
  15.     int b = a | (a << 16);  
  16.     int sum = x & b;  
  17.     sum = sum + ((x >> 1) & b);  
  18.     sum = sum + ((x >> 2) & b);  
  19.     sum = sum + ((x >> 3) & b);  
  20.     sum = sum + (sum >> 16);  
  21.     a = 0xF | (0xF << 8);  
  22.     sum = (sum & a) + ((sum >> 4) & a);  
  23.     return ((sum + (sum >> 8)) & 0x3F);  
  24. }  

    在加和時實際上也采用了分組的技巧,這種類似於分治的思想成功地把一個O(n)的問題簡化為了O(logn)的問題。

    此外還有一例,用單純的邏輯運算和+實現logn:

    實際上我們是要找到最高位的1所在的位置即可,所以這是一個搜索問題,對於位運算,除了最簡單的線性搜索之外,比較容易實現的方法就是二分搜索。把二分搜索的方法應用到位運算中,並且注意這里邊始終是優先取高位(因為qr是使用高位得到的),經過5次一定能找到結果。

  25. int ilog2(int x) {  
  26.     int exp = 0;  
  27.     int cx = x;  
  28.     int rx = x >> 16;  
  29.     int qr = (!!rx);  
  30.     qr = (qr << 31) >> 31;  
  31.     rx = (qr & rx) | ((~qr) & cx);  
  32.     exp = exp + ((qr & 16) | (~qr & 0));  
  33.  
  34.     cx = rx;  
  35.     rx = rx >> 8;  
  36.     qr = (!!rx);  
  37.     qr = (qr << 31) >> 31;  
  38.     rx = (qr & rx) | (~qr & cx);  
  39.     exp = exp + ((qr & 8) | (~qr & 0));  
  40.  
  41.     cx = rx;  
  42.     rx = rx >> 4;  
  43.     qr = (!!rx);  
  44.     qr = (qr << 31) >> 31;  
  45.     rx = (qr & rx) | (~qr & cx);  
  46.     exp = exp + ((qr & 4) | (~qr & 0));  
  47.  
  48.     cx = rx;  
  49.     rx = rx >> 2;  
  50.     qr = (!!rx);  
  51.     qr = (qr << 31) >> 31;  
  52.     rx = (qr & rx) | (~qr & cx);  
  53.     exp = exp + ((qr & 2) | (~qr & 0));  
  54.  
  55.     cx = rx;  
  56.     rx = rx >> 1;  
  57.     qr = (!!rx);  
  58.     qr = (qr << 31) >> 31;  
  59.     rx = (qr & rx >> 1) | (~qr & cx);  
  60.     exp = exp + ((qr & 1) | (~qr & 0));  
  61.     return exp;  
  62. }  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM