星標(biāo)公眾號,讓嵌入式知識 “投喂” 不停歇!
大家好,我是雜燴君。
我們一起來看看libevhtp這個高性能HTTP服務(wù)器庫中,用到的宏高級技巧。
現(xiàn)代CPU都有分支預(yù)測器,一旦預(yù)測錯誤,流水線得全部沖掉,性能瞬間暴跌。libevhtp用__builtin_expect給編譯器提個醒。

關(guān)鍵在這個!!(x)的雙重否定:
int x = 5;
!x // 0 (false)
!!x // 1 (true) - 把任意值規(guī)范化為0或1
__builtin_expect(!!x, 1)告訴編譯器:"這個條件大概率是true"。
錯誤處理代碼99%的時間都不會執(zhí)行,用unlikely能顯著提升性能:

編譯器會把"unlikely"的分支移到函數(shù)末尾,讓主路徑保持緊湊,提高指令緩存命中率。
注意看#else分支,不支持__builtin_expect的編譯器上,宏會退化成普通判斷。功能不受影響,只是少了優(yōu)化。一份代碼,多種實現(xiàn),這就是宏的魅力。
##操作符能把兩個token粘在一起,讓我們在編譯期就生成代碼,效果堪比C++模板。
libevhtp有個設(shè)計很講究,hook回調(diào)函數(shù)和它的參數(shù)總是成對出現(xiàn):

每次都手寫這兩個字段,代碼得累死。用##自動拼:
#define HOOK_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
一個宏,所有hook都適用。這種命名規(guī)范統(tǒng)一起來,維護代碼輕松多了。
libevhtp還用這招簡化深層結(jié)構(gòu)體的訪問:
/* rc == request->conn. 簡化深層訪問 */
#define rc_scratch conn->scratch_buf
#define rc_parser conn->parser
/* ch_ == conn->hooks->on_... */
#define ch_fini_arg hooks->on_connection_fini_arg
#define ch_fini hooks->on_connection_fini
/* cr_ == conn->request */
#define cr_status request->status
#define cr_flags request->flags
#define cr_proto request->proto
代碼里就能這樣寫:
// 原本要寫:
if (request->conn->request->status == 200) { ... }
// 簡化后:
if (cr_status == 200) { ... }
不僅簡潔,更關(guān)鍵的是將來結(jié)構(gòu)體改了,只需要改宏定義,業(yè)務(wù)代碼一行不用動。
在數(shù)據(jù)結(jié)構(gòu)庫中,##還能生成完整的函數(shù)名。libevhtp的tree.h里就有這樣的用法:
#define RB_INSERT(name, x, y) name##_RB_INSERT(x, y)
#define RB_REMOVE(name, x, y) name##_RB_REMOVE(x, y)
#define RB_FIND(name, x, y) name##_RB_FIND(x, y)
#define RB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF)
#define RB_MAX(name, x) name##_RB_MINMAX(x, RB_INF)
使用時:
RB_HEAD(test, node) head;
// 自動生成:test_RB_INSERT, test_RB_FIND 等函數(shù)
RB_INSERT(test, &head, new_node);
node_t *found = RB_FIND(test, &head, key);
這就是編譯期代碼生成,每個樹類型都有獨立的函數(shù)集,類型安全,零運行時開銷。
C99引入了可變參數(shù)宏,但真正好用的是GNU的##__VA_ARGS__擴展。它能自動處理空參數(shù),這在日志系統(tǒng)中簡直是救命稻草。
先看libevhtp的日志是怎么實現(xiàn)的:
#if !defined(EVHTP_DEBUG)
#define log_debug(M, ...)
#else
#define log_debug(M, ...) \
fprintf(stderr, __log_debug_color("DEBUG") " " \
"%s/%s:%-9d" M "\n", \
__FILENAME__, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif
關(guān)鍵在這個##__VA_ARGS__。為什么要用##?看兩個調(diào)用:
log_debug("Connection established"); // 沒有參數(shù)
log_debug("Received %d bytes", bytes_read); // 有參數(shù)
如果沒有##,第一行會展開成:
fprintf(stderr, "DEBUG %s/%s:%-9d" "Connection established" "\n",
__FILENAME__, __FUNCTION__, __LINE__, ); // 注意最后多了個逗號!
這會直接編譯錯誤。##__VA_ARGS__的妙處就在這:當(dāng)__VA_ARGS__為空時,它會自動把前面的逗號吃掉:
// 有參數(shù)時:
fprintf(stderr, "..." "\n", file, func, line, bytes_read);
// 無參數(shù)時:
fprintf(stderr, "..." "\n", file, func, line); // 逗號消失了!
更騷的操作是嵌套使用,把可變參數(shù)一層層傳下去:

這個宏接受可變參數(shù),然后原樣轉(zhuǎn)發(fā)給函數(shù)調(diào)用。這招讓宏能適配任意數(shù)量的參數(shù),是C語言泛型編程的基石。
看看效果:
// 0個額外參數(shù)
HOOK_REQUEST_RUN(req, on_headers_start);
// 1個額外參數(shù)
HOOK_REQUEST_RUN(req, on_header, header);
// 2個額外參數(shù)
HOOK_REQUEST_RUN(req, on_read, buffer, length);
一個宏,所有場景通吃。
#操作符能把宏參數(shù)轉(zhuǎn)成字符串,這是C語言"編譯期反射"的關(guān)鍵。
標(biāo)準(zhǔn)的assert只告訴你掛了,但不告訴你為啥掛。libevhtp用#把條件表達式也打出來:
#define evhtp_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 != NULL);
// 展開后:
if (!(conn != NULL)) {
fprintf(stderr, "Assertion failed: %s (%s:%s:%d)\n",
"conn != NULL", // #x 自動轉(zhuǎn)成字符串
__func__, __FILE__, __LINE__);
abort();
}
錯誤信息直接包含源代碼,調(diào)試時一眼就知道哪出問題了。
更進一步,把#和可變參數(shù)組合起來:
#define evhtp_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 < MAX_BUF_SIZE (process_data:evhtp.c:1234)
Buffer overflow: len=8192, max=4096
既有條件表達式,又有具體數(shù)值,定位問題快多了。
__FILE__會包含完整路徑,在嵌入式系統(tǒng)里太浪費ROM了。libevhtp有個巧招:
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
這是一個常量表達式,編譯器會在編譯期計算出結(jié)果:
// __FILE__ = "/home/LinuxZn/project/src/evhtp.c"
// __FILENAME__ = "evhtp.c"
因為是常量表達式,編譯器會在編譯期算好,最終二進制里只有文件名,完整路徑被優(yōu)化掉了。嵌入式系統(tǒng)每個字節(jié)都金貴,這招能省不少空間。
掌握這些宏技巧,能一定程度幫助我們寫出高性能又好維護的嵌入式代碼。記住一點:能用內(nèi)聯(lián)函數(shù)就用內(nèi)聯(lián)函數(shù),只有宏能解決的場景才上宏。宏雖強大,但別濫用。
