contributed by < haoyu0970624763
>
ab -n 100000 -c 500 -k http://127.0.0.1:8081/
gcc -std=gnu11 -Wall -Wextra -Werror -o htstress htstress.c -lpthread
./htstress -n 10000 -c 100 -t 8 localhost:8081
在 Computed goto for efficient dispatch tables 一文中提及, goto 在執行效率上比 switch-case 要更好,主要可以從 2 個層面探討:
If no converted case constant expression matches and there is no default label, no part of the switch body is executed.
branch prediction 的部份可參考老師之前撰寫的
switch-背後的-goto-和實作考量
我們透過 Labels as Values,在 http_parser.c 的兩個函式中加入 conditions 作為 label address table 讓 state 進行查表,並透過 goto 跳躍到對應的程式碼。
以 http_parse_request_body
conditions 為範例
const void *conditions[] = {
&&c_start,
&&c_key,
&&c_spaces_before_colon,
&&c_spaces_after_colon,
&&c_value,
&&c_cr,
&&c_crlf,
&&c_crlfcr,
};
接著用 goto 取代 switch
goto *conditions[state];
我用 perf 觀察原本的程式碼 以及 使用 computed goto 改寫後的效率
switch 版本的觀測紀錄如下
$ sudo perf stat ./sehttpd -p 8081
$ ./htstress -n 100000 -c 1 -t 4 http:/127.0.0.1:8081/
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socket errors: 0 [0%]
seconds: 4.626
requests/sec: 21617.794
Performance counter stats for './sehttpd -p 8081':
4,551.69 msec task-clock # 0.498 CPUs utilized
458 context-switches # 100.622 /sec
36 cpu-migrations # 7.909 /sec
100,070 page-faults # 21.985 K/sec
14,861,152,979 cycles # 3.265 GHz
10,629,920,230 instructions # 0.72 insn per cycle
2,043,158,393 branches # 448.879 M/sec
25,004,139 branch-misses # 1.22% of all branches
goto 版本的觀測紀錄如下
$ sudo perf stat ./sehttpd -p 8081
$ ./htstress -n 100000 -c 1 -t 4 http://127.0.0.1:8081/
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socket errors: 0 [0%]
seconds: 4.150
requests/sec: 24094.580
Performance counter stats for './sehttpd -p 8081':
4,093.64 msec task-clock # 0.207 CPUs utilized
359 context-switches # 87.697 /sec
34 cpu-migrations # 8.306 /sec
100,071 page-faults # 24.445 K/sec
13,730,441,736 cycles # 3.354 GHz
10,613,940,571 instructions # 0.77 insn per cycle
2,038,720,358 branches # 498.021 M/sec
24,397,318 branch-misses # 1.20% of all branches
可以看出在 computed goto 版本中的執行的 cycles 少於 switch 版本的 且 branch miss rate 也略少於 switch 版本
在改寫的過程中有參照 oscarshiang 的共筆,不過在測量的過程中我沒辦法觀測出 branch miss rate 有明顯的改善,我更傾向在這個實驗中 goto 之所以效能更好的原因在於減少了bounds checking
更改 buffer size 從 8124 更改為 8192 , 變成 2 的冪 , 我們就可以透過跟 (MAX_BUF-1) 做 bitwise-AND , 可取得 0~8191的範圍來取代 % 的運算 , 因為 & 運算比 % 來的更有效率
原本
p = (uint8_t *) &r->buf[pi % MAX_BUF];
更改成
p = (uint8_t *) &r->buf[pi & (MAX_BUF - 1)];
可將 pi % MAX_BUF
改為 pi & (MAX_BUF - 1)
,當 MAX_BUF
為 2 的冪
詳見: 你所不知道的 C 語言: bitwise 操作
節錄自 epoll 中
monitoring multiple file descriptors to see if I/O is possible on any of them
epoll 用來監聽 I/O 事件是否準備好了 , 而 epoll 又可分成
以 test_epoll_lt_and_et 舉例說明可能會講的更清楚
在客户端输入aabbcc,客户端發送包含 aabbcc 内容的數據包到服務端,在服務端一次可讀取2個字元的情況下
我們可以看出
此外, et 模式的效能會比 lt 來的更好 , 原因在於當 kernel 發送通知後 , 會把該事件從 ready list 中刪除 , 而 lt 模式則不會從 ready list 中刪除該事件 , 當 list中有越來越多事件時 , epoll 下次要通知用戶時 , 搜尋 list 的時間會增長
兩種模式各有其優缺點 , 在高效的 web server 中 , 我認為採用 edge-triggered 的方式可能會更合適
web server 需要同時間處理多項請求 , 要優先處理哪個請求就變成一個重要的課題 , 而常見的作法之一是先處理最快到期的請求
在 mainloop.c 中 server 接收請求後會先呼叫 timer.c 中的handle_expired_timers
函式處理已經超出期限但還沒處理的請求 , 接著會呼叫 add_timer
函式將新的請求加入 queue 中
而 add_timer
中根據 node->key = current_msec + timeout
(current_msec
表示當前的時間 , timeout
代表處理請求的最大延遲時間),顯而易見的可以發現 node->key 值代表的是此請求處理的期限
我們想取得最快到期的請求就可以透過 priority queue(MIN_HEAP)的方式獲取 , 也就是 timer.c
中的 prio_queue_min
函式