下方的圖表顯示 SQLite 在標準工作負載下使用的 CPU 週期數,針對約 10 年前的 SQLite 版本。相較於舊版本,最新版本的 SQLite 使用的 CPU 週期約為三分之一。
本文說明 SQLite 開發人員如何測量 CPU 使用率、這些測量實際上代表什麼,以及 SQLite 開發人員在持續追求進一步減少 SQLite 函式庫 CPU 使用率時所使用的技術。
簡而言之,SQLite 的 CPU 效能測量如下
針對效能測量,SQLite 編譯的方式與在生產系統中使用的方式大致相同。編譯時期的設定是「近似的」,原因在於 SQLite 的每個生產用途都不同。一個系統使用的編譯時期選項不一定與其他系統相同。重點在於避免會大幅影響產生的機器碼的選項。例如,會省略 -DSQLITE_DEBUG 選項,因為該選項會在 SQLite 函式庫效能關鍵區段中插入數千個 assert() 陳述式。會省略 -pg 選項(在 GCC 上),因為它會導致編譯器發出額外的機率效能測量碼,而這會干擾實際效能測量。
針對效能測量,使用 -Os 選項(針對大小最佳化)而不是 -O2,因為 -O2 選項會產生太多程式碼移動,導致難以將特定的 CPU 指令與 C 原始碼行關聯起來。
「典型」工作負載是由正規 SQLite 原始碼樹中的 speedtest1.c 程式產生。此程式致力於以典型真實世界應用程式的執行方式來執行 SQLite 函式庫。當然,每個應用程式都不同,因此沒有任何測試程式可以完全反映所有應用程式的行為。
speedtest1.c 程式會隨著 SQLite 開發人員對「典型」使用方式的了解而時常更新。
正規原始碼樹中也有 speed-check.sh shell 指令碼,用於執行 speedtest1.c 程式。若要複製效能測量,請將下列檔案收集到單一目錄中
然後執行「sh speed-check.sh trunk」。
Cachegrind 用於量測效能,因為它提供的答案可重複到 7 個或更多有效位數。相較之下,實際(時脈)執行時間幾乎無法重複超過一個有效位數。
Cachegrind 的高重複性讓 SQLite 開發人員能夠實作和量測「微最佳化」。微最佳化是對程式碼的變更,會導致極小的效能提升。典型的微最佳化會將 CPU 週期數減少 0.1% 或 0.05%,甚至更少。這種改善無法用實際時間來量測。但數百或數千個微最佳化加起來,會產生可量測的實際效能提升。
當 SQLite 開發人員編輯 SQLite 原始碼時,他們會執行 speed-check.sh shell 指令碼來追蹤變更對效能的影響。此指令碼會編譯 speedtest1.c 程式,在 Cachegrind 下執行,使用 cg_anno.tcl TCL 指令碼處理 Cachegrind 輸出,然後將結果儲存到一系列文字檔中。speed-check.sh 指令碼的典型輸出如下所示
==8683== ==8683== I refs: 1,060,925,768 ==8683== I1 misses: 23,731,246 ==8683== LLi misses: 5,176 ==8683== I1 miss rate: 2.24% ==8683== LLi miss rate: 0.00% ==8683== ==8683== D refs: 557,686,925 (361,828,925 rd + 195,858,000 wr) ==8683== D1 misses: 5,067,063 ( 3,544,278 rd + 1,522,785 wr) ==8683== LLd misses: 57,958 ( 16,067 rd + 41,891 wr) ==8683== D1 miss rate: 0.9% ( 1.0% + 0.8% ) ==8683== LLd miss rate: 0.0% ( 0.0% + 0.0% ) ==8683== ==8683== LL refs: 28,798,309 ( 27,275,524 rd + 1,522,785 wr) ==8683== LL misses: 63,134 ( 21,243 rd + 41,891 wr) ==8683== LL miss rate: 0.0% ( 0.0% + 0.0% ) text data bss dec hex filename 523044 8240 1976 533260 8230c sqlite3.o 220507 1007870 7769352 sqlite3.c
輸出中重要的部分(開發人員最關注的部分)以紅色顯示。基本上,開發人員想知道已編譯 SQLite 函式庫的大小,以及執行效能測試需要多少 CPU 週期。
cg_anno.tcl 指令碼的輸出顯示在每一行程式碼上花費的 CPU 週期數。報告大約有 80,000 行長。以下是從報告中間擷取的簡短片段,以顯示其外觀
. SQLITE_PRIVATE int sqlite3BtreeNext(BtCursor *pCur, int *pRes){ . MemPage *pPage; . assert( cursorOwnsBtShared(pCur) ); . assert( pRes!=0 ); . assert( *pRes==0 || *pRes==1 ); . assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); 369,648 pCur->info.nSize = 0; 369,648 pCur->curFlags &= ~(BTCF_ValidNKey|BTCF_ValidOvfl); 369,648 *pRes = 0; 739,296 if( pCur->eState!=CURSOR_VALID ) return btreeNext(pCur, pRes); 1,473,580 pPage = pCur->apPage[pCur->iPage]; 1,841,975 if( (++pCur->aiIdx[pCur->iPage])>=pPage->nCell ){ 4,340 pCur->aiIdx[pCur->iPage]--; 5,593 return btreeNext(pCur, pRes); . } 728,110 if( pPage->leaf ){ . return SQLITE_OK; . }else{ 3,117 return moveToLeftmost(pCur); . } 721,876 }
左邊的數字當然是該行程式碼的 CPU 週期計數。
cg_anno.tcl 這個指令碼會從預設的 cachegrind 標註輸出中移除不必要的細節,以便在嘗試微最佳化時,可以使用並排差異的方式來比較前後報告,以檢視特定細節對效能的影響。
使用標準化的 speedtest1.c 工作負載和 cachegrind 已能顯著提升效能。然而,了解此方法的限制也很重要
效能測量使用單一編譯器 (gcc 5.4.0)、最佳化設定 (-Os),並在單一平台上執行 (x64 上的 Ubuntu 16.04 LTS)。其他編譯器和處理器的效能可能有所不同。
正在測量的 speedtest1.c 工作負載試圖代表 SQLite 的各種典型使用方式。但每個應用程式都不同。speedtest1.c 工作負載可能無法適當地代表某些應用程式執行的活動類型。SQLite 開發人員不斷致力於改善 speedtest1.c 程式,使其更能代表實際的 SQLite 使用方式。歡迎社群提供意見回饋。
cachegrind 提供的週期計數是實際效能的良好代表,但並非 100% 準確。
這裡僅測量 CPU 週期計數。CPU 週期計數是能源消耗的良好代表,但未必與實際時間有良好的關聯性。花在執行 I/O 的時間不會反映在 CPU 週期計數中,而 I/O 時間在許多 SQLite 使用情境中佔了絕大部分。
此頁面最後修改於 2022-01-08 05:02:57 UTC