戰(zhàn))
《 嵌入式 C 語言宏的高級編程技巧與實(shí)戰(zhàn)》大家好我是雜燴君。我們一起來看看libevhtp這個(gè)高性能HTTP服務(wù)器庫中用到的宏高級技巧。1. 分支預(yù)測優(yōu)化現(xiàn)代CPU都有分支預(yù)測器一旦預(yù)測錯(cuò)誤流水線得全部沖掉性能瞬間暴跌。libevhtp用 __builtin_expect 給編譯器提個(gè)醒。1.1 likely/unlikely宏關(guān)鍵在這個(gè) !!(x) 的雙重否定int x 5 ; !x // 0 (false) !!x // 1 (true) - 把任意值規(guī)范化為0或1__builtin_expect(!!x, 1) 告訴編譯器這個(gè)條件大概率是true。1.2 錯(cuò)誤處理中的應(yīng)用錯(cuò)誤處理代碼99%的時(shí)間都不會(huì)執(zhí)行用 unlikely 能顯著提升性能錯(cuò)誤處理代碼99%的時(shí)間都不會(huì)執(zhí)行用 unlikely 能顯著提升性能編譯器會(huì)把unlikely的分支移到函數(shù)末尾讓主路徑保持緊湊提高指令緩存命中率。1.3 跨平臺兼容性注意看#else分支不支持 __builtin_expect 的編譯器上宏會(huì)退化成普通判斷。功能不受影響只是少了優(yōu)化。一份代碼多種實(shí)現(xiàn)這就是宏的魅力。2. Token拼接#### 操作符能把兩個(gè)token粘在一起讓我們在編譯期就生成代碼效果堪比C模板。2.1 命名規(guī)范的統(tǒng)一libevhtp有個(gè)設(shè)計(jì)很講究hook回調(diào)函數(shù)和它的參數(shù)總是成對出現(xiàn)每次都手寫這兩個(gè)字段代碼得累死。用 ## 自動(dòng)拼#defineHOOK_ARGS(var, hook_name)\ var-hooks-hook_name##_arg看看展開效果HOOK_ARGS(request, on_headers) → request-hooks-on_headers_arg HOOK_ARGS(request, on_path) → request-hooks-on_path_arg HOOK_ARGS(request, on_read) → request-hooks-on_read_arg一個(gè)宏所有hook都適用。這種命名規(guī)范統(tǒng)一起來維護(hù)代碼輕松多了。2.2 簡化深層訪問libevhtp還用這招簡化深層結(jié)構(gòu)體的訪問/* rc request-conn. 簡化深層訪問 */ #definerc_scratchconn-scratch_buf #definerc_parserconn-parser /* ch_ conn-hooks-on_... */ #definech_fini_arghooks-on_connection_fini_arg #definech_finihooks-on_connection_fini /* cr_ conn-request */ #definecr_statusrequest-status #definecr_flagsrequest-flags #definecr_protorequest-proto代碼里就能這樣寫// 原本要寫 if(request-conn-request-status 200){...} // 簡化后 if(cr_status 200){...}不僅簡潔更關(guān)鍵的是將來結(jié)構(gòu)體改了只需要改宏定義業(yè)務(wù)代碼一行不用動(dòng)。2.3 函數(shù)名自動(dòng)生成在數(shù)據(jù)結(jié)構(gòu)庫中 ## 還能生成完整的函數(shù)名。libevhtp的tree.h里就有這樣的用法#defineRB_INSERT(name, x, y) name##_RB_INSERT(x, y) #defineRB_REMOVE(name, x, y) name##_RB_REMOVE(x, y) #defineRB_FIND(name, x, y) name##_RB_FIND(x, y) #defineRB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF) #defineRB_MAX(name, x) name##_RB_MINMAX(x, RB_INF)使用時(shí)RB_HEAD(test, node) head; // 自動(dòng)生成test_RB_INSERT, test_RB_FIND 等函數(shù) RB_INSERT(test,head, new_node); node_t *found RB_FIND(test,head, key);這就是編譯期代碼生成每個(gè)樹類型都有獨(dú)立的函數(shù)集類型安全零運(yùn)行時(shí)開銷。3. 可變參數(shù)宏##__ VA_ARGS__C99引入了可變參數(shù)宏但真正好用的是GNU的 ##__VA_ARGS__ 擴(kuò)展。它能自動(dòng)處理空參數(shù)這在日志系統(tǒng)中簡直是救命稻草。3.1 ##的吞逗號魔法先看libevhtp的日志是怎么實(shí)現(xiàn)的#if!defined(EVHTP_DEBUG) #definelog_debug(M,...) #else #definelog_debug(M,...)\ fprintf(stderr,__log_debug_color(DEBUG) \ %s/%s:%-9dM \n,\ __FILENAME__, __FUNCTION__,__LINE__,##__VA_ARGS__) #endif關(guān)鍵在這個(gè) ##__VA_ARGS__ 。為什么要用 ## 看兩個(gè)調(diào)用log_debug(Connection established);// 沒有參數(shù) log_debug(Received %d bytes, bytes_read);// 有參數(shù)如果沒有 ## 第一行會(huì)展開成fprintf(stderr,DEBUG %s/%s:%-9dConnection established\n, __FILENAME__, __FUNCTION__,__LINE__,);// 注意最后多了個(gè)逗號這會(huì)直接編譯錯(cuò)誤。 ##__VA_ARGS__ 的妙處就在這當(dāng) __VA_ARGS__ 為空時(shí)它會(huì)自動(dòng)把前面的逗號吃掉// 有參數(shù)時(shí) fprintf(stderr,...\n, file, func, line, bytes_read); // 無參數(shù)時(shí) fprintf(stderr,...\n, file, func, line);// 逗號消失了3.2 嵌套可變參數(shù)宏更騷的操作是嵌套使用把可變參數(shù)一層層傳下去這個(gè)宏接受可變參數(shù)然后原樣轉(zhuǎn)發(fā)給函數(shù)調(diào)用。這招讓宏能適配任意數(shù)量的參數(shù)是C語言泛型編程的基石??纯葱Ч?/ 0個(gè)額外參數(shù) HOOK_REQUEST_RUN(req, on_headers_start); // 1個(gè)額外參數(shù) HOOK_REQUEST_RUN(req, on_header, header); // 2個(gè)額外參數(shù) HOOK_REQUEST_RUN(req, on_read, buffer, length);一個(gè)宏所有場景通吃。4. 字符串化## 操作符能把宏參數(shù)轉(zhuǎn)成字符串這是C語言編譯期反射的關(guān)鍵。4.1 斷言信息自動(dòng)生成標(biāo)準(zhǔn)的 assert 只告訴你掛了但不告訴你為啥掛。libevhtp用 # 把條件表達(dá)式也打出來#defineevhtp_assert(x)\ do{\ if(evhtp_unlikely(!(x))){\ fprintf(stderr,Assertion failed: %s (%s:%s:%d)\n,\ #x,__func__,__FILE__,__LINE__);\ fflush(stderr);\ abort;\ }\ }while(0)看展開效果evhtp_assert(conn !); // 展開后 if(!(conn !)){ fprintf(stderr,Assertion failed: %s (%s:%s:%d)\n, conn ! ,// #x 自動(dòng)轉(zhuǎn)成字符串 __func__,__FILE__,__LINE__); abort; }錯(cuò)誤信息直接包含源代碼調(diào)試時(shí)一眼就知道哪出問題了。4.2 帶格式化的斷言更進(jìn)一步把 # 和可變參數(shù)組合起來#defineevhtp_assert_fmt(x, fmt,...)\ do{\ if(evhtp_unlikely(!(x))){\ fprintf(stderr,Assertion failed: %s (%s:%s:%d) fmt \n,\ #x,__func__,__FILE__,__LINE__, __VA_ARGS__);\ fflush(stderr);\ abort;\ }\ }while(0)使用evhtp_assert_fmt(len MAX_BUF_SIZE, Buffer overflow: len%zu, max%zu, len, MAX_BUF_SIZE);輸出Assertion failed: len Buffer overflow: len8192, max4096既有條件表達(dá)式又有具體數(shù)值定位問題快多了。4.3 編譯期文件名優(yōu)化__FILE__ 會(huì)包含完整路徑在嵌入式系統(tǒng)里太浪費(fèi)ROM了。libevhtp有個(gè)巧招#define__FILENAME__\ (strrchr(__FILE__,/)?strrchr(__FILE__,/)1 :__FILE__)這是一個(gè)常量表達(dá)式編譯器會(huì)在編譯期計(jì)算出結(jié)果// __FILE__ /home/LinuxZn/project/src/evhtp.c // __FILENAME__ evhtp.c因?yàn)槭浅A勘磉_(dá)式編譯器會(huì)在編譯期算好最終二進(jìn)制里只有文件名完整路徑被優(yōu)化掉了。嵌入式系統(tǒng)每個(gè)字節(jié)都金貴這招能省不少空間。5. 總結(jié)掌握這些宏技巧能一定程度幫助我們寫出高性能又好維護(hù)的嵌入式代碼。記住一點(diǎn)能用內(nèi)聯(lián)函數(shù)就用內(nèi)聯(lián)函數(shù)只有宏能解決的場景才上宏。宏雖強(qiáng)大但別濫用?!?硬核、原創(chuàng)、實(shí)戰(zhàn)、高價(jià)值的資源合集 —成就百萬電子人