小巧。快速。可靠。
選擇任何三個。
測量和減少 SQLite 中的 CPU 使用

1. 概觀

下方的圖表顯示 SQLite 在標準工作負載下使用的 CPU 週期數,針對約 10 年前的 SQLite 版本。相較於舊版本,最新版本的 SQLite 使用的 CPU 週期約為三分之一。

本文說明 SQLite 開發人員如何測量 CPU 使用率、這些測量實際上代表什麼,以及 SQLite 開發人員在持續追求進一步減少 SQLite 函式庫 CPU 使用率時所使用的技術。



在 Ubuntu 16.04 上使用 gcc 5.4.0 和 -Os,以 cachegrind 測量。

2. 測量效能

簡而言之,SQLite 的 CPU 效能測量如下

  1. 在已發布的組態中編譯 SQLite,不使用任何特殊遙測或除錯選項。
  2. 將 SQLite 連結到測試程式,該程式執行約 30,000 個 SQL 陳述式,代表典型的工作負載。
  3. 使用 cachegrind 計算消耗的 CPU 週期數。

2.1. 編譯選項

針對效能測量,SQLite 編譯的方式與在生產系統中使用的方式大致相同。編譯時期的設定是「近似的」,原因在於 SQLite 的每個生產用途都不同。一個系統使用的編譯時期選項不一定與其他系統相同。重點在於避免會大幅影響產生的機器碼的選項。例如,會省略 -DSQLITE_DEBUG 選項,因為該選項會在 SQLite 函式庫效能關鍵區段中插入數千個 assert() 陳述式。會省略 -pg 選項(在 GCC 上),因為它會導致編譯器發出額外的機率效能測量碼,而這會干擾實際效能測量。

針對效能測量,使用 -Os 選項(針對大小最佳化)而不是 -O2,因為 -O2 選項會產生太多程式碼移動,導致難以將特定的 CPU 指令與 C 原始碼行關聯起來。

2.2. 工作負載

「典型」工作負載是由正規 SQLite 原始碼樹中的 speedtest1.c 程式產生。此程式致力於以典型真實世界應用程式的執行方式來執行 SQLite 函式庫。當然,每個應用程式都不同,因此沒有任何測試程式可以完全反映所有應用程式的行為。

speedtest1.c 程式會隨著 SQLite 開發人員對「典型」使用方式的了解而時常更新。

正規原始碼樹中也有 speed-check.sh shell 指令碼,用於執行 speedtest1.c 程式。若要複製效能測量,請將下列檔案收集到單一目錄中

然後執行「sh speed-check.sh trunk」。

2.3. 效能量測

Cachegrind 用於量測效能,因為它提供的答案可重複到 7 個或更多有效位數。相較之下,實際(時脈)執行時間幾乎無法重複超過一個有效位數。

2.4. 微最佳化

Cachegrind 的高重複性讓 SQLite 開發人員能夠實作和量測「微最佳化」。微最佳化是對程式碼的變更,會導致極小的效能提升。典型的微最佳化會將 CPU 週期數減少 0.1% 或 0.05%,甚至更少。這種改善無法用實際時間來量測。但數百或數千個微最佳化加起來,會產生可量測的實際效能提升。

3. 效能量測工作流程

當 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 標註輸出中移除不必要的細節,以便在嘗試微最佳化時,可以使用並排差異的方式來比較前後報告,以檢視特定細節對效能的影響。

4. 限制

使用標準化的 speedtest1.c 工作負載和 cachegrind 已能顯著提升效能。然而,了解此方法的限制也很重要

此頁面最後修改於 2022-01-08 05:02:57 UTC