算實(shí)戰(zhàn)指南:從權(quán)限控制到位圖與性能優(yōu)化)
1. 位運(yùn)算不是“玩具”而是解決特定問(wèn)題的降維武器先說(shuō)個(gè)我自己的經(jīng)歷。早年做后端服務(wù)遇到一個(gè)需求用戶有幾十種通知開(kāi)關(guān)每種開(kāi)關(guān)獨(dú)立控制存數(shù)據(jù)庫(kù)的時(shí)候總不能建幾十個(gè)布爾字段吧后來(lái)看到老工程師在表里加了一個(gè)bigint字段用幾個(gè)看起來(lái)像魔法數(shù)字的常量去做判斷——當(dāng)時(shí)只覺(jué)著高端真正看懂之后才發(fā)現(xiàn)位運(yùn)算從來(lái)不是“面試考點(diǎn)”或者“炫技工具”它是一套用數(shù)學(xué)邏輯解決工程問(wèn)題的思維方式。位運(yùn)算的本質(zhì)很簡(jiǎn)單把多個(gè)二進(jìn)制的“開(kāi)關(guān)”塞進(jìn)同一個(gè)數(shù)字里用一次計(jì)算同時(shí)完成判斷或修改。這種能力在權(quán)限控制、狀態(tài)管理、算法優(yōu)化、存儲(chǔ)壓縮、圖形處理這些場(chǎng)景里幾乎無(wú)可替代。很多人在教科書(shū)里學(xué)過(guò) | ^ ~ 的運(yùn)算規(guī)則但從來(lái)沒(méi)想過(guò)它們到底能干嘛——這正是這篇文章想解決的。如果你現(xiàn)在遇到下面任一種情況這篇文章都值得看完系統(tǒng)里有大量“互不影響的布爾狀態(tài)”存儲(chǔ)和判斷都變得很笨重在算法題或業(yè)務(wù)代碼里看到x (x - 1)、x -x完全不知道在干什么需要處理海量數(shù)據(jù)去重、快速過(guò)濾覺(jué)得常規(guī)寫(xiě)法效率不夠聽(tīng)說(shuō)位運(yùn)算快但不知道什么時(shí)候不該用、什么時(shí)候用了反而踩坑。我會(huì)先從“為什么位運(yùn)算能省空間和時(shí)間”講起然后分別覆蓋權(quán)限管理、狀態(tài)機(jī)、算法加速、位圖應(yīng)用幾個(gè)真正的生產(chǎn)場(chǎng)景最后重點(diǎn)講一個(gè)最容易翻車(chē)的地方——優(yōu)先級(jí)。內(nèi)容盡量給到可以直接抄走參考的代碼和參數(shù)也會(huì)講一些我實(shí)際踩過(guò)的坑。2. 從掩碼到權(quán)限系統(tǒng)位運(yùn)算最經(jīng)典的生產(chǎn)級(jí)應(yīng)用2.1 開(kāi)關(guān)狀態(tài)的本質(zhì)一組布爾值的壓縮存儲(chǔ)權(quán)限系統(tǒng)是位運(yùn)算的“職業(yè)賽場(chǎng)”。假設(shè)你的系統(tǒng)里有四種權(quán)限讀、寫(xiě)、改、刪。最普通的做法是四個(gè)布爾字段或者一張多對(duì)多的關(guān)聯(lián)表——功能沒(méi)問(wèn)題但一旦權(quán)限種類(lèi)變多表和判斷邏輯都會(huì)變得很臃腫。用位運(yùn)算的話四個(gè)權(quán)限對(duì)應(yīng)四位二進(jìn)制權(quán)限二進(jìn)制位權(quán)值讀00011寫(xiě)00102改01004刪10008一個(gè)整型數(shù)字就能同時(shí)表示四種權(quán)限的組合。5這個(gè)值表示0101也就是“讀 改”兩種權(quán)限。用位運(yùn)算做操作時(shí)代碼非常簡(jiǎn)潔// 權(quán)限定義的典型寫(xiě)法 enum Permission { READ 1 0, // 1 WRITE 1 1, // 2 MODIFY 1 2, // 4 DELETE 1 3 // 8 }; // 增刪改查全部基于位運(yùn)算 int userPerm READ | MODIFY; // 授予“讀改” userPerm | WRITE; // 追加“寫(xiě)” userPerm ~DELETE; // 撤銷(xiāo)“刪” bool canWrite (userPerm WRITE) ! 0; // 判斷是否有寫(xiě)權(quán)限這里有個(gè)非常常見(jiàn)的困惑賦值的時(shí)候?yàn)槭裁从脇而不是直接直接會(huì)覆蓋掉原有的其他權(quán)限位這在真實(shí)業(yè)務(wù)里是嚴(yán)重事故——本來(lái)用戶有“讀改”你只是要加“寫(xiě)”結(jié)果寫(xiě)成了userPerm WRITE他的“讀改”就全沒(méi)了。位運(yùn)算寫(xiě)法的價(jià)值恰恰在這里|和 ~都是“只動(dòng)目標(biāo)位不影響其他位”這在并發(fā)和多人協(xié)作的系統(tǒng)里尤其重要。2.2 數(shù)據(jù)表設(shè)計(jì)的實(shí)際收益一個(gè)整數(shù)位代替十張關(guān)聯(lián)表如果權(quán)限種類(lèi)不多布爾字段的方案勉強(qiáng)也能跑。但當(dāng)權(quán)限種類(lèi)擴(kuò)展到幾十種比如一個(gè)SaaS平臺(tái)的“功能模塊權(quán)限”每個(gè)模塊都可獨(dú)立開(kāi)通——這時(shí)候?yàn)槊糠N權(quán)限建一個(gè)字段或者建一張“用戶-模塊”關(guān)聯(lián)表數(shù)據(jù)庫(kù)負(fù)擔(dān)和代碼復(fù)雜度都會(huì)直線上升。我自己做過(guò)一次重構(gòu)印象特別深。原來(lái)的方案是一張user_module_permission關(guān)聯(lián)表一個(gè)用戶幾十條記錄查詢時(shí)還要JOIN高峰期一個(gè)管理后臺(tái)的權(quán)限查詢拖慢了整個(gè)接口。重構(gòu)以后用戶表里直接加一個(gè)BIGINT字段每個(gè)模塊占一個(gè)位一次查詢拿到一個(gè)數(shù)字然后內(nèi)存里做位判斷。結(jié)果關(guān)聯(lián)表消失查詢時(shí)間從幾十毫秒降到接近于零代碼量還少了三分之一。這里有一個(gè)參數(shù)建議如果狀態(tài)位數(shù)量在 64 以內(nèi)用BIGINT64位就夠了超過(guò) 64 個(gè)才需要拆成多個(gè)字段或用 String 類(lèi)型的 bitmap 方案。絕大多數(shù)業(yè)務(wù)場(chǎng)景不會(huì)超過(guò) 64 個(gè)獨(dú)立開(kāi)關(guān)所以一個(gè)BIGINT通常是最終解。注意不要為了“看著高級(jí)”就把所有布爾屬性都往位里塞。如果某個(gè)狀態(tài)字段需要單獨(dú)建索引、單獨(dú)參與 SQL 查詢保留獨(dú)立字段更合適。位運(yùn)算擅長(zhǎng)的是“整體取出、內(nèi)存判斷”而不是“數(shù)據(jù)庫(kù) WHERE 條件里做位運(yùn)算”——雖然 MySQL 也支持但那樣會(huì)讓索引失效屬于典型的用錯(cuò)場(chǎng)景。2.3 特性開(kāi)關(guān)Feature Flag里的位運(yùn)算思維你可能沒(méi)意識(shí)到很多系統(tǒng)里的“灰度開(kāi)關(guān)”“功能特性開(kāi)關(guān)”本質(zhì)也是權(quán)限系統(tǒng)的變體。比如一個(gè)客戶端軟件不同渠道包需要開(kāi)啟不同功能用位標(biāo)志做開(kāi)關(guān)一個(gè)整數(shù)就能承載幾十個(gè)功能的啟停狀態(tài)。// Go 里的特性開(kāi)關(guān)一個(gè) int 承載一組功能 const ( FeatureNewUI 1 iota // 1 FeatureChat // 2 FeatureDarkMode // 4 FeatureExport // 8 ) features : FeatureNewUI | FeatureDarkMode func IsEnabled(features, flag int) bool { return featuresflag ! 0 }這個(gè)模式在移動(dòng)端 SDK、游戲客戶端里很常見(jiàn)。好處有兩個(gè)一是狀態(tài)傳遞非常省——客戶端上報(bào)一個(gè)整數(shù)服務(wù)端就知道全部功能開(kāi)關(guān)狀態(tài)二是做 A/B 實(shí)驗(yàn)時(shí)可以一次性下發(fā)一份組合值不用每個(gè)實(shí)驗(yàn)單獨(dú)拉一個(gè)字段。3. 狀態(tài)機(jī)與算法加速位掩碼的高階用法3.1 用一個(gè)整數(shù)記錄整個(gè)狀態(tài)集合位圖 (Bitset)如果說(shuō)權(quán)限是用位來(lái)記錄“開(kāi)關(guān)”那位圖就是用位來(lái)記錄“存在性”。有一個(gè)面試反復(fù)出現(xiàn)的問(wèn)題40 億個(gè)整數(shù)中找重復(fù)數(shù)字內(nèi)存限制 500MB怎么做常規(guī)想法是排序或哈希但內(nèi)存都會(huì)爆。如果用位圖一個(gè) bit 代表一個(gè)數(shù)的存在狀態(tài)40 億個(gè)數(shù)只需要40億/8 500MB左右。這正好是這道題的內(nèi)存上限屬于標(biāo)準(zhǔn)答案。工程上Redis 的 Setbit 操作、Lucene 的位圖索引、布隆過(guò)濾器全都是這套思想的產(chǎn)物——把千萬(wàn)級(jí)數(shù)據(jù)的存在性壓進(jìn)一個(gè)緊湊的位序列用位運(yùn)算做 O(1) 的成員判斷。// 位圖實(shí)現(xiàn)整數(shù)集合的經(jīng)典寫(xiě)法C# int[] bits new int[1 20]; // 每 int 存 32 位 public void Add(int value) { bits[value / 32] | (1 (value % 32)); } public bool Contains(int value) { return (bits[value / 32] (1 (value % 32))) ! 0; }這套代碼的關(guān)鍵就是value / 32定位到哪個(gè) intvalue % 32定位到哪一位。實(shí)際項(xiàng)目中Java 有現(xiàn)成的BitSetC 有std::bitsetGo 有big.Int別看它是個(gè)大數(shù)類(lèi)型底層就是一串位。優(yōu)先用現(xiàn)成的庫(kù)自己寫(xiě)位圖除了學(xué)習(xí)場(chǎng)景通常屬于重復(fù)造輪子。3.2 集合運(yùn)算一筆完成交、并、差全是位操作位圖更大的威力在于集合運(yùn)算。傳統(tǒng)寫(xiě)法要做循環(huán)遍歷時(shí)間復(fù)雜度 O(N)位圖寫(xiě)法里交集是并集是|差集是 ~——都是 O(N/字長(zhǎng)) 的批量操作實(shí)際是內(nèi)存帶寬級(jí)別的速度。舉一個(gè)真實(shí)場(chǎng)景。內(nèi)容推薦系統(tǒng)里要根據(jù)用戶標(biāo)簽過(guò)濾內(nèi)容一個(gè)內(nèi)容可能有上萬(wàn)條標(biāo)簽記錄。用位圖存標(biāo)簽集合做“同時(shí)包含標(biāo)簽A和標(biāo)簽B”的篩選就是兩個(gè)位圖做一次運(yùn)算CPU 一次指令能處理 64 位整個(gè)篩選操作可能就是幾千條指令的事。相比之下如果用 IN JOIN 的 SQL 或者多重循環(huán)數(shù)據(jù)量大時(shí)性能差距可以達(dá)到幾個(gè)數(shù)量級(jí)。3.3 經(jīng)典算法里的位運(yùn)算套路從 N 皇后到子集枚舉算法競(jìng)賽和面試題里位運(yùn)算出現(xiàn)頻率極高但很多人不知道這些“套路”其實(shí)在生產(chǎn)里也有對(duì)應(yīng)場(chǎng)景。首先是子集枚舉比如給一個(gè)包含 n 個(gè)元素的集合枚舉所有子集。常見(jiàn)寫(xiě)法是遞歸但位運(yùn)算一行就能遍歷for (int mask 0; mask (1 n); mask) { // mask 的二進(jìn)制為 1 的位置就是選中元素的位置 }1 n就是 2 的 n 次方這個(gè)寫(xiě)法在各種配置組合遍歷、測(cè)試用例生成、窮舉搜索里都很常用。我自己做某種 SKU 組合價(jià)格計(jì)算時(shí)就用過(guò)它商品有 4 個(gè)可選項(xiàng)每個(gè)可選可不選一共 16 種組合這個(gè)循環(huán)一句代碼就把所有組合列完了。其次是快速統(tǒng)計(jì) 1 的個(gè)數(shù)__builtin_popcount/bits.OnesCount/Integer.bitCount這類(lèi)內(nèi)置函數(shù)底層都用了巧妙的位運(yùn)算并行加法。如果你需要統(tǒng)計(jì)“一個(gè)角色同時(shí)擁有多少種權(quán)限”來(lái)排序用它比逐位循環(huán)快得多。還有一個(gè)出現(xiàn)率極高的x -x提取最低位的 1x (x - 1)消掉最低位的 1。它們?cè)跇?shù)狀數(shù)組、哈夫曼編碼、某些垃圾回收算法里都有應(yīng)用大家如果只記兩個(gè)結(jié)論就行x (x - 1)清除最低位的 1常用于判斷 2 的冪x -x只保留最低位的 1常用于獲取狀態(tài)里的最低優(yōu)先級(jí)事件。3.4 位運(yùn)算加速的邊界在哪里位運(yùn)算快但快多少這個(gè)要理性看?,F(xiàn)代 CPU 做一次加法、移位、與運(yùn)算的時(shí)鐘周期基本差不多位運(yùn)算的優(yōu)勢(shì)通常不是“單條指令快”而是“一條指令代替了一整個(gè)循環(huán)”。如果你用循環(huán)去檢查 64 個(gè)布爾狀態(tài)要執(zhí)行最多 64 次比較和跳轉(zhuǎn)用位運(yùn)算做一次CPU 一個(gè)周期內(nèi)同時(shí)檢查完這才是性能差距的來(lái)源。所以一個(gè)實(shí)用的判斷標(biāo)準(zhǔn)是你的熱點(diǎn)代碼里是否存在“要遍歷很多個(gè)布爾狀態(tài)做判斷、而這些狀態(tài)彼此獨(dú)立”的模式如果有位運(yùn)算是真正能降延遲的手段如果只是單次判斷省下的微不足道還犧牲了可讀性。我見(jiàn)過(guò)有人為了炫技把if (a 1 b 1)改成if ((flags 3) 3)改動(dòng)之后代碼確實(shí)“高級(jí)了”但團(tuán)隊(duì)接手時(shí)全都得停下來(lái)查這是什么意思——這種屬于負(fù)優(yōu)化不推薦。4. 存儲(chǔ)與網(wǎng)絡(luò)傳輸中的位操作看不見(jiàn)的底層功臣4.1 協(xié)議數(shù)據(jù)包里的標(biāo)志字段如果你接觸過(guò)網(wǎng)絡(luò)協(xié)議、二進(jìn)制文件格式或嵌入式開(kāi)發(fā)對(duì)“標(biāo)志位”一定不陌生。TCP 頭的 8 個(gè)標(biāo)志位FIN、SYN、RST、PSH、ACK、URG、ECE、CWR每個(gè)占一位擠在同一個(gè) 16 位字段里IP 頭里的分片標(biāo)志、IPv6 的流標(biāo)簽全是位級(jí)操作。// 網(wǎng)絡(luò)協(xié)議標(biāo)志位的常見(jiàn)解析手法 #define TCP_FLAG_FIN 0x001 #define TCP_FLAG_SYN 0x002 #define TCP_FLAG_RST 0x004 #define TCP_FLAG_PSH 0x008 #define TCP_FLAG_ACK 0x010 // 判斷 SYNACK 同時(shí)置位 flags tcp_header.flags; if ((flags (TCP_FLAG_SYN | TCP_FLAG_ACK)) (TCP_FLAG_SYN | TCP_FLAG_ACK)) { // 三次握手第二步 }這種代碼省的不是“幾個(gè)字段”而是協(xié)議頭的整體尺寸。每個(gè) TCP 包少一個(gè)字節(jié)對(duì)海量傳輸來(lái)說(shuō)都是巨大的帶寬節(jié)省。你在業(yè)務(wù)代碼里可能永遠(yuǎn)不用寫(xiě)這種解析但理解它之后再看到那些0x8001這樣的協(xié)議常量就不會(huì)一頭霧水了。4.2 壓縮存儲(chǔ)把幾個(gè)小整數(shù)塞進(jìn)一個(gè)大整數(shù)在嵌入式、游戲存檔、低帶寬 IoT 場(chǎng)景里位字段是數(shù)據(jù)壓縮的最原始手段。比如一個(gè)狀態(tài)消息里有三個(gè)值溫度誤差范圍 -15~15需要 5 位、開(kāi)關(guān)狀態(tài)1 位、檔位 0~73 位理論上 9 位就能裝完而按常規(guī)寫(xiě)法三個(gè)整型字段要占 96 位。// 位字段壓縮示例3 個(gè)整數(shù)塞進(jìn) 16 位 uint16_t packed 0; packed | (temperature_offset 0x1F) 0; // 位 0~4 packed | (power_state 0x01) 5; // 位 5 packed | (gear_level 0x07) 6; // 位 6~8這種手法在工業(yè)報(bào)文、傳感器數(shù)據(jù)上傳里非常常見(jiàn)。數(shù)據(jù)量小意味著省電、省流量、省存儲(chǔ)——對(duì)嵌入式設(shè)備來(lái)說(shuō)每 bit 都有價(jià)值。不過(guò)這類(lèi)代碼有一個(gè)運(yùn)維難點(diǎn)位段一旦定義后續(xù)加字段很容易破壞兼容性。所以我一般建議在協(xié)議設(shè)計(jì)階段就預(yù)留擴(kuò)展位并且文檔里畫(huà)清楚每一位的用途否則半年后沒(méi)人看得懂代碼里那些魔法位移數(shù)字。4.3 圖像與顏色處理RGB 打包與像素操作寫(xiě)圖形、游戲渲染的人一天到晚跟位運(yùn)算打交道。常見(jiàn)操作是把 RGBA 四個(gè)通道打包進(jìn)一個(gè) 32 位整數(shù)uint32_t pixel (r 24) | (g 16) | (b 8) | a; uint8_t red (pixel 24) 0xFF;之前做圖像處理工具時(shí)要對(duì)一張圖做通道互換和閾值過(guò)濾如果用逐通道數(shù)組處理一塊 4096x4096 的圖要跑好一陣改成整型像素一次讀入、位運(yùn)算提取通道后速度直接上一個(gè)臺(tái)階。此外很多圖片格式如 BMP、DDS本身就帶位標(biāo)志解碼器的第一步就是解析位結(jié)構(gòu)。另外一個(gè)常見(jiàn)技巧判斷RGB灰度化時(shí)標(biāo)準(zhǔn)公式是0.299R 0.587G 0.114B浮點(diǎn)運(yùn)算慢可以近似用移位版// 經(jīng)典整數(shù)灰度公式只有整數(shù)運(yùn)算 gray (r * 299 g * 587 b * 114 500) / 1000; // 更快的移位近似誤差可接受時(shí) gray (r * 77 g * 150 b * 29) 8;這個(gè)近似版在不少嵌入式圖像算法里能看到誤差很小但避開(kāi)浮點(diǎn)非常適合沒(méi)有 FPU 的單片機(jī)。5. 優(yōu)先級(jí)與陷阱必須刻進(jìn)腦子的運(yùn)算順序5.1 為什么位運(yùn)算符優(yōu)先級(jí)是熱搜??途W(wǎng)上搜“位運(yùn)算符優(yōu)先級(jí)”的人一直很多因?yàn)檫@個(gè)問(wèn)題真的太容易踩坑了。C 系語(yǔ)言里位與/位或的優(yōu)先級(jí)低于關(guān)系運(yùn)算符這導(dǎo)致一個(gè)非常經(jīng)典的錯(cuò)誤// 錯(cuò)誤示例實(shí)際執(zhí)行順序和直覺(jué)完全不同 if (flags 1 1) { ... }你直覺(jué)以為這是(flags 1) 1但實(shí)際上的優(yōu)先級(jí)比高它先算1 1得1然后執(zhí)行flags 1——在 C 語(yǔ)言里這恰好碰對(duì)了但換個(gè)場(chǎng)景比如flags 2 2結(jié)果就很迷惑。這不是猜謎游戲而是實(shí)打?qū)嵉倪壿?bug。另外還有移位運(yùn)算符優(yōu)先級(jí)低于加減法的問(wèn)題// 經(jīng)典錯(cuò)誤想左移 8 位再加 1先加了反而錯(cuò) int x a 8 1; // 實(shí)際是 a 9 int x (a 8) 1; // 這個(gè)才對(duì)5.2 完整優(yōu)先級(jí)清單與避坑策略在 C、C、Java、C#、Go 這些語(yǔ)言里和位運(yùn)算相關(guān)的優(yōu)先級(jí)大致是最高后綴 -- [] .等一元運(yùn)算~ ! --、正負(fù)號(hào)乘除加減移位 關(guān)系 相等 !位與異或^位或|邏輯與邏輯或||這個(gè)順序產(chǎn)生一個(gè)核心結(jié)論 ^ |的結(jié)果幾乎總是跟在、!、之后算的。所以但凡同一個(gè)表達(dá)式里既出現(xiàn)了比較運(yùn)算又出現(xiàn)了位運(yùn)算無(wú)腦加括號(hào)就完事了// 正確的防御性寫(xiě)法 if ((flags MASK) MASK) { ... } if ((value 0xF0) ! 0) { ... }這里有兩點(diǎn)需要解釋一下一是位移運(yùn)算符低于加減法這個(gè)坑在 C/C/Java 里都存在但在 Python 和 Rust 里的優(yōu)先級(jí)是相反的加減在移位之前。如果你寫(xiě)多語(yǔ)言不要憑肌肉記憶每換一門(mén)語(yǔ)言都得重新確認(rèn)一次。二是邏輯與/或 和 ||優(yōu)先級(jí)高于賦值但低于位運(yùn)算。所以flags MASK MASK isEnabled這種表達(dá)式會(huì)先算flags (MASK MASK)結(jié)果再送去很容易出現(xiàn)類(lèi)型不匹配的編譯錯(cuò)誤或隱蔽邏輯錯(cuò)誤。永遠(yuǎn)不要吝嗇那對(duì)括號(hào)。5.3 不同類(lèi)型的位運(yùn)算符混合使用時(shí)容易出現(xiàn)的意外位運(yùn)算雖然分工清晰但混在一起時(shí)也有不少“看起來(lái)對(duì)、實(shí)際錯(cuò)”的寫(xiě)法用||代替|flags || MASK的結(jié)果是布爾值不是位標(biāo)志。一旦需要繼續(xù)參與位運(yùn)算類(lèi)型就炸了。誤用~做取反~0在 32 位整型里是0xFFFFFFFF即 -1不是 0。如果你想“取最低 8 位以外的位”正確寫(xiě)法是x ~0xFF寫(xiě)成x 0xFFFFFF00也行但用補(bǔ)碼的~更可讀。移位越界C/C 里移位位數(shù)超過(guò)類(lèi)型寬度是未定義行為當(dāng)時(shí)可能沒(méi)事?lián)Q編譯器、換優(yōu)化級(jí)別可能就炸了。Go 和 Java 會(huì)自動(dòng)截?cái)辔粩?shù)但語(yǔ)義不同寫(xiě)之前先確認(rèn)。提示我自己寫(xiě)位運(yùn)算代碼的習(xí)慣是“能一眼看懂就不炫技”每條位運(yùn)算表達(dá)式一定有一行注釋說(shuō)明它的掩碼含義。位運(yùn)算代碼最大的敵人不是性能而是半年后的自己看不懂。凡是出現(xiàn)非 0/1 的魔法數(shù)字寫(xiě)成命名的常量才是工程化的做法。6. 性能優(yōu)化的邊際收益什么時(shí)候值得用位運(yùn)算很多人問(wèn)位運(yùn)算既然快是不是所有布爾判斷都應(yīng)該改成位運(yùn)算答案是絕對(duì)不要。需要考慮三個(gè)成本可讀性、維護(hù)性、以及實(shí)際提升是否值得。6.1 編譯器早就幫你做了一部分優(yōu)化現(xiàn)代編譯器GCC、Clang、MSVC 等在開(kāi)優(yōu)化后很多“看起來(lái)普通”的代碼會(huì)自動(dòng)生成位運(yùn)算指令。比如連續(xù)的多個(gè)布爾字段如果用struct定義并且開(kāi)了-O2編譯器可能會(huì)主動(dòng)做位域合并再比如if (a % 2 0)編譯器會(huì)優(yōu)化成if ((a 1) 0)。所以如果你的動(dòng)機(jī)只是“聽(tīng)說(shuō)位運(yùn)算快”可能編譯器早就替你做了手動(dòng)改寫(xiě)往往白費(fèi)功夫。這不是說(shuō)位運(yùn)算性能優(yōu)勢(shì)是虛的——它的優(yōu)勢(shì)集中在批量場(chǎng)景一次處理整組標(biāo)志、海量數(shù)據(jù)存在性過(guò)濾單點(diǎn)判斷的提升完全可以忽略。判斷的依據(jù)從來(lái)不是“單次操作快”而是“循環(huán)次數(shù)被抹掉”。6.2 實(shí)戰(zhàn)判斷這個(gè)場(chǎng)景該不該用位運(yùn)算我給自己定了一個(gè)簡(jiǎn)單的檢查清單寫(xiě)完后照著過(guò)一遍就知道該不該用判斷標(biāo)準(zhǔn)適合用不適合用狀態(tài)數(shù)量超過(guò) 4 個(gè)互相獨(dú)立的布爾狀態(tài)只有兩三個(gè)狀態(tài)操作頻率在熱點(diǎn)循環(huán)、傳輸協(xié)議、存儲(chǔ)編碼中低頻請(qǐng)求、UI 事件處理傳遞方式需要壓縮進(jìn)一個(gè)參數(shù)/字段字段需獨(dú)立索引或可讀性優(yōu)先代碼維護(hù)者團(tuán)隊(duì)都熟悉位運(yùn)算且文檔清晰團(tuán)隊(duì)成員容易讀錯(cuò)、新人多還要考慮調(diào)試成本位運(yùn)算出了問(wèn)題排查比普通布爾難得多。一個(gè)0x5A到底代表哪些權(quán)限、哪些位被誤置不是一眼能看出來(lái)的。完善的日志和單元測(cè)試能緩解但不會(huì)消失。6.3 一個(gè)完整的優(yōu)化案例訂單狀態(tài)過(guò)濾器的重構(gòu)之前做過(guò)一個(gè)訂單查詢系統(tǒng)訂單有十幾個(gè)狀態(tài)維度支付狀態(tài)、發(fā)貨狀態(tài)、退款狀態(tài)、風(fēng)控狀態(tài)、是否加急、來(lái)源渠道……原來(lái)的代碼是十幾個(gè) if 嵌套判斷統(tǒng)計(jì)一天訂單時(shí)每次要循環(huán)幾十萬(wàn)條記錄做判斷。優(yōu)化的做法是給每個(gè)訂單打一個(gè)狀態(tài)位標(biāo)記哪些維度發(fā)生了變化用一個(gè) 32 位整數(shù)記錄所有維度狀態(tài)。每次統(tǒng)計(jì)不再逐層 if而是用位掩碼過(guò)濾一次操作就篩掉絕大多數(shù)不匹配的訂單。最終統(tǒng)計(jì)接口延遲從 800ms 降到了 60ms——這個(gè)數(shù)量級(jí)才值得動(dòng)手。但要注意這個(gè)優(yōu)化能被接受核心原因不是省了 CPU而是位掩碼可以合并多條判斷邏輯讓過(guò)濾從“幾十個(gè)條件組合”變成“一個(gè)數(shù)字的幾個(gè)位測(cè)試”代碼整體還變短了。如果重構(gòu)后代碼量漲了、可讀性降了哪怕性能提升明顯也要謹(jǐn)慎評(píng)估——除非那確實(shí)是每天跑幾億次的瓶頸否則維護(hù)成本可能抵消收益。7. 比普通讀寫(xiě)多走一層位運(yùn)算的工程化實(shí)踐心得文章寫(xiě)到最后分享幾點(diǎn)個(gè)人在項(xiàng)目里使用位運(yùn)算的體會(huì)。第一位運(yùn)算的代碼永遠(yuǎn)要和注釋一起出現(xiàn)。任何包含、 0xFF、1 iota的代碼旁邊都要寫(xiě)明“這個(gè)位代表什么、為什么這樣設(shè)計(jì)”。我看到太多線上事故來(lái)自一個(gè)被隨意加進(jìn)去的位導(dǎo)致老數(shù)據(jù)全部錯(cuò)位。第二在設(shè)計(jì)和編碼階段就定義好常量名。直接用10111011這種字面量寫(xiě)邏輯是災(zāi)難但用名的常量比如FLAG_IS_PAID 1 3代碼的自我解釋能力會(huì)強(qiáng)很多。Linux 內(nèi)核源碼里這種定義比比皆是因?yàn)閮?nèi)核開(kāi)發(fā)者在“代碼復(fù)用共享”的條件下對(duì)可讀性要求極高這點(diǎn)非常值得業(yè)務(wù)代碼學(xué)習(xí)。第三測(cè)試一定要覆蓋邊界。位運(yùn)算的單元測(cè)試必須包含全 0 的輸入、全 1 的輸入、最高位為 1 的輸入、單個(gè)位的輸入。很多位運(yùn)算 bug 都是在這些極端值下才會(huì)暴露常規(guī)正例完全測(cè)不出來(lái)。第四順手積累一套自己的位運(yùn)算工具函數(shù)。無(wú)論你切到什么語(yǔ)言下面這幾個(gè)函數(shù)都值得沉淀成本地工具庫(kù)HasFlag(flags, flag)判斷是否包含某標(biāo)志SetFlag(flags, flag)設(shè)置標(biāo)志位ClearFlag(flags, flag)清除標(biāo)志位TogggleFlag(flags, flag)翻轉(zhuǎn)標(biāo)志位CountBits(x)統(tǒng)計(jì)二進(jìn)制 1 的個(gè)數(shù)語(yǔ)言內(nèi)置函數(shù)優(yōu)先。這些函數(shù)邏輯都一樣跨項(xiàng)目復(fù)用能有效減少“每寫(xiě)一次就查一次優(yōu)先級(jí)”的煩惱?;氐阶畛醯膯?wèn)題位運(yùn)算符有什么實(shí)際應(yīng)用場(chǎng)景答案其實(shí)不在運(yùn)算符本身而在你如何看待“一組獨(dú)立的布爾狀態(tài)”。當(dāng)你認(rèn)識(shí)到現(xiàn)實(shí)中大量信息天然就是“一堆開(kāi)關(guān)”并且希望用最緊湊、最快的方式去處理它們時(shí)位運(yùn)算就會(huì)從“課本習(xí)題”變成“思考工具”。它不適用于所有問(wèn)題但在權(quán)限控制、位圖索引、網(wǎng)絡(luò)協(xié)議、圖像處理、狀態(tài)機(jī)這些場(chǎng)景里它就是最貼合數(shù)據(jù)本質(zhì)的解法。掌握它的關(guān)鍵不是背下規(guī)則而是不斷在實(shí)際場(chǎng)景中“用上”它——用上一次你就會(huì)真正理解它為什么被保留到今天。