位運算的奇技淫巧:Bit Twiddling Hacks

原文名:Bit Twiddling Hacks

原文地址:http://graphics.stanford.edu/~seander/bithacks.html

作者:Sean Eron Anderson, [email protected]

本文所包含的代碼片段不受著作權法的限制(除非有特別註明),任何人可以自由使用。本文的收集整理工作由Sean Eron Anderson在1997-2005年完成。希望這篇文章以及這些代碼能幫助到讀者,但是在使用這些代碼時,發生錯誤不提供任何擔保。截止到2005年5月5日,這些代碼也已經被徹底地進行了測試,並且很多人閱讀過這些代碼。除此之外,卡內基梅隆大學計算機科學學院院長Randal Bryant教授使用他的Uclid代碼檢驗系統親自爲大部分代碼進行了測試。對於其他沒有被測試覆蓋到的部分,我在32位計算機上測試了所有可行的輸入。對於第一個在代碼中發現一個合理bug的人,我會懸賞10美元(支票或者Paypal)。如果發現者有意將賞金捐獻給慈善機構,那麼我願意支付20美元。

關於運算次數的統計方法

當討論到計算某個算法的運算次數時,任何一個C語言的運算符都會被統計爲一次運算。中間變量的賦值,即不需要寫入到內存中的賦值操作,不會被統計在內。當然,這種統計方法只能得到綜合機器指令和CPU時間的一個近似值。影響一段程序在系統中的運行時間的因素非常多,比如緩存大小,內存帶寬,不同的指令集等等。所有運算消耗的時間相同在現實中是不成立的,但是CPU技術隨着時間的推移,正在往這個方向飛速發展。總的來說,想要判斷一種方法比另一種方法更快,最好的方式是直接到你的目標機器上去跑基準測試,測試性能的優異。

計算整數的符號

int v;      // we want to find the sign of v
            // 我們希望得出v的符號(正負)
int sign;   // the result goes here
            // 結果保存在這個變量裏

// CHAR_BIT is the number of bits per byte (normally 8).
// 常量CHAR_BIT指是一個比特里包含多少位(通常情況下是8位)
sign = -(v < 0);  // if v < 0 then -1, else 0.
// or, to avoid branching on CPUs with flag registers (IA32):
// 或者,爲了防止在有標誌寄存器的CPU(Intel32位X86架構)上出現分支指令
sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
// or, for one less instruction (but not portable):
// 或者,犧牲移植性來減少一個指令
sign = v >> (sizeof(int) * CHAR_BIT - 1);

譯者注:IntelX86架構的比較指令(cmp)通常與條件跳轉語句配合使用。參考鏈接:關於這段代碼爲何能夠防止出現分支指令的討論

對於32位整型數來說,上面的最後一條語句會計算sign=v>>31。這樣的方式比sign=-(v<0)這種直接的方式要快一次運算左右。由於右移時,最左端的符號位會被填充到多出來的位中,所以在這個技巧(指v>>31)能夠工作。如果最左端的符號位是1,那麼結果就是-1;否則就是0。因爲右移時,負數的所有位都會被填充爲1,而二進制位全1正好是是-1的補碼。不過不幸的是,這個操作是依賴底層實現的(所以是說犧牲了移植性)。

譯者注:關於右移操作自動填充符號位的討論

也許你可能更喜歡,對於正數返回1,對於負數返回-1,那麼有:

sign = +1 | (v >> (sizeof(int) * CHAR_BIT - 1));  // if v < 0 then -1, else +1

更或者,還有對於負數零正數而返回-1, 0, 1的方案,那麼有:

sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
// Or, for more speed but less portability:
// 或者,犧牲移植性來提升速度
sign = (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1));  // -1, 0, or +1
// Or, for portability, brevity, and (perhaps) speed:
// 或者,更易移植,更加簡潔,或者(有可能)更快的方案
sign = (v > 0) - (v < 0); // -1, 0, or +1

反之,如果你希望對於負數返回0,非負數返回+1,那麼有:

sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); // if v < 0 then 0, else 1

附加說明:

2003年3月7日,Augus Duggan指出1989 ANSI C標準指明帶符號數右移的結構是由編譯器實現時定義(implementation-defined)的,所以這個技巧有可能不會正常工作。

2005年9月28日,Toby Speight爲了提高移植性,他提議使用CHAR_BIT常量表示比特的長度,而不是簡單地假設比特長度是8位。

2006年3月4日,Augus提出了幾種更具移植性的代碼版本,包括類型轉換。

2009年9月12日,Rohit Gary提出了集中支持非負數的代碼版本。

判斷兩整數符號是否相反

int x, y;               // input values to compare signs
                        // 輸入的數值放在變量x和y中,用於比較符號
bool f = ((x ^ y) < 0); // true iff x and y have opposite signs
                        // 當且僅當(iff) x和y的符號相反時返回true

2009年11月26日,Manfred Weis建議我加入這一條內容。

計算整數的絕對值(不使用分支指令)

int v;           // we want to find the absolute value of v
                 // 我們希望算出變量v的絕對值
unsigned int r;  // the result goes here
                 // 結果保存在這裏
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

一個簡單的變形:

r = (v ^ mask) - mask;

有些CPU並不支持計算整數絕對值的指令(也可以說有些編譯器沒用上這些指令)。在有的機器上,分支判斷操作非常昂貴,會消耗較多計算資源。在這些機器上,上面的表達式會比 r = (v < 0) ? -(unsigned)v : v 這種簡單的實現更快一些,儘管他們的操作數都是相同的。

2003年3月7日,Augus Duggan指出1989 ANSI C標準指明帶符號數右移的結構是由編譯器實現時定義(implementation-defined)的,所以這個技巧有可能不會正常工作。同時,我也閱讀了ANSI C標準,發現ANSI C並沒有要求數值一定要以二補數(two’s complement,即補碼)的形式表示出來,所以由於這個原因,上面的技巧(在一些極少部分仍使用一補數(one’s complement)的古董機器上)也有可能不工作。

2004年3月14日,Keith H. Duggar提出了上面的變形。這個版本比我一開始想出來的初始版本更好,r=(+1|(v>>(sizeof(int)*CHAR_BIT-1)))*v,其中有一次乘法是沒用的。

不幸的是,2000年6月6日,這個技巧已經被Vladimir Yu Volkonsky在美國申請了專利,並且歸屬於Sun公司的Microsystems
2006年8月13日,Yuriy Kaminskiy告訴我這個專利可能是無效的,因爲這個技巧在申請專利之前就被人公開發表了,見1996年11月9日,由Agner Fog發表的How to Optimize for the Pentium Processor。Yuriy同時也提到這份文檔在1997年被翻譯成了俄語,所以Vladimir有可能閱讀過。除此之外,The Internet Archive(網站時光倒流機器)網站也收錄了這個老舊的鏈接。

2007年1月30日,Peter Kankowski給我分享了一個他的發現。這來源於他在觀察微軟的Visual C++編譯器的輸出時的發現。這個技巧在這裏被採用爲最優解法。

(譯者注,Peter發現了VC++的編譯器有可能使用了之前那個被Sun公司專利保護的技巧,但在評論中也同時有人指出Sun公司的這個專利是無效的)

2007年12月6日,Hai Jin提出反對意見,算法的結果是帶符號的,所以在計算最大的負數時,結果會依然是負的。

2008年4月15日,Andrew Shapira指出上面的那個簡單實現的版本可能會溢出,需要一個(unsigned)來做強制類型轉換;爲了最大程度的兼容性,他提議使用(v < 0) ? (1 + ((unsigned)(-1-v))) : (unsigned)v。但是根據2008年7月9日的ISO C99標準,Vincent Lefèvre說服我刪除了這個版本,因爲即便是在非基於二補數的機器上,-(unsigned)v這條語句也會做我們希望他做的事情。在計算-(signed)v時,程序會通過將負數v增加2**N來得到無符號類型的數,這個數正好是v的補碼錶示形式,我們令U等於這個數。然後將U的符號取負,就能得出結果,有-U=0-U=2**N-U=2**N-(v+2**N)=-v=abs(v)。

計算兩個整數之間的最大值和最小值(不使用分支指令)

int x;  // we want to find the minimum of x and y
int y;
        // 我們希望找出x和y之間的最小值
int r;  // the result goes here
        // 結果保存在這裏
r = y ^ ((x ^ y) & -(x < y)); // min(x, y)

這個技巧能工作的原因是當x<y, 那麼-(x<y)數值的二進制補碼會是全1(-1的補碼是全1),所以r = y ^ (x ^ y) & ~0 = y ^ x ^ y = x。反之,如果x>=y,那麼-(x<y)會是全0,所以r = y ^ ((x ^ y) & 0) = y。在有些分支操作非常昂貴的機器,和沒有提供條件跳轉指令的機器上,上面的技巧會比這種常見的寫法更快一些:r = (x < y) ? x : y,儘管這種常見的寫法只使用了兩三個指令。(雖然通常來講,這種簡單實現是最好的)。需要注意的是,在有的機器上,計算x<y的值也需要使用分支指令,所以這個時候這個技巧對比普通的實現也沒有任何優勢。

如果需要計算最大值,那麼有

r = x ^ ((x ^ y) & -(x < y)); // max(x, y)

快但是有缺陷(dirty)的版本:

如果事先知道INT_MIN <= x - y <= INT_MAX(譯者注:不會溢出),那麼你就可以使用以下技巧。由於(x-y)只需要計算一次,所以這個版本會更快一些。

r = y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); // min(x, y)
r = x - ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); // max(x, y)

注意,1989年的ANSI C標準並沒有指明帶符號類型變量的右移行爲,所以這個版本不具備兼容性。如果計算時由於溢出而導致拋出異常,x和y的值都應該是無符號型的或者被強制轉換成無符號型的,來避免由於減法而導致不必要地拋出異常。然而,當進行右移操作是需要用強制類型轉換,將數值轉換成帶符號的,這樣才能根據數值的正負來產生全0和全1。

2003年3月7日,Angus Duggan指出了右移操作的兼容性問題。
2005年5月3日,Randal E.Bryant提示我只有在INT_MIN <= x - y <= INT_MAX的先決條件下,那個炫酷版本的代碼纔算完善,並且他還提出了之前那個較樸實的解法。這些問題都需要在炫酷版本的代碼中考慮到。
2005年7月6日,Nigel Horspoon注意到gcc在一款奔騰處理器上編譯這份代碼時,由於其計算(x-y)的方式,而產生了和之前的簡單寫法相同的代碼。
2008年7月9日,Vincent Lefèvre指出上一個版本中,即r = y + ((x - y) & -(x < y)),存在減法溢出的潛在風險。
2009年6月2日,Timothy B. Terriberry建議使用異或來代替加減以避免強制類型轉換和溢出的風險。

判斷某個整數是不是2的次冪

unsigned int v; // we want to see if v is a power of 2
                // 判斷變量v是否是2的次冪
bool f;         // the result goes here
                // 結果保存在這裏

f = (v & (v - 1)) == 0;

注意,0也是2的冪,但運算髮生錯誤。爲了更嚴謹一些,有:

f = v && !(v & (v - 1));

符號擴展

符號擴展(固定位長)

符號擴展(sign extension)是系統內建的自動機制,比如char型和int型之間的互相轉換。但當你有一個帶符號長度爲b位的補碼數x,你想要把x轉換爲長度超過b位的int型時,這個機制就不能滿足需求了。誠然,簡單賦值對於正數x是有效的,但是負數x都不行了,因爲符號位需要被擴展。舉個例子來簡單說明什麼是符號擴展(sign extension),我們現在有一個4位長的變量來保存數,對於-3來說,保存下來的補碼形式爲1101。如果我們有8位長,那麼-3保存下來的補碼形式爲11111101。當嘗試將一個4位長的數轉換爲更多位長的數時,符號位會向左複製填充空出來的位,直到填滿。在C語言中,使用結構體或聯合體的位域很容易實現固定長度的符號擴展。比如,將長度爲5位的數轉換成整型。

int x; // convert this from using 5 bits to a full int
       // 變量x中保存長度爲5位的數
int r; // resulting sign extended number goes here
       // 轉換的結果保存在變量r中
struct {signed int x:5;} s;
r = s.x = x;

下面的C++模版函數使用了同樣的語言特性通過一次操作來轉換長度爲B的數到整型(當然,編譯器會生成更多代碼)。

template <typename T, unsigned B>
inline T signextend(const T x)
{
  struct {T x:B;} s;
  return s.x = x;
}

int r = signextend<signed int,5>(x);  // sign extend 5 bit number x to r
                                      // 從5位長的數x轉換到r

2005年5月2日,John Byrd找到了一處由於html格式問題導致的樣式顯示錯誤。

2006年3月4日,Pat Wood指出ANSI C標準規定帶符號的位域必須要用關鍵字“signed”來顯式地指定其帶符號,否則其符號位是未定義的。

符號擴展(可變位長)

有時,我們可能事先不知道位的長度,來完成符號擴展,上面的技巧就失效了。(也有可能是在某些不提供位域功能的編程語言,如Java)

unsigned b; // number of bits representing the number in x
            // 變量b指定需要擴展的位長
int x;      // sign extend this b-bit number to r
            // 需要將變量x中的數值符號擴展的結果保存到r中
int r;      // resulting sign-extended number
            // 存放計算結果到變量r
int const m = 1U << (b - 1); // mask can be pre-computed if b is fixed
                             // 如果b是常量,那麼這個掩碼可以被預處理

x = x & ((1U << b) - 1);  // (Skip this if bits in x above position b are already zero.)
                          // (如果超過b位的部分都已經是0了,那麼這步可以跳過)
r = (x ^ m) - m;

這段代碼需要四次操作,但當位長是常量時,假設高位部分都已經清零了,那麼這個技巧只需要兩次操作。

還有一個更快但是略微損失移植性的方法,這個方法不需要假設位長度超過b的部分,即高位部分,都已經被清零:

int const m = CHAR_BIT * sizeof(x) - b;
r = (x << m) >> m;

2004年6月13日,Sean A. Irvine建議我將符號擴展的方法添加進這個頁面。同時他提供了這段代碼m = (1 << (b - 1)) - 1; r = -(x & ~m) | x。後來我在這份代碼的基礎上,優化出了m = 1U << (b - 1); r = -(x & m) | x這個版本。

但是在2007年5月11日,Shay Green提出了上面的這個比我少一個操作的版本。

2008年10月15日,Vipin Sharma 建議我考慮增加一個步驟來解決如果x在除了b位長之外的二進制部分還存在1的情況。

2009年12月31日,Chris Pirazzi建議我增加目前最快的版本,這個版本對於固定位長的符號擴展,只需要2次操作;對於變長的,也只需要3次操作。

使用3次運算的符號擴展(可變位長)

這個技巧由於乘法和除法的關係,在某些機器上可能會慢一些。這個版本準確來說需要4次運算。如果你知道你的初始位長大於1的話,那麼你就可以用r = (x * multipliers[b]) / multipliers[b]這種方法來完成符號擴展。這個技巧是基於一個事先初始化的表,它只需要3次操作。

unsigned b; // number of bits representing the number in x
            // 變量b指定需要擴展的位長
int x;      // sign extend this b-bit number to r
            // 需要將變量x中的數值符號擴展的結果保存到r中
int r;      // resulting sign-extended number
            // 存放計算結果到變量r
#define M(B) (1U << ((sizeof(x) * CHAR_BIT) - B)) // CHAR_BIT=bits/byte
                                                  // CHAR_BIT是指一個字節中有多少位
static int const multipliers[] =
{
  0,     M(1),  M(2),  M(3),  M(4),  M(5),  M(6),  M(7),
  M(8),  M(9),  M(10), M(11), M(12), M(13), M(14), M(15),
  M(16), M(17), M(18), M(19), M(20), M(21), M(22), M(23),
  M(24), M(25), M(26), M(27), M(28), M(29), M(30), M(31),
  M(32)
}; // (add more if using more than 64 bits)
   // (如果需要支持到64位的話,可以繼續添加)
static int const divisors[] =
{
  1,    ~M(1),  M(2),  M(3),  M(4),  M(5),  M(6),  M(7),
  M(8),  M(9),  M(10), M(11), M(12), M(13), M(14), M(15),
  M(16), M(17), M(18), M(19), M(20), M(21), M(22), M(23),
  M(24), M(25), M(26), M(27), M(28), M(29), M(30), M(31),
  M(32)
}; // (add more for 64 bits)
   // (繼續添加以支持64位)
#undef M
r = (x * multipliers[b]) / divisors[b];

下面這個變種可能兼容性不高,但在某些支持算術右移架構,可以保持符號位的系統上,這個變種會更快一些。

const int s = -b; // OR:  sizeof(x) * CHAR_BIT - b;
                  // 或者:sizeof(x) * CHAR_BIT - b;
r = (x << s) >> s;

2005年3月3日,Randal E.Bryant指出了一個最初版本的bug(即使用查表的版本),當x和b都爲1時,這個技巧就會失效。

帶條件判斷的設置位或清除位(不使用分支指令)

bool f;         // conditional flag
                // 使用這個標誌來表示條件判斷
unsigned int m; // the bit mask
                // 位掩碼
unsigned int w; // the word to modify:  if (f) w |= m; else w &= ~m;
                // 需要進行操作的變量

w ^= (-f ^ w) & m;

// OR, for superscalar CPUs:
// 在一些超標量架構的CPU上,也可以這樣:
w = (w & ~m) | (-f & m);

在某些架構上,不使用分支指令會比使用分支指令多出2個甚至更多的操作。舉個例子,通過非正式速度測試表明,AMD Athlon™ XP 2100+能快5-10%; Intel Core 2 Duo的超標量版本能比能比前一個快16%。
2003年12月11日,Gelnn Slayden告訴了我第一個算法。
2007年4月3日,Marco Yu給我分享了超標量版本的算法,在兩天後給我提出了一處顯示排版錯誤。

帶條件判斷的將變量置爲相反數(不使用分支指令)

在不使用分支指令的情況下,你可能會需要判斷某個flag是否false,來將某個變量置爲其相反數:

bool fDontNegate;  // Flag indicating we should not negate v.
                   // Flag標誌,用於判斷我們是否需要將變量v置爲相反數
int v;             // Input value to negate if fDontNegate is false.
                   // 輸入的數值保存在v中,當fDontNegate爲false時,就將變量v置爲相反數
int r;             // result = fDontNegate ? v : -v;

r = (fDontNegate ^ (fDontNegate - 1)) * v;

如果flag爲true纔將變量置爲相反,那麼可以用這個:

bool fNegate;  // Flag indicating if we should negate v.
               // Flag標誌,用於判斷我們是否需要將變量v置爲相反數
int v;         // Input value to negate if fNegate is true.
               // 輸入的數值保存在v中,當fDontNegate爲true時,就將變量v置爲相反數
int r;         // result = fNegate ? -v : v;

r = (v ^ -fNegate) + fNegate;

2009年6月2日,Avraham Plotnitzky建議我添加第一個版本。

2009年6月8日,爲了去除掉乘法,我想出了第二個版本。

2009年11月26日,Alfonso De Gregorio指出某個地方缺少括號。這是一個合理的bug,所以它得到了指出bug的賞金。

根據掩碼對兩個數值進行位合併

unsigned int a;    // value to merge in non-masked bits
                   // 將變量a中沒被掩碼覆蓋的位保留下來
unsigned int b;    // value to merge in masked bits
                   // 將變量b中被掩碼覆蓋的位保留下來
unsigned int mask; // 1 where bits from b should be selected; 0 where from a.
                   // 如果某一位是1,那麼結果中對應的位就保存b所對應位置的值;如果是0,則保存a所對應位置的值。
unsigned int r;    // result of (a & ~mask) | (b & mask) goes here
                   // 這裏保存(a & ~mask) | (b & mask)的結果

r = a ^ ((a ^ b) & mask);

這個算法比這種簡單的實現`(a & ~mask) | (b & mask)節省一次操作。然而如果掩碼是一個常量,那麼這兩種算法實際上都差不多。

2006年2月9日,Ron Jeffery將這個算法發給我了。

統計二進制位

統計二進制位中1的個數(普通實現)

unsigned int v; // count the number of bits set in v
                // 計算變量v的二進制中1的個數
unsigned int c; // c accumulates the total bits set in v
                // 保存計算的結果

for (c = 0; v; v >>= 1)
{
  c += v & 1;
}

這個簡單算法對於每一位都需要一次操作,直到結束。所以對於32位字長,且只有最高位爲1時(即最壞情況),這個算法會操作32次。

統計二進制位中1的個數(查表法)

static const unsigned char BitsSetTable256[256] =
{
#   define B2(n) n,     n+1,     n+1,     n+2
#   define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2)
#   define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2)
    B6(0), B6(1), B6(1), B6(2)
};

unsigned int v; // count the number of bits set in v
                // 計算變量v的二進制中1的個數
unsigned int c; // c accumulates the total bits set in v
                // 保存計算的結果
// Option 1:
// 第一種:
c = BitsSetTable256[v & 0xff] +
    BitsSetTable256[(v >> 8) & 0xff] +
    BitsSetTable256[(v >> 16) & 0xff] +
    BitsSetTable256[v >> 24];

// Option 2:
// 第二種:
unsigned char * p = (unsigned char *) &v;
c = BitsSetTable256[p[0]] +
    BitsSetTable256[p[1]] +
    BitsSetTable256[p[2]] +
    BitsSetTable256[p[3]];


// To initially generate the table algorithmically:
// 使用算法來預處理表的內容
BitsSetTable256[0] = 0;
for (int i = 0; i < 256; i++)
{
  BitsSetTable256[i] = (i & 1) + BitsSetTable256[i / 2];
}

2009年7月14日,Hallvard Furuseth提出了宏壓縮版本的預處理表的方法。

統計二進制位中1的個數(Brian Kernighan方法)

unsigned int v; // count the number of bits set in v
                // 計算變量v的二進制中1的個數
unsigned int c; // c accumulates the total bits set in v
                // 保存計算的結果
for (c = 0; v; c++)
{
  v &= v - 1; // clear the least significant bit set
              // 清除掉從最低位到最高位數的第一個爲1的位
}

Brian Kernighan的方法運算次數取決於二進制位中1的個數。所以如果一個32位字長的數,只有最高位是1,那麼這個算法只會執行1次。

1988年,發佈於《C程序設計語言》(第二版),作者Brian W. Kernighan和Dennis M. Ritchie。在此書的練習2-9中提到了這個算法。

2006年4月19日,Don Knuth向我指出這個算法,“是被Peter Wegner首先在CACM 3 (1960), 322發表的”。(同時也被Derrick Lehmer獨立發現,並且在1964年由Beckenbach編輯發表在一本書上)

統計14位字長,24位字長,32位字長的二進制位中1的個數(64位架構下)

unsigned int v; // count the number of bits set in v
                // 計算變量v的二進制中1的個數
unsigned int c; // c accumulates the total bits set in v
                // 保存計算的結果

// option 1, for at most 14-bit values in v:
// 第一種情況,只計算統計變量v中的14位
c = (v * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;

// option 2, for at most 24-bit values in v:
// 第二種情況,只計算統計變量v中的24位
c =  ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL)
     % 0x1f;

// option 3, for at most 32-bit values in v:
// 第三種情況,只計算統計變量v中的32位
c =  ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) %
     0x1f;
c += ((v >> 24) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;

這個算法需要在支持快速模除的64位CPU上才能達到高性能的效果。第一種情況只需要3次操作,第二種需要10次,第三種需要15次。

Rich Schroeppel最初想出了一個和第一種類似的9位長版本,見Programming Hacks的這一章節Beeler, M., Gosper, R. W., and Schroeppel, R. HAKMEM. MIT AI Memo 239, Feb. 29, 1972。他的想法是收此啓發,並最終由Sean Anderson完成設計。

2005年5月3日,Randal E.Byrant提了幾個bug修復補丁。

2007年2月1日,Bruce Dawson對原來的12位版本做了一些調整,將其變成了兼容性更好的14位版本,並且保持操作數不變。

統計二進制位中1的個數(並行計算的方法)

unsigned int v; // count the number of bits set in v
                // 計算變量v的二進制中1的個數
unsigned int c; // c accumulates the total bits set in v
                // 保存計算的結果
static const int S[] = {1, 2, 4, 8, 16}; // Magic Binary Numbers
static const int B[] = {0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF, 0x0000FFFF};

c = v - ((v >> 1) & B[0]);
c = ((c >> S[1]) & B[1]) + (c & B[1]);
c = ((c >> S[2]) + c) & B[2];
c = ((c >> S[3]) + c) & B[3];
c = ((c >> S[4]) + c) & B[4];

B數組,以及其二進制的形式如下:

B[0] = 0x55555555 = 01010101 01010101 01010101 01010101
B[1] = 0x33333333 = 00110011 00110011 00110011 00110011
B[2] = 0x0F0F0F0F = 00001111 00001111 00001111 00001111
B[3] = 0x00FF00FF = 00000000 11111111 00000000 11111111
B[4] = 0x0000FFFF = 00000000 00000000 11111111 11111111

通過添加兩個幻數數組B和S,就能夠擴展這個方法,以適應位長更多的整數類型。如果有k位的話,那麼我們只需要把數組S和B擴展到ceil(lg(k))個元素就好,同時添加對應數量的計算c的表達式。對於32位長度的v來說,一共需要16次操作。

然而對於計算32位整型數來說,最好的計算方法下面這種:

v = v - ((v >> 1) & 0x55555555);                    // reuse input as temporary
                                                    // 將輸入變量作爲臨時變量重複使用
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);     // temp
                                                    // 臨時變量
c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count
                                                    // 計算結果

這種計算方法只需要12次操作,雖然和上面查表的方法差不多,但是卻能夠節省了內存和避免了潛在的緩存未命中而導致的額外操作。這是在並行計算方法和之前使用乘法的那種方法(在64位架構下,二進制位中1的個數那一小節中)之間的結合,然而這個方法卻不需要64位架構的指令。每個比特中的1統計可以並行的計算,最終的結果計算是通過乘以0x1010101後右移24位來得出的。

這個方法還有一個推廣,可以計算長度多達128位的整型數(128位整型的數據類型使用T來代替),如下:

v = v - ((v >> 1) & (T)~(T)0/3);                           // temp
v = (v & (T)~(T)0/15*3) + ((v >> 2) & (T)~(T)0/15*3);      // temp
v = (v + (v >> 4)) & (T)~(T)0/255*15;                      // temp
c = (T)(v * ((T)~(T)0/255)) >> (sizeof(T) - 1) * CHAR_BIT; // count

Ian Ashdown’s nice newsgroup post還可以看到更多關於計算二進制位中1個數(也被人稱爲sideways addition)的相關信息。

2005年12月14日,Charlie Gordon提出了一種方法,可以讓純平行計算的版本減少一次操作。2005年12月30日,Don Clugston在此之上又優化掉了3次操作。

2006年1月8日,Eric Cole指出了我在按照Don的建議修改本文時留下的一處顯示錯誤。

2006年11月17日,Eric提出了最好計算方法的可變位長推廣方案。

2007年4月5日,Al Williams發現我在第一個方法中留下了一行無用的代碼。

統計從最高位到指定的某位之間的二進制位1的個數

這個方法是用來計算某一位的rank,意思是統計從最高位到指定的某位之間二進制位1的個數

uint64_t v; // Compute the rank (bits set) in v from the MSB(最高位) to pos.
            // 計算v中從第pos位到最高位的rank(二進制位1的個數)
unsigned int pos; // Bit position to count bits upto.
                  // 指定某一位,向最高位統計
uint64_t r; // Resulting rank of bit at pos goes here.
            // 保存統計的結果

// Shift out bits after given position.
// 將其餘的位右移出去
r = v >> (sizeof(v) * CHAR_BIT - pos);
// Count set bits in parallel.
// 並行地統計1的個數
// r = (r & 0x5555...) + ((r >> 1) & 0x5555...);
r = r - ((r >> 1) & ~0UL/3);
// r = (r & 0x3333...) + ((r >> 2) & 0x3333...);
r = (r & ~0UL/5) + ((r >> 2) & ~0UL/5);
// r = (r & 0x0f0f...) + ((r >> 4) & 0x0f0f...);
r = (r + (r >> 4)) & ~0UL/17;
// r = r % 255;
r = (r * (~0UL/255)) >> ((sizeof(v) - 1) * CHAR_BIT);

2009年11月21日,Juha Järvi將這個算法發給了我,這個算法是下一個算法(給定從某位到最高位1的個數,推算出該位的位置)的逆運算。

給定從某位到最高位1的個數,推算該位的位置

接下來這份64位的代碼可以選取出從左到右第r個二進制1的位置。也就是說,如果我們從最高位往右,統計二進制位爲1的個數,直到達到了預期的rank(譯者:解釋見上一條),那麼我們停下的位置就是答案。如果超出了最低位還沒有算出結果,那麼會返回64。這段代碼可以改編出32位版本,也可以從最右邊開始統計。

// Do a normal parallel bit count for a 64-bit integer,
// 並行地統計出二進制1的個數
 // but store all intermediate steps.
 // 保存所有的中間結果
 // a = (v & 0x5555...) + ((v >> 1) & 0x5555...);
 a =  v - ((v >> 1) & ~0UL/3);
 // b = (a & 0x3333...) + ((a >> 2) & 0x3333...);
 b = (a & ~0UL/5) + ((a >> 2) & ~0UL/5);
 // c = (b & 0x0f0f...) + ((b >> 4) & 0x0f0f...);
 c = (b + (b >> 4)) & ~0UL/0x11;
 // d = (c & 0x00ff...) + ((c >> 8) & 0x00ff...);
 d = (c + (c >> 8)) & ~0UL/0x101;
 t = (d >> 32) + (d >> 48);
 // Now do branchless select!
 // 這裏進行無分支指令的條件選取
 s  = 64;
 // if (r > t) {s -= 32; r -= t;}
 s -= ((t - r) & 256) >> 3; r -= (t & ((t - r) >> 8));
 t  = (d >> (s - 16)) & 0xff;
 // if (r > t) {s -= 16; r -= t;}
 s -= ((t - r) & 256) >> 4; r -= (t & ((t - r) >> 8));
 t  = (c >> (s - 8)) & 0xf;
 // if (r > t) {s -= 8; r -= t;}
 s -= ((t - r) & 256) >> 5; r -= (t & ((t - r) >> 8));
 t  = (b >> (s - 4)) & 0x7;
 // if (r > t) {s -= 4; r -= t;}
 s -= ((t - r) & 256) >> 6; r -= (t & ((t - r) >> 8));
 t  = (a >> (s - 2)) & 0x3;
 // if (r > t) {s -= 2; r -= t;}
 s -= ((t - r) & 256) >> 7; r -= (t & ((t - r) >> 8));
 t  = (v >> (s - 1)) & 0x1;
 // if (r > t) s--;
 s -= ((t - r) & 256) >> 8;
 s = 65 - s;

如果在你的CPU上分支指令速度足夠快,可以考慮將使用被註釋掉的那些if語句,將對應的其它語句註釋掉。

2009年11月21日,Juha Järvi將這個發給了我。

計算奇偶校驗位(給定位數的二進制數中1的個數是奇數還是偶數)

計算奇偶校驗位(普通實現)

unsigned int v;       // word value to compute the parity of
                      // 需要計算的值保存在變量v中
bool parity = false;  // parity will be the parity of v
                      // 變量parity保存v的奇偶校驗位
while (v)
{
  parity = !parity;
  v = v & (v - 1);
}

上面這段代碼實現使用了類似Brian Kernigan的統計二進制位中1個數的方法。二進制中有多少個1,這個算法就會計算多少次。

計算奇偶校驗位(查表法)

static const bool ParityTable256[256] =
{
#   define P2(n) n, n^1, n^1, n
#   define P4(n) P2(n), P2(n^1), P2(n^1), P2(n)
#   define P6(n) P4(n), P4(n^1), P4(n^1), P4(n)
    P6(0), P6(1), P6(1), P6(0)
};

unsigned char b;  // byte value to compute the parity of
                  // 需要計算的值保存在變量b中
bool parity = ParityTable256[b];

// OR, for 32-bit words:
// 或者,32位字長下
unsigned int v;
v ^= v >> 16;
v ^= v >> 8;
bool parity = ParityTable256[v & 0xff];

// Variation:
// 變種
unsigned char * p = (unsigned char *) &v;
parity = ParityTable256[p[0] ^ p[1] ^ p[2] ^ p[3]];

2005年5月3日,Randal E.Bryant提出了使用變量p的那個變種版本。

2005年9月27日,Bruce Rawles發現了表中有一處變量名拼寫錯誤,並獲得了10美刀的獎勵。

2006年10月9日,Fabrice Bellard提出了32位字長的變種,這個變種只需要查表一次;最初的版本需要4次查表(每個字節一次),明顯更慢一些。

2009年7月14日,Hallvard Furuseth提出用宏來精簡表的長度。

計算單個字節的奇偶校驗位(使用64位的乘法和模除)

unsigned char b;  // byte value to compute the parity of
                  // 需要計算的值保存在變量b中
bool parity =
  (((b * 0x0101010101010101ULL) & 0x8040201008040201ULL) % 0x1FF) & 1;

這個方法只需要4次操作,然而只能計算單個字節。

計算單個字的奇偶校驗位(使用乘法)

這個方法計算32位字長的值在使用乘法的情況下,只需要8次操作。

unsigned int v; // 32-bit word
                // 32位長度的字
v ^= v >> 1;
v ^= v >> 2;
v = (v & 0x11111111U) * 0x11111111U;
return (v >> 28) & 1;

對於64位,仍只需要8次操作。

unsigned long long v; // 64-bit word
                      // 64位長度的字

v ^= v >> 1;
v ^= v >> 2;
v = (v & 0x1111111111111111UL) * 0x1111111111111111UL;
return (v >> 60) & 1;

2007年9月2日,Andrew Shapira想出的這個算法,併發給了我。

計算奇偶校驗位(並行計算)

unsigned int v;  // word value to compute the parity of
                 // 需要計算奇偶校驗位的字長度的值
v ^= v >> 16;
v ^= v >> 8;
v ^= v >> 4;
v &= 0xf;
return (0x6996 >> v) & 1;

這個方法需要9次運算,可以工作在32位字長的環境下。如果是隻需要對字節進行計算,那麼可以把“unsigned int v;”的下兩行去掉,這樣可以把操作數優化到5次。這個方法先是將這個32位值的分成8個半字節,通過右移和異或將v壓縮到v的最低的半字節中。然後將二進制位0110 1001 1001 0110(十六位表示爲0x6996)的數值右移,右移的位數是剛剛計算出來的v的最低半字節的值。這個幻數就像是一個16位的小型奇偶校驗位的表,通過v的最低半字節的值可以查到v的奇偶校驗位。最終結果存放在最低位中,代碼最後通過掩碼的方式計算出了結果並返回。

2002年12月15日,感謝Mathew Hendry提出了右移查表的想法。相比只使用右移和異或的方法,這個優化可以減少掉節省兩次操作。

數值交換

交換數值(使用加法和減法)

#define SWAP(a, b) ((&(a) == &(b)) || \
                    (((a) -= (b)), ((b) += (a)), ((a) = (b) - (a))))

這個交換的方法不使用臨時變量。一開始有一個檢查變量a和變量b在內存中的位置是否相同,如果你能確保這種情況不會發生,那麼這個檢查可以去掉。(編譯器可能也會把這個給優化掉)如果程序有溢出時拋異常的機制,那麼傳入無符號型的值就不會拋異常了。待會兒會介紹一個使用異或的方法,這個方法在某些機器上可能會稍微快一些。注意這個方法不能應用在浮點數的交換上(除非你就是想使用他們的整數形式)。

交換數值(使用異或)

#define SWAP(a, b) (((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b)))

2005年1月20日,Iain A. Fleming指出如果我們交換的數值在內存中的地址相同,這個宏不會起作用,比如SWAP(a[i], a[j]),i == j。所以,如果那種情況可能發生,可以考慮增加一個判斷,就像這樣 (((a) == (b)) || (((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b))))。

2009年7月14日,Hallvard Furuseth建議,在有些機器上可能這條語句會更快一點(((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b))),因爲(a) ^ (b)這條表達式被再利用了(譯者注:意思應該是省去了重複計算的步驟)。

指定範圍,交換數值的二進制位(使用異或)

unsigned int i, j; // positions of bit sequences to swap
                   // 指定交換的位置
unsigned int n;    // number of consecutive bits in each sequence
                   // 區間的長度
unsigned int b;    // bits to swap reside in b
                   // 變量b中的二進制位需要交換
unsigned int r;    // bit-swapped result goes here
                   // 變量r存放位交換後的結果

unsigned int x = ((b >> i) ^ (b >> j)) & ((1U << n) - 1); // XOR temporary
                                                          // 異或操作的臨時變量
r = b ^ ((x << i) | (x << j));

舉一個 指定二進制位範圍來交換數值 的例子,我們有b = 00101111(二進制形式),希望交換的位長度爲n = 3,起始點是i = 1(從右往左數第2個位)的連續3個位,以及起點爲j = 5的連續3個位;那麼結果就會是r = 11100011(二進制)。

這個交換數值的技巧很像之前那個通用的異或交換的技巧,區別於這個技巧是用來操作特定的某些位。變量x中保存我們想要交換的兩段二進制位值異或後的結果,然後用x與原來的值進行異或,便可以達到交換的效果。當然如果指定的範圍溢出了的話,計算結果是未定義的。

2009年7月14日,Hallvard Furuseth建議我將1 << n 改成 1U << n,因爲使用無符號整型可以防止移位操作覆蓋掉了符號位。

反轉位序列

位的反轉(樸素方法)

unsigned int v;     // input bits to be reversed
                    // 需要翻轉的數值輸入保存在這裏
unsigned int r = v; // r will be reversed bits of v; first get LSB of v
                    // 將v反轉後的結果保存在變量r中;首先會算出v的最低有效位(注:推測此處的LSB是指Least Significant Bit,故翻譯爲最低有效位,不太確定)
int s = sizeof(v) * CHAR_BIT - 1; // extra shift needed at end
                                  // 最終需要額外左移的長度

for (v >>= 1; v; v >>= 1)
{
  r <<= 1;
  r |= v & 1;
  s--;
}
r <<= s; // shift when v's highest bits are zero
         // v的高位可能存在0,所以這裏需要左移

2004年10月15日,Michael Hoisie指出了一個最初版本的bug。

2005年5月3日,Randal E. Bryant提議去除掉一處多餘的操作。

2005年5月18日,Behdad Esfabod指出一個改動,可以讓少循環一次。

2007年2月6日,Liyong Zhou給出了一個更好的版本,如果v不是0的話才進入循環,而不是循環遍歷完所有位,這樣可以早一些退出循環。

位的反轉(查表法)

static const unsigned char BitReverseTable256[256] =
{
#   define R2(n)     n,     n + 2*64,     n + 1*64,     n + 3*64
#   define R4(n) R2(n), R2(n + 2*16), R2(n + 1*16), R2(n + 3*16)
#   define R6(n) R4(n), R4(n + 2*4 ), R4(n + 1*4 ), R4(n + 3*4 )
    R6(0), R6(2), R6(1), R6(3)
};

unsigned int v; // reverse 32-bit value, 8 bits at time
                // 需要反轉的32位值,每次反轉8位
unsigned int c; // c will get v reversed
                // 變量c結果保存v反轉後的值

// Option 1:
c = (BitReverseTable256[v & 0xff] << 24) |
    (BitReverseTable256[(v >> 8) & 0xff] << 16) |
    (BitReverseTable256[(v >> 16) & 0xff] << 8) |
    (BitReverseTable256[(v >> 24) & 0xff]);

// Option 2:
unsigned char * p = (unsigned char *) &v;
unsigned char * q = (unsigned char *) &c;
q[3] = BitReverseTable256[p[0]];
q[2] = BitReverseTable256[p[1]];
q[1] = BitReverseTable256[p[2]];
q[0] = BitReverseTable256[p[3]];

假定你的CPU可以輕鬆存取字節,那麼第一個方法需要17次左右的操作,第二個需要12個。

2009年7月14日,Hallvard Furuseth提供了這個宏壓縮的表。

單字節的位反轉(3次操作,需要64位乘和模)

unsigned char b; // reverse this (8-bit) byte
                 // 反轉這個(8位長)字節

b = (b * 0x0202020202ULL & 0x010884422010ULL) % 1023;

乘法操作產生了5份8位長的串,保存在64位整數裏。按位與操作選取出一些特定位置上(反轉)的位,並按照10位一組的方式分組。乘法和按位與操作將需要的二進制位從原始的字節中提取出來,使得他們都只出現在10位長的組裏。原始字節反轉後的位置,正好是他們在每個10位小組裏面的相對位置。最後一步,通過模除2^10 - 1,可以使64位整數的值按照每10位每10位的方式合併在一起。這個操作不會讓他們溢出,所以這個模除的步驟看起來很像按位或。

這個方法出自 Rich Schroeppel 的Beeler, M., Gosper, R. W., and Schroeppel, R. HAKMEM. MIT AI Memo 239, Feb. 29, 1972中的Programming Hacks小節。

單字節的位反轉(4次操作,需要64位乘,不需要除法)

unsigned char b; // reverse this byte
                 // 反轉這個字節的二進制位

b = ((b * 0x80200802ULL) & 0x0884422110ULL) * 0x0101010101ULL >> 32;

下圖中展示了計算的每個步驟,通過a, b, c, d, e, f, g和h來表示8位長字節的每一位。仔細觀察可以發現,第一個乘法產生了幾份原始串的拷貝,最後一個乘法則將散落的位從第五個字節開始向右將他們合併在了一起。

                                                                                        abcd efgh (-> hgfe dcba)
*                                                      1000 0000  0010 0000  0000 1000  0000 0010 (0x80200802)
-------------------------------------------------------------------------------------------------
                                            0abc defg  h00a bcde  fgh0 0abc  defg h00a  bcde fgh0
&                                           0000 1000  1000 0100  0100 0010  0010 0001  0001 0000 (0x0884422110)
-------------------------------------------------------------------------------------------------
                                            0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
*                                           0000 0001  0000 0001  0000 0001  0000 0001  0000 0001 (0x0101010101)
-------------------------------------------------------------------------------------------------
                                            0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
                                 0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
                      0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
           0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
0000 d000  h000 0c00  0g00 00b0  00f0 000a  000e 0000
-------------------------------------------------------------------------------------------------
0000 d000  h000 dc00  hg00 dcb0  hgf0 dcba  hgfe dcba  hgfe 0cba  0gfe 00ba  00fe 000a  000e 0000
>> 32
-------------------------------------------------------------------------------------------------
                                            0000 d000  h000 dc00  hg00 dcb0  hgf0 dcba  hgfe dcba
&                                                                                       1111 1111
-------------------------------------------------------------------------------------------------
                                                                                        hgfe dcba

注意在某些處理器上最後兩步可以合併,因爲32位寄存器可以作爲8位字節長度訪問(譯者注:IntelX86架構上EAX的最低8位可以使用AL訪問),寄存器存儲了乘法運算的結果而我們只需要取低位字節,因此它可能只需要6個操作。

2001年7月13日,出自Sean Anderson之手。

單字節的位反轉(7次操作,不需要64位操作)

b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16;

這個技巧藉助高位溢出來消除計算中產生的無用數值,使用前要確保操作的結果保存在無符號char型變量裏,以避免這個技巧失效。

2001年7月13日,出自Sean Anderson之手。
2002年1月3日,Mike Keith指出並糾正了書寫錯誤。

N位長的串的位反轉(5*lg(N)次操作,並行)

unsigned int v; // 32-bit word to reverse bit order
                // 反轉32位長的字

// swap odd and even bits
// 反轉奇數位和偶數位的位
v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1);
// swap consecutive pairs
// 反轉兩兩一組的位
v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2);
// swap nibbles ...
// 反轉半字節
v = ((v >> 4) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 4);
// swap bytes
// 反轉字節
v = ((v >> 8) & 0x00FF00FF) | ((v & 0x00FF00FF) << 8);
// swap 2-byte long pairs
// 反轉兩字節一組的位
v = ( v >> 16             ) | ( v               << 16);

下面的這個變種時間複雜度同樣是O(lg(N)),然而它需要額外的操作來反轉變量v。它的優點是常數在過程中計算,這樣可以佔用更少的內存。

unsigned int s = sizeof(v) * CHAR_BIT; // bit size; must be power of 2
                                       // 位長;必須要是2的乘冪
unsigned int mask = ~0;
while ((s >>= 1) > 0)
{
  mask ^= (mask << s);
  v = ((v >> s) & mask) | ((v << s) & ~mask);
}

這些方法很適合用在N很大的場景下。如果你需要用在大於64位的整型數時,那麼就便需要按照對應的模式添加代碼;不然只會有低32位會被反轉,答案也會保存在低32位下。

參考1983年的Dr.Dobb日誌,Binary Magic Numbers中Edwin Freed的文章可以查到更多信息。

2005年9月13日,Ken Raeburn提出了第二個變種。

2006年3月19日,Veldmeijer提到,第一個版本的算法的最後一行可以不用位與操作。

除法求模運算(或者稱爲求餘運算)

手工計算模除(模數是 1<<s 時)

const unsigned int n;          // numerator
                               // 變量n爲分子(被模除的數)
const unsigned int s;
const unsigned int d = 1U << s; // So d will be one of: 1, 2, 4, 8, 16, 32, ...
                                // 那麼變量d從小到大依次爲:1, 2, 4, 8, 16, 32, ...
unsigned int m;                // m will be n % d
                               // m保存n%d的結果
m = n & (d - 1);

這個技巧大多數程序員都會,爲了保持完整性,這裏還是把這個技巧放在了這裏。

手工計算模除(模數是 (1<<s)-1 時)

unsigned int n;                      // numerator
                                     // 變量n爲分子(被模除的數)
const unsigned int s;                // s > 0
const unsigned int d = (1 << s) - 1; // so d is either 1, 3, 7, 15, 31, ...).
                                     // 那麼變量d從小到大依次爲:1, 3, 7, 15, 31, ...
unsigned int m;                      // n % d goes here.
                                     // 保存n%d的結果
for (m = n; n > d; n = m)
{
  for (m = 0; n; n >>= s)
  {
    m += n & d;
  }
}
// Now m is a value from 0 to d, but since with modulus division
// 此時m的值範圍時0到d,但由於這裏是模除(譯者注:所以需要特殊處理m等於d的情況)
// we want m to be 0 when it is d.
// 當m的值爲d時,我們希望m的值變成0
m = m == d ? 0 : m;

這個用來處理 模數是比2的乘冪少1的整數 的模除技巧,最多需要 5 + (4 + 5 * ceil(N / s)) * ceil(lg(N / s)) 次操作,此處N表示被模數的有效位。也就是說,這個技巧最多需要O(N * lg(N))的時間複雜度。

2001年8月15日,出自Sean Anderson之手。

2004年6月17日,Sean A. Irvine糾正了我一個錯誤,我之前曾錯誤地寫道“我們也可以在後面直接對m賦值,m = ((m + 1) & d) - 1;”。

2005年4月25日,Michael Miller訂正了代碼中的一處排版顯示錯誤。

手工計算模除(模數是 (1<<s)-1 時,並行)

// The following is for a word size of 32 bits!
// 下面的方式適用於字長爲32位的情況
static const unsigned int M[] =
{
  0x00000000, 0x55555555, 0x33333333, 0xc71c71c7,
  0x0f0f0f0f, 0xc1f07c1f, 0x3f03f03f, 0xf01fc07f,
  0x00ff00ff, 0x07fc01ff, 0x3ff003ff, 0xffc007ff,
  0xff000fff, 0xfc001fff, 0xf0003fff, 0xc0007fff,
  0x0000ffff, 0x0001ffff, 0x0003ffff, 0x0007ffff,
  0x000fffff, 0x001fffff, 0x003fffff, 0x007fffff,
  0x00ffffff, 0x01ffffff, 0x03ffffff, 0x07ffffff,
  0x0fffffff, 0x1fffffff, 0x3fffffff, 0x7fffffff
};

static const unsigned int Q[][6] =
{
  { 0,  0,  0,  0,  0,  0}, {16,  8,  4,  2,  1,  1}, {16,  8,  4,  2,  2,  2},
  {15,  6,  3,  3,  3,  3}, {16,  8,  4,  4,  4,  4}, {15,  5,  5,  5,  5,  5},
  {12,  6,  6,  6 , 6,  6}, {14,  7,  7,  7,  7,  7}, {16,  8,  8,  8,  8,  8},
  { 9,  9,  9,  9,  9,  9}, {10, 10, 10, 10, 10, 10}, {11, 11, 11, 11, 11, 11},
  {12, 12, 12, 12, 12, 12}, {13, 13, 13, 13, 13, 13}, {14, 14, 14, 14, 14, 14},
  {15, 15, 15, 15, 15, 15}, {16, 16, 16, 16, 16, 16}, {17, 17, 17, 17, 17, 17},
  {18, 18, 18, 18, 18, 18}, {19, 19, 19, 19, 19, 19}, {20, 20, 20, 20, 20, 20},
  {21, 21, 21, 21, 21, 21}, {22, 22, 22, 22, 22, 22}, {23, 23, 23, 23, 23, 23},
  {24, 24, 24, 24, 24, 24}, {25, 25, 25, 25, 25, 25}, {26, 26, 26, 26, 26, 26},
  {27, 27, 27, 27, 27, 27}, {28, 28, 28, 28, 28, 28}, {29, 29, 29, 29, 29, 29},
  {30, 30, 30, 30, 30, 30}, {31, 31, 31, 31, 31, 31}
};

static const unsigned int R[][6] =
{
  {0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000},
  {0x0000ffff, 0x000000ff, 0x0000000f, 0x00000003, 0x00000001, 0x00000001},
  {0x0000ffff, 0x000000ff, 0x0000000f, 0x00000003, 0x00000003, 0x00000003},
  {0x00007fff, 0x0000003f, 0x00000007, 0x00000007, 0x00000007, 0x00000007},
  {0x0000ffff, 0x000000ff, 0x0000000f, 0x0000000f, 0x0000000f, 0x0000000f},
  {0x00007fff, 0x0000001f, 0x0000001f, 0x0000001f, 0x0000001f, 0x0000001f},
  {0x00000fff, 0x0000003f, 0x0000003f, 0x0000003f, 0x0000003f, 0x0000003f},
  {0x00003fff, 0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f},
  {0x0000ffff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff},
  {0x000001ff, 0x000001ff, 0x000001ff, 0x000001ff, 0x000001ff, 0x000001ff},
  {0x000003ff, 0x000003ff, 0x000003ff, 0x000003ff, 0x000003ff, 0x000003ff},
  {0x000007ff, 0x000007ff, 0x000007ff, 0x000007ff, 0x000007ff, 0x000007ff},
  {0x00000fff, 0x00000fff, 0x00000fff, 0x00000fff, 0x00000fff, 0x00000fff},
  {0x00001fff, 0x00001fff, 0x00001fff, 0x00001fff, 0x00001fff, 0x00001fff},
  {0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff},
  {0x00007fff, 0x00007fff, 0x00007fff, 0x00007fff, 0x00007fff, 0x00007fff},
  {0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff},
  {0x0001ffff, 0x0001ffff, 0x0001ffff, 0x0001ffff, 0x0001ffff, 0x0001ffff},
  {0x0003ffff, 0x0003ffff, 0x0003ffff, 0x0003ffff, 0x0003ffff, 0x0003ffff},
  {0x0007ffff, 0x0007ffff, 0x0007ffff, 0x0007ffff, 0x0007ffff, 0x0007ffff},
  {0x000fffff, 0x000fffff, 0x000fffff, 0x000fffff, 0x000fffff, 0x000fffff},
  {0x001fffff, 0x001fffff, 0x001fffff, 0x001fffff, 0x001fffff, 0x001fffff},
  {0x003fffff, 0x003fffff, 0x003fffff, 0x003fffff, 0x003fffff, 0x003fffff},
  {0x007fffff, 0x007fffff, 0x007fffff, 0x007fffff, 0x007fffff, 0x007fffff},
  {0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff},
  {0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff},
  {0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff},
  {0x07ffffff, 0x07ffffff, 0x07ffffff, 0x07ffffff, 0x07ffffff, 0x07ffffff},
  {0x0fffffff, 0x0fffffff, 0x0fffffff, 0x0fffffff, 0x0fffffff, 0x0fffffff},
  {0x1fffffff, 0x1fffffff, 0x1fffffff, 0x1fffffff, 0x1fffffff, 0x1fffffff},
  {0x3fffffff, 0x3fffffff, 0x3fffffff, 0x3fffffff, 0x3fffffff, 0x3fffffff},
  {0x7fffffff, 0x7fffffff, 0x7fffffff, 0x7fffffff, 0x7fffffff, 0x7fffffff}
};

unsigned int n;       // numerator
                      // 變量n爲分子(被模除的數)
const unsigned int s; // s > 0
const unsigned int d = (1 << s) - 1; // so d is either 1, 3, 7, 15, 31, ...).
                                     // 那麼變量d從小到大依次爲:1, 3, 7, 15, 31, ...
unsigned int m;       // n % d goes here.
                      // m保存n%d的結果

m = (n & M[s]) + ((n >> s) & M[s]);

for (const unsigned int * q = &Q[s][0], * r = &R[s][0]; m > d; q++, r++)
{
  m = (m >> *q) + (m & *r);
}
m = m == d ? 0 : m; // OR, less portably: m = m & -((signed)(m - d) >> s);

這個用來處理 模數是比2的乘冪少1的整數 的模除技巧,最多需要 O(lg(N)) 的時間複雜度,其中N是指被模除的數(如代碼註釋,32位整數)。操作數最多爲 12 + 9 * ceil(lg(N)) 次。如果在編譯期可以知道分母(除數),那麼這裏的表可以去掉;留下表中需要用到的數據,然後去掉循環。這個方法可以輕易地擴展到更多位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章