# Linux 核心專題: Redis/Valkey + io_uring
> 執行人: Andrewtangtang
> [解說影片](https://www.youtube.com/watch?v=Z7VOwKGi8Ec)
> [github](https://github.com/Andrewtangtang/1brc)
### Reviewed by `salmoniscute`
在先前的介紹中說明到 io_uring 的設計初衷:
減少傳統 epoll + read/write 模式的 syscall 以及減少 context switch
但是我在 `1BRC 任務中 io_uring 與 mmap 的性能比較` 這個實驗結果中感受到矛盾,這個 1BRC 的實驗場景是否不適合 io_uring ?
呈上,我想問把 io_uring 跟 mmap 比較的考量是什麼?他們兩者的實作機制大不相同,像是在後續的 io_uring v.s. epoll 比較,我就較能理解實驗設計跟結果的關係。
另外我覺得可以減少像是「上下文切換」這類翻譯!
> 在 `io_uring` 與 `mmap` 的比較部分,我的主要動機是想探討:在高並行度磁碟 I/O 任務中,哪一種資料載入模式能帶來更好的效能?儘管 `io_uring` 與 `mmap` 的機制確實存在本質差異(前者為 buffer-based I/O,後者為 page-fault ),但兩者都能實現大量檔案讀取。特別是在 1BRC 的資料分析任務中,讀取為主、寫入極少,且為一次性遍歷所有檔案內容的場景,我希望透過此對比觀察系統層面的開銷與執行效能之間的關係。
> 我原先的推論是:`mmap` 在多執行緒下可能因共用 page table 與頻繁分頁載入,導致更高的記憶體資源競爭;而 `io_uring` 採用 buffer 操作、結合批次提交,應能在高並行情境中展現出更好的延展性與穩定性。
然而實驗結果顯示,在多數情況下 `io_uring` 的 context switch 數反而普遍高於 `mmap`。我認為這與 kernel thread 的喚醒路徑與排程行為有關,後續會考慮透過更細緻的工具進一步觀察實際喚醒與切換路徑,以補強對此現象的解釋。
>至於 `io_uring` 是否適合 1BRC 這類場景,我目前的觀察是:在 4 執行緒時,系統的 SSD throughput 幾乎已達飽和,因此在後續提高並行度時,`io_uring` 所能帶來的提升非常有限。不過若在使用更高 bandwidth 的儲存裝置、或改以多檔案讀取等情境下重新測試,我預期 `io_uring` 的非同步優勢可能會更加明顯,但這仍有賴進一步實驗驗證。
>「上下文切換」等翻譯用詞的問題已經進行修改,謝謝你的提醒
### Reviewed by `Andrushika`
在 io_uring 和 mmap 的比較章節中,有提及為了讓原專案能在 ARM 架構上進行,而在原始程式碼基礎上新增了不依賴 SIMD 的版本。這樣的修改本身是否會對效率造成影響,造成修改後之版本的運行速度和原專案不同,而使實驗結果有所偏差?
> 本實驗的核心目的在於比較 `mmap` 與 `io_uring` 這兩種 I/O 模式下的系統層效能表現,而非著重於資料處理階段的數值計算效能。在 1BRC 任務中,絕大部分時間與 CPU 使用量集中於將檔案從磁碟載入使用者空間,因此我預期除 SIMD 優化對於兩種模式的相對比較影響極小。此外, `mmap` 與 `io_uring` 版本皆使用相同的非 SIMD 實作,在處理邏輯與資料來源完全一致的前提下,我認為仍可公平觀察其系統層面的行為差異,如 page fault 數、context switch 數與 syscall 數等指標。
### Reviewed by `leowu0411`
在 Linux 中,mmap 進行 page fault 處理時,是否因為需要使用鎖來保護 VM 結構與 page cache,所以在多執行緒同時 fault-in 時,必須以序列化的方式來處理請求?
而 io_uring 則能透過 kthread pool 排程 RQ 內的多個 I/O 請求,並利用預設的中斷或 polling thread 來同時監控這些請求的狀態,因此在高度並行情況下能夠表現得更好?
想詢問這兩者的效能差異是否主要來自上述原因?
>你的推論我認為是對的。在高並行場景下,`mmap` 的 page fault 路徑會在 page cache 與 PTE 更新時分別取得 page lock 、 PTE lock,並在缺頁時以同步 block I/O 方式讓觸發 fault 的執行緒進入睡眠,這些鎖競爭與序列化處理隨並行度提升而極易成為性能瓶頸;相對地,`io_uring` 將 I/O 請求非同步提交到核心 ,由 kthread 池以及 SQPOLL/IOPOLL polling thread 並行處理與監控完成狀態,避免了使用者執行緒在 page fault 路徑上的鎖與 sleep 開銷,在高度並行時能有效降低鎖競爭帶來的延遲。
### Reviewed by `tsaiiuo`
>io_uring vs mmap context-switch
在這個圖表中可以看到 io_uring 在 32 個執行序的運行下, context switchs 是遠小於 16 個執行序的,但為什麼反而在 32 個執行緒下效能反而下降?
> 目前我的推測是,在高並行(32 執行緒)情況下,`io_uring` 的效能下降可能來自於頻繁的 I/O 中斷與資源競爭,導致部分請求延遲或排程遲滯。而 context switch 數量雖然整體下降,但不一定代表系統實際更有效率,反而有可能是部分執行緒進入長時間等待、無法即時排程所造成。至於 context switch 數量下降的實際成因,我認為需要透過更細緻的事件追蹤工具進行觀察與分析,才能明確解釋這一現象。
>Linux 核心終於導入一套設計現代化、效能較好的非同步 I/O 機制,能有效降低系統呼叫成本、減少 context switch
另外看起來這段敘述也與實驗結果有矛盾,因為看起來 io_uring 的 syscalls 數量以及 context-switchs 數量都遠大於 mmap,那該如何解釋這種情況呢?
>我的原始敘述確實有瑕疵,未能清楚區分 `io_uring` 在不同模式下的行為,我會修正。以目前的實驗設定來看,我使用的是 `io_uring` 的預設中斷驅動模式(未啟用 SQPOLL/IOPOLL),因此每筆 I/O 請求都需要透過 `io_uring_enter()` 系統呼叫提交與等待完成,這也導致 syscall 次數明顯高於 `mmap`。而 `mmap` 雖然在觸發 page fault 時也會進入 kernel,但後續資料載入主要透過核心的 page fault 機制完成,不會額外產生 syscall,因此整體系統呼叫量較低。至於 context switch 數量,`io_uring` 因為每筆請求仍依賴中斷喚醒使用者線程,加上沒有集中輪詢機制,因此會導致大量的睡眠與喚醒切換,這點和我原先「能顯著降低 context switch」的說法確實相悖。
## 任務簡述
Redis 是款高效能的 key-value 資料結構儲存系統,可作為資料庫、快取及資料處理佇列使用。它將所有資料集都儲存在記憶體中,一次送出多筆請求以提升吞吐量並降低通訊延遲。與其他 key-value 快取產品相比,Redis 還具備以下特色:
1. 資料持久化: 可將記憶體中的資料定期或即時寫回磁碟,確保重啟後資料不會遺失。
2. 多種資料結構: 除了單純的 key-value 型別,還支援鏈結串列(list)、集合(set)、有序集合(zset)與雜湊表 (hash table) 等
3. 主從複製: 支援多台節點間的主從複製,實現讀寫分離與高可用性。
在 Redis 6.0 之前,採用單執行緒模型,由主執行緒負責所有客戶端請求的處理,包含 socket 讀取、封包解析、命令執行及 socket 寫出。它透過 ae 事件模型搭配 I/O 多工處理,以便高速回應請求。需要注意的是,自 Redis 4.0 起,背景會額外啟用一些執行緒來處理較慢的操作,但在 Redis 6.0 之前,真正的「單執行緒」指的仍是那個負責 socket 讀寫與命令處理的主執行緒。
Redis 6.0 之後,可選用一組獨立的 I/O 執行緒來處理 socket 的讀寫呼叫(預設未啟用)。主執行緒仍然負責 ae 事件通知,當有讀寫需求時,會將任務平均分派給 I/O 執行緒(主執行緒也會處理部分任務),並等待所有執行緒完成後再繼續後續流程。
阿里巴巴技術團隊引入 io\_uring 的非同步處理與 FAST\_POLL 機制後,Redis 在 event-poll 與 io\_uring 下的 QPS 表現對比如下:
* 在高負載情況下,io\_uring 相較於傳統 event-poll,可提升約 8%~11% 的吞吐量。
* 啟用 sqpoll 時,吞吐量可額外提升 24%~32%。
參照:
* [Anolis: Redis](https://openanolis.cn/sig/high-perf-storage/doc/218937424285597744)
2024 年 3 月 20 日,Redis, Inc. 正式將核心程式碼的授權方式從 BSD 三條款轉換為 Redis Source Available License v2(RSALv2)和 Server Side Public License v1(SSPLv1),這二種授權都非 OSI 認可的開放原始程式碼授權,旨在防止某些雲端服務商「搭便車」地使用 Redis 而不回饋程式碼或營收。
早在 2018 年,Redis 就已針對模組授權作出調整:那時將原本採用 AGPLv3 的模組,改為 Apache 2.0 加入 Commons Clause;到了 2019 年,又統一改採 RSAL,並明確禁止將模組用於資料庫、快取、串流處理、搜尋或 AI/ML 服務等商業模式的捆綁銷售。
為了延續 BSD 三條款的開放精神,Linux Foundation 於 2024 年 3 月 28 日推出 [Valkey 專案](https://valkey.io/),基於尚未改動授權的 Redis 7.2.4 分支,並持續以 BSD 三條款發布,迅速獲得 AWS、Google Cloud, Oracle, Ericsson 和 Snap Inc. 等多家廠商的支持。
隨後在 2025 年 5 月 1 日,Redis 宣布自 v8.0 起變更為 GNU AGPLv3 授權,並與 SSPLv1、RSALv2 共存,使 Redis 重新成為 OSI 認可的開放原始程式碼軟體。由於 Valkey 已建立起穩固的社群與生態,短期內這兩個專案將各自獨立發展。
本任務評估以下現有 Redis/Valkey 的 io\_uring 修改,重新評估和分析其效能表現,協助 code review,並嘗試在近期的 Redis 或 Valkey 予以驗證:
* [Add io_uring support to redis](https://github.com/redis/redis/pull/9440) / [對應的效能評比](https://github.com/redis/redis/issues/9441)
* Valkey: [Use io_uring to make fsync asynchronous when set appendfsync to always to make CPU more efficient](https://github.com/valkey-io/valkey/pull/599)
* Valkey: [Use io_uring to batch handle clients pending writes to reduce SYSCALL count](https://github.com/valkey-io/valkey/pull/112)
* Valkey: [Persist AOF file by io_uring](https://github.com/valkey-io/valkey/pull/750)
* Valkey: [Use MSG_ZEROCOPY for plaintext replication traffic](https://github.com/valkey-io/valkey/pull/1543)
參照: [Dragonfly vs. Valkey Benchmark: 4.5x Higher Throughput on Google Cloud](https://www.dragonflydb.io/blog/dragonfly-vs-valkey-benchmark-on-google-cloud)
## TODO: 探討 io_uring 原理和效益
> 研讀 [針對事件驅動的 I/O 模型演化: io_uring](https://hackmd.io/@sysprog/linux-io-model/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fiouring),指出何以 [io_uring](https://man7.org/linux/man-pages/man7/io_uring.7.html) 能夠加速網路伺服器的運作,又如何細緻地調整 I/O。搭配以下影片:
* [io_uring: So Fast. It's Scary.](https://youtu.be/AaaH6skUEI8)
* [Zero Copy Receive using io_uring](https://youtu.be/LCuTSNDe1nQ)
* [The Evolution of Linux I/O Models: A Path towards IO_uring](https://youtu.be/WizG3TT-zHo)
### I/O 模型的演進與挑戰
現今網路伺服器的 I/O 的模式大多使用 synchronous I/O 並搭配 I/O 多工(例如 `epoll` )機制來監聽網路 socket 事件來確認是否有新事件後再以系統呼叫進行 non-blocking I/O 操作,然而這也意味著對於每一次的 I/O 事件至少需要兩次的系統呼叫,一次為 I/O 事件的收割 (reap) 如 `epoll_wait()` ,接下來的系統呼叫才是真正的 I/O 操作如 `read()` 和 `write()`,然而隨著近年來的處理器的安全 Spectre 與 Meltdown 處理器的安全漏洞被揭露,linux 核心引入了 KPTI 完全分離使用者空間與核心空間頁表來解決洩漏的問題,這項機制讓每次的系統呼叫都會重新 flush TLB 造成極大的效能瓶頸,僅管新的處理器支援 PCID 在一定程度上緩解了 TLB flush 的問題,模式切換所帶來的延遲仍舊明顯,尤其在高頻 I/O 場景下更顯得成本高昂。
為了在 I/O 週期內盡量減少系統呼叫次數,asynchronous I/O(非同步 I/O) 機制成為解決此問題的最佳途徑。它僅需一次系統呼叫,即可讓 I/O 操作在核心空間背景中非同步執行,主應用程式則可繼續處理其他邏輯。當核心完成 I/O 操作後,透過 signal 或其他通知機制,提醒主程式該操作已完成。這樣的設計可將 I/O 延遲隱藏於核心內部,避免傳統 `read()`、`write()` 等阻塞操作造成的應用程式停滯,有效提升整體效能。
過去 Linux 曾實作過兩種非同步 I/O 機制,分別為 glibc 的 POSIX AIO 與 Linux native AIO。儘管這兩者在理論上對於高效能網路伺服器具有潛力,實務上卻因部署與維護困難、API 設計不夠直觀,導致實際被廣泛採用的案例並不多,限制了其在高速網路伺服器中的應用發展。隨著 `io_uring` 在 Linux v5.1 中問世,Linux 核心終於導入一套設計現代化、效能較好的非同步 I/O 機制,能有效降低系統呼叫成本,並支援批次提交與完成通知,為高效能伺服器場景提供了新的解決方案。
### io_uring 的設計原理與效能優勢
#### 共享記憶體與環形緩衝區
io_uring 會在使用者空間與核心空間之間建立一塊共享記憶體區域,並在其中配置兩個 lock-free 的環形緩衝區:
- **提交佇列 (Submission Queue, SQ)**
- 生產者:應用程式將 I/O 請求填入 SQE 陣列,並將對應的索引寫入 SQ 的索引環形陣列
- 消費者:核心從索引環形陣列 head 開始讀取索引,再取出相應的 SQE 執行 I/O 操作
- **完成佇列 (Completion Queue, CQ)**
- 生產者:核心將已完成的 I/O 結果封裝進 CQE,寫入 CQ 的環形陣列
- 消費者:應用程式從 CQ 的環形陣列讀取 CQE,以獲得操作結果

> 取自 [Why you should use io_uring for network I/O](https://developers.redhat.com/articles/2023/04/12/why-you-should-use-iouring-network-io)
這塊共享記憶體可以讓應用程式與核心之間,高效的交換 I/O 請求與結果資訊,除此之外,為了可以符合多核環境中達成高效同步,該環形緩衝區採以 lock free 且單一消費者、單一生產者的形式。對於 SQ 只有應用程式可以寫入更新 tail,也只有核心的有可讀取更新 head;對於 CQ 則相反,只有應用程式可以讀取更新 head 而也只有核心可以寫入更新 tail,並藉由 atomic 操作和 memory barrier 管理頭尾指標可以有效的避免傳統 lock 帶來的開銷。
#### 批次提交機制
io_uring 的環形提交佇列允許應用程式先行批次填入多筆 SQE 請求,再透過一次 `io_uring_enter()` 系統呼叫,一次性將所有請求提交給核心執行對應的 I/O 操作。此外,在某些極致效能要求的場景中,可以啟用 `SQPOLL` (提交佇列輪詢) 模式,由核心背景執行緒主動監控 SQ 的變化,從而達成幾乎零系統呼叫的 I/O 請求。綜合以上優勢,與傳統 epoll 搭配 non-blocking I/O 的模型相比(每次 I/O 至少需兩次系統呼叫),io_uring 大幅減少了高頻 I/O 場景下的模式切換開銷,可以顯著提升 throughput 與延遲表現。
## 1BRC 任務中 io_uring 與 mmap 的性能比較
[十億列挑戰 (1BRC)](https://github.com/gunnarmorling/1brc) 是一項始於 2024 年初的程式設計挑戰,旨在探索現代 Java 在處理大規模資料時的效能極限。任務內容是從一個包含十億筆氣象站溫度紀錄的文字檔中,計算每個氣象站的最低溫、平均溫與最高溫,並依照站名排序後輸出結果。為了探討不同 I/O 模型在 I/O 密集型任務中的實際效能差異,本實驗改採 [Gediminas/1brc](https://github.com/Gediminas/1brc) 提供的 Linux/C 實作版本並比較 `mmap` 與 `io_uring`,實驗將聚焦於多核心環境中處理大型檔案時的延遲、吞吐量與系統資源使用,藉此評估 `io_uring` 在實際應用場景上效能的效益。
### 實驗環境與設定
#### 實驗環境
``` bash
$ lscpu
Architecture: aarch64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 32
On-line CPU(s) list: 0-31
Vendor ID: APM
Model name: -
Model: 2
Thread(s) per core: 1
Core(s) per socket: 32
Socket(s): 1
Stepping: 0x3
Frequency boost: disabled
CPU(s) scaling MHz: 18%
CPU max MHz: 3000.0000
CPU min MHz: 375.0000
BogoMIPS: 80.00
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
Caches (sum of all):
L1d: 1 MiB (32 instances)
L1i: 1 MiB (32 instances)
L2: 4 MiB (16 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-31
Vulnerabilities:
Gather data sampling: Not affected
Itlb multihit: Not affected
L1tf: Not affected
Mds: Not affected
Meltdown: Mitigation; PTI
Mmio stale data: Not affected
Reg file data sampling: Not affected
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Vulnerable
Spectre v1: Mitigation; __user pointer sanitization
Spectre v2: Vulnerable
Srbds: Not affected
Tsx async abort: Not affected
$ gcc --version
$ gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ PKG_CONFIG_PATH=$HOME/lib/pkgconfig pkg-config --modversion liburing
2.11
```
為了提升測試的一致性與準確度,本實驗每次運行程式前皆會清除 pagecache 、 dentries 及 inodes
```shell
echo 3 | sudo tee /proc/sys/vm/drop_caches
```
由於原始專案 [Gediminas/1brc](https://github.com/Gediminas/1brc) 是針對 Intel x86\_64 架構開發,並使用了 AVX2 等 SIMD 指令,因此部分實作與指令集僅適用於該平台。本次實驗是在 ARM 架構(aarch64)平台上進行,為了保留原始程式碼的完整性,我在原始程式碼基礎上新增了不依賴 SIMD 的版本,使其能在 ARM 環境下正確執行,未來將補充對 ARM NEON 等平台 SIMD 指令集的支援,使程式在不同架構下亦能對應各自的 SIMD 指令進行實作與驗證。
#### 實驗目標
比較 `mmap` 和 `io_uring` 在 1、2、4、8、16、32 執行緒下的平均運行時間,並使用 perf 觀察在多執行緒環境下各項系統層級的成本,並嘗試開啟 `IOPOLL` 模式使 `io_uring` 加速。資料集使用 [1BRC](https://github.com/gunnarmorling/1brc) 程式碼產生 10 億行氣象站溫度資料文字檔,檔案大小共 13.7 G。
### 測試結果
#### 執行時間
- [ ] io_uring vs mmap excution time (**with page cache**)

```
| Threads | mmap Mean Time (s) | io_uring Mean Time (s) | Time Difference (s) | io_uring Speedup |
| ------- | ------------------ | ----------------------- | ------------------- | ----------------- |
| 1 | 34.276 ± 0.050 | 36.492 ± 0.080 | +2.216 | –6.46 % |
| 2 | 17.351 ± 0.005 | 18.251 ± 0.051 | +0.900 | –5.19 % |
| 4 | 8.958 ± 0.003 | 9.161 ± 0.023 | +0.203 | –2.27 % |
| 8 | 4.747 ± 0.002 | 4.604 ± 0.002 | –0.143 | +3.01 % |
| 16 | 2.646 ± 0.004 | 2.339 ± 0.014 | –0.307 | +11.60 % |
| 32 | 1.658 ± 0.014 | 1.334 ± 0.008 | –0.324 | +19.54 % |
```
該數據使用了原作者在專案中 [Gediminas/1brc](https://github.com/Gediminas/1brc) 提供的命令
```shell!
echo 3 | sudo tee /proc/sys/vm/drop_caches
hyperfine --warmup=1 --runs=3 "./bin/1brc $FILE"
```
其中,第一行命令旨在清除 Linux 系統中的 page cache、dentries 與 inodes,以模擬在完全沒有任何快取的情境讀取的效能表現。然而,實驗過程中觀察到 hyperfine 這個工具的 `--warmup=1` 參數會在正式測量前先執行一次程式,導致資料提早被 fault-in 至記憶體。此行為使得後續三次測量皆處於已有 page cache 的狀態,未能如預期模擬出連續測量在完全沒有 page cache 的執行情境。為了修正此問題,我重新設計了測試流程,捨棄 hyperfine,改以手動迴圈控制。新的測試命令與結果如下
```shell
for run in {1..3}; do
echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
/usr/bin/time -v ./bin/1brc $FILE
done
```
- [ ] io_uring vs mmap excution time (**without page cache**)

```
| Threads | mmap Mean Time (ms) | io_uring Mean Time (ms) | Time Difference (ms) | io_uring Speedup|
| ------- | ------------------- | ------------------------ | -------------------- | --------------- |
| 1 | 38763.3 ± 18.9 | 53746.7 ± 225.4 | +14983.3 | –38.65 % |
| 2 | 33646.7 ± 95.3 | 34463.3 ± 38.6 | +816.7 | –2.43 % |
| 4 | 27936.7 ± 17.0 | 26110.0 ± 16.3 | –1826.7 | +6.54 % |
| 8 | 27920.0 ± 8.2 | 26060.0 ± 8.2 | –1860.0 | +6.66 % |
| 16 | 27950.0 ± 21.6 | 26033.3 ± 9.4 | –1916.7 | +6.86 % |
| 32 | 28140.0 ± 29.4 | 33136.7 ± 53.1 | +4996.7 | –17.76 % |
```
從測試結果可以觀察到:
- **在單執行緒情境下,`mmap` 顯著優於 `io_uring`**。
這是因為 `mmap` 透過 page fault 機制按需載入資料,對於連續讀取且無多工的場景簡單高效;而 `io_uring` 每次 I/O 操作仍需進行 submit 與完成流程處理,並仰賴中斷(IRQ)喚醒。這種額外開銷在低並行時難以攤平,反而成為效能瓶頸。
* **在 4~16 執行緒的區段,`io_uring` 開始展現優勢,平均可比 `mmap` 快約 6%**。
這代表當多個執行緒同時提交 I/O 時,`io_uring` 的 batched I/O 機制與非同步完成路徑能有效提升總體效能;而 `mmap` 則可能因 page fault 帶來鎖競爭與快取一致性(cache coherence)開銷,使得效能難以線性成長。
* **在高並行(32 threads)下,`io_uring` 效能反而下降**。
從圖中可見,`io_uring` 在 4 執行緒之後的執行時間趨於平緩甚至反彈,推測可能原因包括:
- SSD 裝置吞吐已達上限,不再隨執行緒數成長;
- 在飽和的情況下每個 thread 各自 submit/wait,造成系統呼叫與核心內部資源的競爭,反而引發額外的等待與快取干擾。
#### 系統事件
為了觀察兩種模式下的 page fault 與 context switch 行為,我使用 perf 收集相關統計資料。由於 `io_uring` 在處理讀取請求時,會透過 kernel threads 與底層儲存裝置進行資料搬移,再將資料複製至 user space buffer,因此在效能分析上,除了使用者行程本身的行為外,也需納入 kernel threads 所產生的事件加以考量。考量到本實驗環境中的背景干擾極少,我也額外補充了程式執行期間的 system-wide page fault 與 context switch 數據,以輔助解讀整體行為差異。以下的數據皆是在完整清除頁面快取的環境下進行量測
- [ ] io_uring vs mmap page-faults

```
| Threads | mmap Page Faults | io_uring Page Faults |
| ------- | ---------------- | --------------------- |
| 1 | 210,489 | 169 |
| 2 | 210,532 | 236 |
| 4 | 210,613 | 376 |
| 8 | 210,779 | 651 |
| 16 | 211,093 | 1,201 |
| 32 | 211,745 | 2,302 |
```
- [ ] io_uring vs mmap system-wide page-faults

```
| Threads | mmap Page Faults | io_uring Page Faults |
|---------|------------------|----------------------|
| 1 | 211,430 | 1,107 |
| 2 | 211,052 | 1,175 |
| 4 | 211,375 | 1,319 |
| 8 | 210,783 | 1,594 |
| 16 | 212,745 | 1,997 |
| 32 | 212,524 | 2,525 |
```
##### page fault 結果解讀
從圖表中可以明確看出,`mmap` 模式下的 page fault 數量始終穩定地維持在約 21 萬次上下,即使執行緒數增加也幾乎不變。這是因為 `mmap` 透過 demand paging 的方式,會在每次讀取未載入的檔案頁面時觸發 page fault,由核心載入對應資料至 page cache。相較之下,`io_uring` 的 page fault 數量極低,從單執行緒的約 169 次到 32 執行緒的 2300 次,僅為 `mmap` 的 千分之一以內。這反映出 `io_uring` 在讀取流程中並非透過 page fault 被動載入資料,而是採用明確的非同步提交機制:應用程式先提交 I/O 請求,由核心內部的工作執行緒(例如 kthread)主動發起資料讀取,並將結果複製到使用者提供的緩衝區。雖然資料搬移仍經過 TLB 和 page table 轉換,但由於整體流程避開了 page cache 與 lazy loading 的機制,因此 page fault 數量相較 mmap 顯著降低。
- [ ] io_uring vs mmap context-switch

```
| Threads | mmap Context Switches | io_uring Context Switches |
| ------- | --------------------- | -------------------------- |
| 1 | 14,783 | 43,535 |
| 2 | 25,803 | 79,998 |
| 4 | 31,835 | 105,837 |
| 8 | 34,623 | 109,217 |
| 16 | 35,275 | 107,890 |
| 32 | 48,375 | 66,565 |
```
- [ ] io_uring vs mmap system-wide context-switch

```
| Threads | mmap Context Switches | io_uring Context Switches|
| ------- | --------------------- | ------------------------ |
| 1 | 115928 | 617812 |
| 2 | 262413 | 629726 |
| 4 | 261881 | 611049 |
| 8 | 269715 | 690077 |
| 16 | 405964 | 727924 |
| 32 | 516444 | 721376 |
```
##### context switch 結果解讀
從結果來看, `mmap` 模式下的 context switch 數量相對穩定,隨著執行緒數從 1 到 32 條緩步上升,由約 1.5 萬增加至 4.8 萬;而 `io_uring` 則在 4 執行緒後 `context switch` 數急遽上升,在 8~16 個執行緒區間突破 10 萬,雖於 32 個執行緒稍微下降,但仍高於 `mmap`。造成此現象的可能原因是,`io_uring` 雖為非同步模型,但在未啟用 SQPOLL / IOPOLL 等核心協助機制時,每個執行緒在等待 CQE 時仍需透過 `io_uring_enter()` 進入核心睡眠並等待中斷喚醒,因此每筆 I/O 都可能觸發一次 thread 的排程切出與喚醒,形成大量 context switch。此外,實際的 I/O 操作仍是由核心 thread(如 `iou-wrk` )代為完成,因此 kernel thread 與 user thread 間的交替排程也會進一步推升切換數量。
- [ ] io_uring vs mmap syscall

```
| Threads | mmap Syscalls | io_uring Syscalls |
| ------- | ------------- | ------------------ |
| 1 | 86 | 177,767 |
| 2 | 97 | 207,986 |
| 4 | 117 | 237,334 |
| 8 | 164 | 243,414 |
| 16 | 250 | 245,022 |
| 32 | 423 | 201,505 |
```
## io_uring IOPOLL 模式下的 1BRC 性能表現
io_uring 提供兩種進階的輪詢模式,能有效減少系統呼叫次數並避開裝置中斷(IRQ)對系統效能的干擾,特別適用於 I/O 密集的場景如 1BRC:
1. **核心提交輪詢(Kernel-Side Polling - SQ Polling)**
透過啟用 `IORING_SETUP_SQPOLL`,核心會建立一條 `iou-sqp` 執行緒主動掃描 SQ,應用程式無需透過系統呼叫提交 I/O 請求,即可持續發送任務,降低 syscall 開銷。
2. **完成端輪詢(CQ Polling - IOPOLL 模式)**
設定 `IORING_SETUP_IOPOLL` 後,核心會在 I/O 完成端對儲存裝置執行忙碌輪詢(busy polling),主動等待硬體完成資料傳輸,完全跳過中斷機制,適用於低延遲需求與 `O_DIRECT` 的原始裝置場景。

> 取自 [Understanding Modern Storage APIs: A systematic study of libaio, SPDK, and io_uring](https://atlarge-research.com/pdfs/2022-systor-apis.pdf)
### 使 1BRC io\_uring 測試具備 IOPOLL 模式
> commit: [a94a93d](https://github.com/axboe/liburing/commit/a94a93d58f419a83d344b87d90c12763166e6f25)
在啟用 `IORING_SETUP_IOPOLL` 模式時,有一項關鍵條件需特別注意:
I/O 目標必須是原始區塊裝置(raw device),且必須同時以 `O_DIRECT` 開啟檔案。這是因為: `O_DIRECT` 可讓讀寫操作繞過 page cache,直接由硬體對 user space buffer 執行 DMA。這使得核心能在硬體完成 I/O 後,主動輪詢設備的完成狀態,並將結果填入 CQE,不再依賴 IRQ 中斷來通知應用程式。相關的討論可以參看[liburing#345](https://github.com/axboe/liburing/issues/345)
然而,`O_DIRECT` 也伴隨一系列嚴格的對齊要求:
* 使用者提供的 buffer 必須對齊至裝置的區塊大小(通常為 512 bytes 或 4096 bytes)
* 讀取的 offset 必須為區塊大小的整數倍
* 讀取的長度亦須為區塊大小的整數倍
若不符合這些條件,會導致系統呼叫失敗,返回 `EINVAL` 錯誤,甚至出現 segmentation fault。

> 取自《[User Mode Linux](https://flylib.com/books/en/2.275.1.50/1/)》, Jeff Dike
#### 測試結果
- [ ] io_uring vs io_uring IOPOLL

```
| Threads | io_uring Mean Time (s) | io_uring IOPOLL Mean Time (s) | Time Difference (s) | IOPOLL Speedup |
| ------- | ---------------------- | ----------------------------- | ------------------- | -------------- |
| 1 | 53.75 ± 0.27 | 88.03 ± 1.58 | +34.28 | -63.77 % |
| 2 | 34.46 ± 0.05 | 46.74 ± 0.65 | +12.28 | -35.64 % |
| 4 | 26.11 ± 0.02 | 28.37 ± 0.14 | +2.26 | -8.66 % |
| 8 | 26.06 ± 0.01 | 26.94 ± 0.00 | +0.88 | -3.38 % |
| 16 | 26.03 ± 0.01 | 27.05 ± 0.33 | +1.02 | -3.92 % |
| 32 | 33.14 ± 0.07 | 26.32 ± 0.02 | -6.82 | +20.58 % |
```
從實驗結果來看,啟用 IOPOLL 模式並未在整體上帶來預期中的效能提升。特別是在低並行情境(如 1~4 執行緒)下,效能甚至顯著劣於未啟用 IOPOLL 的一般 `io_uring` 模式。我認為這是因為,當使用者透過 `io_uring_setup()` 開啟 `IORING_SETUP_IOPOLL` 旗標後,每筆 I/O 請求將不再透過中斷(IRQ)機制完成,而是要求使用者執行緒進入核心後主動進行 busy polling —— 也就是在提交請求後,thread 會持續在 kernel 輪詢該請求是否完成,直到獲得結果為止。在 I/O 請求頻率較低、SSD 尚未飽和的情況下,這樣的輪詢行為反而成為效能瓶頸。由於下一次 I/O 尚未送出,thread 在空檔期會不斷進行無效輪詢,持續耗用 CPU,包含大量 MMIO 讀取、cache reload 以及分支迴圈等成本。這些輪詢開銷在某些情況下甚至超過了一次中斷與喚醒所需的延遲,因此整體執行時間被拉長。這也可能可以解釋了為什麼在單執行緒下,IOPOLL 的效能反而更差。相對而言,傳統的 IRQ 驅動路徑雖然涉及中斷處理與喚醒開銷,但能在等待期間讓出 CPU,整體在單執行緒與 I/O 空檔較多時反而更有效率。唯有在高並行下(如 32 執行緒),當 SSD 幾乎處於全速運作、中斷過於頻繁成為負擔時,IOPOLL 模式才能展現其優勢。此時避免中斷處理成本、以低延遲方式快速確認完成狀態,有助於維持接近裝置極限的穩定吞吐量。
### 啟用 pthread_setaffinity 綁定執行 CPU
> commit:[bf15cae](https://github.com/Gediminas/1brc/commit/bf15cae688c9dfcf72b3b1ce905e2f2b1e241a8c)
上面有提到就結果來看 `io_uring` 的 context switch 數量高,可能是因為 I/O 提交與完成等待涉及多次系統呼叫與排程切換,加上 CPU 排程器可能會將 thread 排程至不同實體處理器核,導致頻繁的 CPU migration。這樣不僅會帶來 TLB flush 與快取失效(cache invalidation),更會拉高整體 context switch 次數與延遲。為了進一步釐清這些切換是否來自核心間的遷移,我決定加入 `pthread_setaffinity_np()` 明確將每條使用者執行緒綁定到特定實體 CPU 上,避免作業系統在執行過程中隨意切換 CPU 核心,藉此觀察是否能降低 context switch 次數與提升效能穩定性。
#### 程式改動
為了實驗不同 CPU 核心間 thread migration 對 `io_uring` 效能的影響,我在原本的 `pthread_create` 呼叫流程中加入了 CPU affinity 設定機制。具體做法是額外撰寫一個 `thread_wrapper_with_affinity()` 包裝函式,該函式會根據傳入參數中的 `cpu_core` 欄位,使用 `pthread_setaffinity_np()` 將該 thread 綁定至指定實體處理器核,以避免執行期間被 CPU scheduler 隨意排程到不同核心,該 wrapper 仍保留原先的判斷邏輯,根據是否使用 `mmap` 模式,選擇呼叫對應的處理函式(`process_fsegment_mmap()` 或 `process_fsegment_uring()`),以確保程式功能正常運作。最終將 `pthread_create()` 統一改為呼叫這個 wrapper 函式,使每條 thread 在啟動時都可依需求設定位址綁定策略。透過這樣的改動,我可以進一步觀察在排除 CPU migration 的情況下,`io_uring` 的 context switch 行為是否改善,並對效能變化進行分析。
``` diff
+void* thread_wrapper_with_affinity(void* arg) {
+ thread_info_t *thread_info = (thread_info_t*)arg;
+
+ if (thread_info->cfg.cpu_affinity) {
+ cpu_set_t cpuset;
+ CPU_ZERO(&cpuset);
+ CPU_SET(thread_info->cpu_core, &cpuset);
+
+ int ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
+ if (ret != 0) {
+ fprintf(stderr, "Warning: Failed to set CPU affinity for thread on core %d: %s\n",
+ thread_info->cpu_core, strerror(ret));
+ }
+ }
+
+ if (thread_info->cfg.mmap) {
+ return process_fsegment_mmap(arg);
+ } else {
+ return process_fsegment_uring(arg);
+ }
+}
for (int i = 0; i < cfg.cpus; i++) {
- if (cfg.mmap) {
- const int ret = pthread_create(&th_args[i].tid, NULL, process_fsegment_mmap, &th_args[i]);
- ___EXPECT(ret == 0, "thread create");
- }
- else {
- const int ret = pthread_create(&th_args[i].tid, NULL, process_fsegment_uring, &th_args[i]);
- ___EXPECT(ret == 0, "thread create");
- }
+ const int ret = pthread_create(&th_args[i].tid, NULL, thread_wrapper_with_affinity, &th_args[i]);
+ ___EXPECT(ret == 0, "thread create");
}
```
#### 測量結果
- [ ] io_uring vs io_uring IOPOLL (CPU affinity)

```
| Threads | io_uring (Affinity) Mean Time (s) | io_uring IOPOLL (Affinity) Mean Time (s) | Time Difference (s) | IOPOLL (Affinity) Speedup |
| ------- | --------------------------------- | ------------------------------------------ | ------------------- | ------------------------- |
| 1 | 54.38 ± 0.08 | 78.91 ± 0.15 | +24.53 | -45.11 % |
| 2 | 34.91 ± 0.12 | 45.90 ± 0.35 | +10.99 | -31.48 % |
| 4 | 26.02 ± 0.03 | 28.19 ± 0.04 | +2.17 | -8.34 % |
| 8 | 26.13 ± 0.03 | 26.61 ± 0.01 | +0.48 | -1.84 % |
| 16 | 26.25 ± 0.18 | 26.57 ± 0.01 | +0.32 | -1.22 % |
| 32 | 33.95 ± 0.37 | 26.36 ± 0.01 | -7.59 | +22.36 % |
```
從實驗結果來看,啟用 CPU affinity確實小幅改善了 IOPOLL 在低並行下的效能劣勢,顯示 thread migration 所造成的額外開銷確實會影響延遲與吞吐。然而整體而言,這項調整對 `io_uring` 模式的總體效能提升幅度有限。我推測原因在於本實驗執行前已刻意清除 page cache,使所有資料讀取皆須直接透過 SSD 進行,而非由快取加速。在這樣的前提下,I/O 裝置的實體吞吐能力(bandwidth 與 IOPS)成為主要瓶頸。觀察所有結果可見,當執行緒數增加至 4 條後,效能即趨於平緩甚至略為下降,無論是否啟用 affinity 均無法再有效推升吞吐,顯示 SSD bandwidth 或 queue depth 已接近飽和。此時額外提升 CPU 計算資源、降低 context switch 或 thread migration 的開銷,已無法換取實質效能改善。這也反映出在 I/O 密集情境中,儘管 I/O 模型與排程策略會影響系統層的調度開銷與延遲,但整體效能最終仍需考慮底層儲存裝置的硬體性能。
> TODO: 嘗試調整 io_uring 的 SQ/CQ ring 大小、每次 I/O 的 buffer size,觀察其對 io_uring 效能的影響
## TODO: 重現 Redis + io_uring 實驗
> 針對 [Add io_uring support to redis](https://github.com/redis/redis/pull/9440),重新實驗並比對[其效能評比](https://github.com/redis/redis/issues/9441)和 [Anolis: Redis](https://openanolis.cn/sig/high-perf-storage/doc/218937424285597744),探討效能表現,注意 Processor affinity 的設定。
### 實驗環境
```shell
$ lscpu
Architecture: aarch64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 32
On-line CPU(s) list: 0-31
Vendor ID: APM
Model name: -
Model: 2
Thread(s) per core: 1
Core(s) per socket: 32
Socket(s): 1
Stepping: 0x3
Frequency boost: disabled
CPU(s) scaling MHz: 18%
CPU max MHz: 3000.0000
CPU min MHz: 375.0000
BogoMIPS: 80.00
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
Caches (sum of all):
L1d: 1 MiB (32 instances)
L1i: 1 MiB (32 instances)
L2: 4 MiB (16 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-31
$ gcc --version
$ gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ PKG_CONFIG_PATH=$HOME/lib/pkgconfig pkg-config --modversion liburing
2.11
```
### 實驗目標
本次實驗將使用 [Anolis: Redis](https://openanolis.cn/sig/high-perf-storage/doc/218937424285597744) 並搭配 memtier-benchmark 測試 `epoll` 、 `io_uring` 、 `io_uring+sqpoll` 三種模式下的在讀寫比例 1:1 與 100:1 場景下的性能表現。
#### 組態設定
為避免 Redis 伺服器端與負載生成端之間產生資源競爭,影響測試結果的準確性與穩定性,我將 Redis Server 綁定於 CPU 0~4 處理器核心上執行,如果 io_uring 開啟 SQPOLL 模式的話則會將 iou-sqp kernel threads 額外綁定於 CPU 9 處理器上,而將負責產生請求的 memtier_benchmark 工具綁定於 CPU 5~8 處理器核心上執行。在負載產生方面,memtier_benchmark 的參數配置使用預設為啟動 4 條執行緒,每條執行緒建立 50 條 TCP client 連線,總共建立 200 條並行連線。每條連線將發出 20,000 筆請求(--requests=100000),因此整體總請求數為 200 × 20,000 = 4,000,000 筆,提供足夠的樣本數進行後續統計分析。為使測試資料更貼近真實世界應用,key 採用完全隨機的模式(--key-pattern=R:R),key 值範圍設定為 1 至 1,000,000(--key-minimum=1,--key-maximum=1000000),value 則設定為 512 bytes 並採用隨機內容(--random-data),以避免因資料重複導致 Redis 快取命中率異常,進而影響測試公正性。
```shell
$ taskset -c 0-4 ./redis-server
$ taskset -c 5-8 memtier_benchmark \
-s 127.0.0.1 \
-p 6379 \
--ratio=1:1 \
--key-pattern=R:R \
--key-minimum=1 \
--key-maximum=1000000 \
-d 512 \
--pipeline=1 \
--random-data \
--requests=100000 \
--print-all-runs \
--json-out-file=out-light.json \
```
### 讀寫比例 1:1
#### 吞吐量
- [ ] SET OPS/sec

- [ ] GET OPS/sec

#### 平均延遲
- [ ] SET Average Latency

- [ ] GET Average Latency

#### P50 延遲
- [ ] SET p50 Latency

- [ ] GET p50 Latency

#### P99 延遲
- [ ] SET p99 Latency

- [ ] GET p99 Latency

#### P99.9 延遲
- [ ] SET p99.9 Latency

- [ ] GET p99.9 Latency

#### 測試結果
**SET 測試結果**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|----------|----------|-----------------|------------------------|-------------------------------|
| Throughput (Ops/sec) | 27351.1 | 30069 | 34613.4 | +9.9% | +26.6% |
| Mean Latency (ms) | 3.672 | 3.308 | 2.881 | -9.9% | -21.5% |
| Median (P50) (ms) | 3.551 | 3.295 | 2.863 | -7.2% | -19.4% |
| P99 (ms) | 7.135 | 5.055 | 4.511 | -29.2% | -36.8% |
| P99.9 (ms) | 7.551 | 6.559 | 5.887 | -13.1% | -22.0% |
```
**GET 測試結果**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|----------|----------|------------------|-------------------------|--------------------------------|
| Throughput (Ops/sec) | 27351.1 | 30069 | 34613.4 | +9.9% | +26.6% |
| Mean Latency (ms) | 3.67 | 3.333 | 2.887 | -9.2% | -21.3% |
| Median (P50) (ms) | 3.551 | 3.343 | 2.879 | -5.9% | -18.9% |
| P99 (ms) | 7.135 | 5.023 | 4.511 | -29.6% | -36.8% |
| P99.9 (ms) | 7.551 | 6.463 | 5.919 | -14.4% | -21.6% |
```
**操作結果總和**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|----------|----------|-----------------|------------------------|-------------------------------|
| Throughput (Ops/sec) | 54702.2 | 60138 | 69226.8 | +9.9% | +26.6% |
| Mean Latency (ms) | 3.671 | 3.321 | 2.884 | -9.5% | -21.4% |
| Median (P50) (ms) | 3.551 | 3.327 | 2.879 | -6.3% | -18.9% |
| P99 (ms) | 7.135 | 5.023 | 4.511 | -29.6% | -36.8% |
| P99.9 (ms) | 7.551 | 6.527 | 5.919 | -13.6% | -21.6% |
```
### 讀寫比例 100:1
#### 吞吐量
- [ ] SET OPS/sec

- [ ] GET OPS/sec

#### 平均延遲
- [ ] SET Average Latency

- [ ] GET Average Latency

#### P50 延遲
- [ ] SET p50 Latency

- [ ] GET p50 Latency

#### P99 延遲
- [ ] SET p99 Latency

- [ ] GET p99 Latency

#### P99.9 延遲
- [ ] SET p99.9 Latency

- [ ] GET p99.9 Latency

#### 測試結果
**SET 測試結果**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|---------|----------|-----------------|------------------------|-------------------------------|
| Throughput (Ops/sec) | 575.82 | 621.39 | 700.03 | +7.9% | +21.6% |
| Mean Latency (ms) | 3.5 | 3.217 | 2.851 | -8.1% | -18.5% |
| Median (P50) (ms) | 3.391 | 3.215 | 2.847 | -5.2% | -16.0% |
| P99 (ms) | 6.815 | 4.927 | 4.383 | -27.7% | -35.7% |
| P99.9 (ms) | 7.391 | 5.887 | 5.759 | -20.3% | -22.1% |
```
**GET 測試結果**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|----------|-----------|-----------------|------------------------|-------------------------------|
| Throughput (Ops/sec) | 57295.7 | 61829.6 | 69654.9 | +7.9% | +21.6% |
| Mean Latency (ms) | 3.499 | 3.198 | 2.84 | -8.6% | -18.8% |
| Median (P50) (ms) | 3.391 | 3.199 | 2.831 | -5.7% | -16.5% |
| P99 (ms) | 6.815 | 4.863 | 4.319 | -28.6% | -36.6% |
| P99.9 (ms) | 7.327 | 6.143 | 5.535 | -16.2% | -24.5% |
```
**操作結果總和**
```
| Metric | epoll | io_uring | io_uring+sqpoll | io_uring improvement % | io_uring+sqpoll improvement % |
|----------------------|----------|-----------|-----------------|------------------------|-------------------------------|
| Throughput (Ops/sec) | 57871.6 | 62451 | 70354.9 | +7.9% | +21.6% |
| Mean Latency (ms) | 3.499 | 3.198 | 2.84 | -8.6% | -18.8% |
| Median (P50) (ms) | 3.391 | 3.199 | 2.831 | -5.7% | -16.5% |
| P99 (ms) | 6.815 | 4.863 | 4.319 | -28.6% | -36.6% |
| P99.9 (ms) | 7.327 | 6.143 | 5.535 | -16.2% | -24.5% |
```
### 系統層級事件
使用 perf stat 以及 strace 觀察 `redis-server` 進行讀寫比例 1:1 測試時的各項事件
#### epoll
```
Performance counter stats for process id '1188004':
236,069,763,081 cycles # 3.294 GHz
93,177,515,471 instructions # 0.39 insn per cycle
38,882,739,799 cache-references # 542.540 M/sec
1,492,348,412 cache-misses # 3.84% of all cache refs
25,614 context-switches # 357.398 /sec
2,594 page-faults # 36.195 /sec
71,667.99 msec cpu-clock # 0.956 CPUs utilized
71,686.62 msec task-clock # 0.956 CPUs utilized
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
60.75 31.147087 1832181 17 7 futex
28.76 14.745272 21 672689 write
9.73 4.990859 7 673537 read
0.69 0.355709 104 3410 epoll_pwait
0.02 0.011344 16 670 openat
0.01 0.005339 7 670 close
0.01 0.003345 4 670 getpid
0.01 0.002911 14 201 1 accept
0.00 0.002269 5 400 fcntl
0.00 0.001630 8 200 epoll_ctl
0.00 0.001470 7 200 getpeername
0.00 0.001276 6 200 setsockopt
0.00 0.000478 21 22 madvise
0.00 0.000069 13 5 mmap
------ ----------- ----------- --------- --------- ----------------
100.00 51.269058 37 1352891 8 total
```
#### io_uring
```
Performance counter stats for process id '1185386':
215,082,077,406 cycles # 3.286 GHz
96,169,901,947 instructions # 0.45 insn per cycle
40,306,489,203 cache-references # 615.727 M/sec
1,261,170,969 cache-misses # 3.13% of all cache refs
23,751 context-switches # 362.823 /sec
2,804 page-faults # 42.834 /sec
65,461.60 msec cpu-clock # 0.873 CPUs utilized
65,479.45 msec task-clock # 0.873 CPUs utilized
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
53.18 57.023894 1677173 34 8 futex
46.78 50.159947 1242 40368 io_uring_enter
0.01 0.011684 17 684 openat
0.01 0.010457 15 684 read
0.01 0.005449 7 684 close
0.00 0.004045 5 684 getpid
0.00 0.002640 13 201 1 accept
0.00 0.001717 122 14 madvise
0.00 0.001308 6 200 getpeername
0.00 0.001278 6 200 setsockopt
0.00 0.001167 5 200 io_uring_register
0.00 0.000034 17 2 mmap
------ ----------- ----------- --------- --------- ------------------
100.00 107.223620 2439 43955 9 total
```
#### io_uring+sqpoll
```
Performance counter stats for process id '1233920':
224,126,801,550 cycles # 3.268 GHz
108,098,363,639 instructions # 0.48 insn per cycle
44,227,121,486 cache-references # 644.912 M/sec
1,307,420,065 cache-misses # 2.96% of all cache refs
95,630 context-switches # 1.394 K/sec
2,776 page-faults # 40.479 /sec
68,578.49 msec cpu-clock # 1.055 CPUs utilized
68,657.67 msec task-clock # 1.056 CPUs utilized
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
55.86 63.033463 4202230 15 6 futex
44.05 49.707270 615 80729 io_uring_enter
0.02 0.023128 40 577 openat
0.02 0.021593 37 577 read
0.02 0.020551 101 202 2 accept
0.02 0.017446 30 577 close
0.01 0.014166 24 577 getpid
0.00 0.004677 23 200 setsockopt
0.00 0.004659 23 200 getpeername
0.00 0.000255 28 9 madvise
0.00 0.000013 13 1 mmap
------ ----------- ----------- --------- --------- ----------------
100.00 112.847221 1348 83664 8 total
```
根據上述數據可以看出,其實 `epoll` 與 `io_uring` 在 page-fault 以及 context-switch 上的數量級相同,兩者都屬於中低負擔的事件處理模型。而當 `io_uring` 開啟 SQPOLL 模式後,雖然 context-switch 次數顯著上升,但其整體吞吐表現依然最為出色,並能提供高效且低延遲的事件處理能力。此外,在系統呼叫數方面,它仍遠低於 `epoll`。在處理相同數量的請求下,epoll 需上百萬次系統呼叫,而 io_uring 僅需幾萬次即可完成,顯示其在降低模式切換的成本與提升 CPU 效能方面的顯著優勢。
### 小結
綜合上述數據結果可知,使用 `io_uring` 模式下的 Redis 相較於傳統 synchrounous I/O `epoll` 版本,在 SET 與 GET 指令上吞吐量可提升約 7~10%,若進一步開啟 SQPOLL 模式並將 kernel threads 綁定於獨立實體處理器核心,則可達到 22~27% 的吞吐量提升。在尾端延遲(P99、P99.9)方面,`io_uring` 機制亦能有效降低極端情境下的響應時間,對於 Redis 作為雲端服務的 in-memory 資料庫有助於改善服務穩定性。雖然 Redis 主要命令的處理流程仍以單執行緒為主,我認為尚未能完全發揮 `io_uring` 在高並行環境下 lock-free 設計的擴展性優勢,但從系統層面來看, `io_uring` 透過批次提交、合併系統呼叫,減少 syscall overhead,使其在高負載下仍具備明顯的效能優勢。
## TODO: 彙整現有針對 io_uring 效能改進的手法
> 針對 Redis 和 Valkey 現有 io_uring 相關 pull request 的成果,解釋其所要克服的技術議題,並說明何以 io_uring 有效益。
## TODO: 將 io_uring 應用於近期的 Valkey
> 整合 (或重作部分) 上述成果,應用於近期的 Valkey