小巧、快速、可靠。
任選三項。
SQLite 中的動態記憶體配置

概述

SQLite 使用動態記憶體配置來取得記憶體,以儲存各種物件(例如:資料庫連線已準備好的陳述),並建立資料庫檔案的記憶體快取,以及保存查詢結果。SQLite 的動態記憶體配置子系統已投入大量心力,使其可靠、可預測、強健、安全且有效率。

本文件提供 SQLite 中動態記憶體配置的概觀。目標受眾是軟體工程師,他們調整其使用 SQLite 以在要求嚴苛的環境中達到最佳效能。本文件中的內容並非使用 SQLite 的必要知識。SQLite 的預設設定和組態在大部分應用程式中都能正常運作。然而,本文件中的資訊可能對調整 SQLite 以符合特殊需求或在不尋常情況下執行的工程師有所幫助。

1. 特色

SQLite 核心及其記憶體配置子系統提供下列功能

2. 測試

SQLite 原始碼樹中的大部分程式碼專門用於 測試和驗證。可靠性對 SQLite 來說很重要。測試基礎架構的任務之一是確保 SQLite 沒有誤用動態配置的記憶體、SQLite 沒有記憶體外洩,以及 SQLite 能正確回應動態記憶體配置失敗。

測試基礎架構透過使用特別設定的記憶體配置器來驗證 SQLite 沒有誤用動態配置的記憶體。設定的記憶體配置器會在編譯時使用 SQLITE_MEMDEBUG 選項啟用。設定的記憶體配置器比預設的記憶體配置器慢很多,因此不建議在生產環境中使用。但在測試期間啟用時,設定的記憶體配置器會執行下列檢查

無論是否使用工具化的記憶體配置器,SQLite 都會追蹤目前已簽出的記憶體量。有數百個測試腳本用於測試 SQLite。在每個腳本結束時,所有物件都會被銷毀,並執行測試以確保已釋放所有記憶體。這就是偵測記憶體外洩的方式。請注意,在測試建置和生產建置期間,記憶體外洩偵測隨時都在執行。每當開發人員執行任何個別測試腳本時,記憶體外洩偵測都會處於啟用狀態。因此,在開發期間發生的記憶體外洩會被快速偵測並修正。

SQLite 對記憶體不足 (OOM) 錯誤的回應會使用可模擬記憶體故障的特殊記憶體配置器覆蓋層進行測試。覆蓋層是插入在記憶體配置器和 SQLite 其他部分之間的一層。覆蓋層會將大多數記憶體配置請求直接傳遞到基礎配置器,並將結果傳遞回請求者。但覆蓋層可以設定為導致第 N 次記憶體配置失敗。若要執行 OOM 測試,會先將覆蓋層設定為在第一次配置嘗試時失敗。然後執行一些測試腳本,並驗證配置已正確捕獲和處理。然後將覆蓋層設定為在第二次配置時失敗,並重複測試。失敗點會持續一次前進一個配置,直到整個測試程序執行完畢而沒有發生記憶體配置錯誤。整個測試序列執行兩次。在第一次執行時,覆蓋層設定為僅讓第 N 次配置失敗。在第二次執行時,覆蓋層設定為讓第 N 次和所有後續配置失敗。

請注意,即使在使用 OOM 覆蓋時,記憶體外洩偵測邏輯仍會持續運作。這驗證了 SQLite 即使在遇到記憶體配置錯誤時,也不會外洩記憶體。另請注意,OOM 覆蓋可與任何基礎記憶體配置器搭配使用,包括檢查記憶體配置錯誤的工具化記憶體配置器。如此一來,便可驗證 OOM 錯誤不會引發其他類型的記憶體使用錯誤。

最後,我們觀察到工具化記憶體配置器和記憶體外洩偵測器都適用於整個 SQLite 測試套件,而 TCL 測試套件 提供超過 99% 的陳述測試涵蓋率,且 TH3 測試架構提供 100% 分支測試涵蓋率,且無任何外洩。這有力地證明了動態記憶體配置在 SQLite 內部的各處都使用正確。

2.1. 使用 reallocarray()

reallocarray() 介面是 OpenBSD 社群最近(約 2014 年)的一項創新,其源自於避免下一個 "heartbleed" 漏洞 的努力,方法是避免記憶體配置大小運算中的 32 位元整數算術溢位。reallocarray() 函數同時具有單位大小和計數參數。若要配置足夠容納 N 個元素的記憶體,每個元素大小為 X 位元組,則呼叫 "reallocarray(0,X,N)"。這比呼叫 "malloc(X*N)" 的傳統技術更佳,因為 reallocarray() 消除了 X*N 乘法會溢位並導致 malloc() 傳回與應用程式預期大小不同的緩衝區的風險。

SQLite 不使用 reallocarray()。原因是 reallocarray() 對 SQLite 沒有用。結果發現 SQLite 絕不會執行兩個整數的簡單乘積的記憶體配置。相反地,SQLite 會執行 "X+C"、"N*X+C"、"M*N*X+C" 或 "N*X+M*Y+C" 等形式的配置,依此類推。reallocarray() 介面在避免這些情況中的整數溢位方面沒有幫助。

儘管如此,SQLite 希望處理記憶體配置大小計算中的整數溢位問題。為了避免問題,所有 SQLite 內部記憶體配置都使用採用有符號 64 位元整數大小參數的精簡包裝函數。SQLite 原始碼經過稽核,以確保所有大小計算也使用有符號 64 位元整數執行。SQLite 一次拒絕配置超過約 2GB 的記憶體。(在一般使用中,SQLite 一次很少配置超過約 8KB 的記憶體,因此 2GB 配置限制並非負擔。)因此,64 位元大小參數提供了大量緩衝區來偵測溢位。驗證所有大小計算都以 64 位元有符號整數執行的相同稽核也驗證在計算期間無法溢位 64 位元整數。

用於確保 SQLite 中的記憶體配置大小計算不會溢位的程式碼稽核會在每次 SQLite 發行前重複執行。

3. 設定

SQLite 中的預設記憶體配置設定適合大多數應用程式。但是,具有異常或特別嚴格需求的應用程式可能想要調整設定,以更符合其需求。編譯時間和啟動時間設定選項都可用。

3.1. 替代低階記憶體配置器

SQLite 原始碼包含幾個不同的記憶體配置模組,可以在編譯時間或在一定程度上在啟動時間選擇。

3.1.1. 預設記憶體配置器

預設情況下,SQLite 使用標準 C 函式庫中的 malloc()、realloc() 和 free() 常式來滿足其記憶體配置需求。這些常式被一個薄的包裝函式包圍,它也提供一個「memsize()」函式,該函式將傳回現有配置的大小。memsize() 函式用於精確計算未配置記憶體的位元組數目;當配置被釋放時,memsize() 決定從未配置計數中移除多少位元組。預設配置器透過在每個 malloc() 要求中始終配置額外的 8 個位元組,並將配置大小儲存在該 8 位元組標頭中,來實作 memsize()。

建議大多數應用程式使用預設記憶體配置器。如果您沒有令人信服的理由使用替代記憶體配置器,請使用預設配置器。

3.1.2. 偵錯記憶體配置器

如果 SQLite 使用 SQLITE_MEMDEBUG 編譯時間選項編譯,則在系統 malloc()、realloc() 和 free() 周圍使用一個不同的、繁重的包裝函式。繁重的包裝函式在每個配置中配置大約 100 位元組的額外空間。額外空間用於將哨兵值放置在傳回給 SQLite 核心配置的兩端。當配置被釋放時,這些哨兵值會被檢查,以確保 SQLite 核心不會在任一方向上覆寫緩衝區。當系統函式庫是 GLIBC 時,繁重的包裝函式也會使用 GNU backtrace() 函式來檢查堆疊並記錄 malloc() 呼叫的祖先函式。在執行 SQLite 測試套件時,繁重的包裝函式也會記錄當前測試案例的名稱。後兩項功能對於追蹤由測試套件偵測到的記憶體外洩來源很有用。

SQLITE_MEMDEBUG 設定時所使用的繁重包裝器,也會確保每個新配置在將配置傳回給呼叫者之前都填滿無意義的資料。而且配置一釋放,就會再次填滿無意義的資料。這兩個動作有助於確保 SQLite 核心不會對新配置記憶體的狀態做出假設,以及在釋放記憶體配置後不會使用它們。

SQLITE_MEMDEBUG 使用的繁重包裝器僅供在測試、分析和除錯 SQLite 時使用。繁重包裝器有顯著的效能和記憶體開銷,可能不應在生產中使用。

3.1.3. Win32 原生記憶體配置器

如果 SQLite 是使用 SQLITE_WIN32_MALLOC 編譯時間選項為 Windows 編譯,則會在 HeapAlloc()、HeapReAlloc() 和 HeapFree() 周圍使用不同的精簡包裝器。精簡包裝器使用已設定的 SQLite 堆積,如果使用 SQLITE_WIN32_HEAP_CREATE 編譯時間選項,則會不同於預設的程序堆積。此外,如果在啟用 assert() 的情況下編譯 SQLite,且啟用 SQLITE_WIN32_MALLOC_VALIDATE 編譯時間選項,則在配置或釋放時會呼叫 HeapValidate()。

3.1.4. 零配置記憶體配置器

當使用 SQLITE_ENABLE_MEMSYS5 選項編譯 SQLite 時,會在建置中包含不使用 malloc() 的替代記憶體配置器。SQLite 開發人員將此替代記憶體配置器稱為「memsys5」。即使包含在建置中,memsys5 在預設情況下仍會停用。若要啟用 memsys5,應用程式必須在啟動時呼叫下列 SQLite 介面

sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);

在上述呼叫中,pBuf 是指向一大段連續記憶體空間的指標,SQLite 將使用此空間來滿足其所有記憶體配置需求。pBuf 可能指向一個靜態陣列,或可能是從其他應用程式特定機制取得的記憶體。szBuf 是一個整數,表示由 pBuf 指向的記憶體空間位元組數。mnReq 是另一個整數,表示配置的最小大小。任何呼叫 sqlite3_malloc(N),其中 N 小於 mnReq,將會向上取整至 mnReq。mnReq 必須是 2 的次方。我們稍後會看到,mnReq 參數在降低 n 的值以及因此降低 Robson 證明 中的最小記憶體大小需求方面非常重要。

memsys5 配置器是設計用於嵌入式系統,儘管沒有任何因素會阻止其在工作站上使用。szBuf 通常介於數百 KB 到數十 MB 之間,具體取決於系統需求和記憶體預算。

memsys5 使用的演算法可以稱為「2 的次方、首次適配」。所有記憶體配置請求的大小都會向上取整至 2 的次方,並且請求會由 pBuf 中第一個足夠大的可用插槽滿足。相鄰的已釋放配置會使用夥伴系統合併。適當地使用時,此演算法會提供數學保證,以防止片段化和中斷,如下 所述

3.1.5. 實驗性記憶體配置器

用於零配置記憶體配置器的名稱「memsys5」暗示有幾個額外的記憶體配置器可用,實際上也的確有。預設的記憶體配置器是「memsys1」。除錯記憶體配置器是「memsys2」。這些已經涵蓋在內。

如果 SQLite 是使用 SQLITE_ENABLE_MEMSYS3 編譯,則會在原始碼樹中包含另一個類似於 memsys5 的零配置記憶體配置器。memsys3 配置器與 memsys5 一樣,必須透過呼叫 sqlite3_config(SQLITE_CONFIG_HEAP,...) 來啟用。Memsys3 使用提供為其來源的記憶體緩衝區來進行所有記憶體配置。memsys3 和 memsys5 之間的差異在於 memsys3 使用不同的記憶體配置演算法,在實務上似乎運作良好,但並未提供針對記憶體分段和中斷的數學保證。Memsys3 是 memsys5 的前身。SQLite 開發人員現在相信 memsys5 優於 memsys3,而且所有需要零配置記憶體配置器的應用程式都應該優先使用 memsys5,而非 memsys3。Memsys3 被視為實驗性質且已棄用,而且很可能會在未來版本的 SQLite 中從原始碼樹中移除。

Memsys4 和 memsys6 是在 2007 年左右推出的實驗性質記憶體配置器,後來在 2008 年左右從原始碼樹中移除,因為很明顯它們沒有增加任何新的價值。

其他實驗性質記憶體配置器可能會在未來的 SQLite 版本中加入。可以預期這些配置器會稱為 memsys7、memsys8,以此類推。

3.1.6. 應用程式定義的記憶體配置器

新的記憶體配置器不必是 SQLite 原始碼樹的一部分,也不必包含在 sqlite3.c 合併中。個別應用程式可以在啟動時提供自己的記憶體配置器給 SQLite。

為了讓 SQLite 使用新的記憶體配置器,應用程式只需呼叫

sqlite3_config(SQLITE_CONFIG_MALLOC, pMem);

在上述呼叫中,pMem 是指向 sqlite3_mem_methods 物件的指標,定義應用程式特定記憶體配置器的介面。sqlite3_mem_methods 物件實際上只是一個結構,包含指向函式的指標,用於實作各種記憶體配置原語。

在多執行緒應用程式中,存取 sqlite3_mem_methods 會序列化,但前提是 SQLITE_CONFIG_MEMSTATUS 已啟用。如果 SQLITE_CONFIG_MEMSTATUS 已停用,則 sqlite3_mem_methods 中的方法必須自行處理序列化需求。

3.1.7. 記憶體配置器覆蓋

應用程式可以在 SQLite 核心和底層記憶體配置器之間插入層級或「覆蓋」。例如,SQLite 的 記憶體不足測試邏輯 使用可以模擬記憶體配置失敗的覆蓋。

可以使用

sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem);

介面來建立覆蓋,以取得指向現有記憶體配置器的指標。現有配置器會由覆蓋儲存,並用作執行實際記憶體配置的備用。然後,使用 sqlite3_config(SQLITE_CONFIG_MALLOC,...) 將覆蓋插入現有記憶體配置器,如 上方 所述。

3.1.8. 不執行任何操作的記憶體配置器 stub

如果 SQLite 是使用 SQLITE_ZERO_MALLOC 選項編譯,則會略過 預設記憶體配置器,並以永遠不會配置任何記憶體的 stub 記憶體配置器取代它。任何呼叫 stub 記憶體配置器的動作都會回報沒有可用記憶體。

no-op 記憶體配置器本身沒有用處。它只存在作為一個佔位符,以便 SQLite 在其標準函式庫中可能沒有 malloc()、free() 或 realloc() 的系統上有一個記憶體配置器可以連結。使用 SQLITE_ZERO_MALLOC 編譯的應用程式需要使用 sqlite3_config() 搭配 SQLITE_CONFIG_MALLOCSQLITE_CONFIG_HEAP 在開始使用 SQLite 之前指定一個新的替代記憶體配置器。

3.2. 頁快取記憶體

在大部分應用程式中,SQLite 內的資料庫頁快取子系統使用的動態配置記憶體比 SQLite 其他所有部分加起來還要多。看到資料庫頁快取消耗的記憶體比 SQLite 其他所有部分加起來還要多出 10 倍並非不尋常。

可以設定 SQLite 從一個獨立且不同的固定大小區塊記憶體池中進行頁快取記憶體配置。這可能有兩個優點

預設情況下,頁快取記憶體配置器已停用。應用程式可以在啟動時如下啟用它

sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);

pBuf 參數是指向 SQLite 將用於頁快取記憶體配置的連續位元組範圍的指標。緩衝區的大小必須至少為 sz*N 位元組。「sz」參數是每個頁快取配置的大小。N 是可用配置的最大數量。

如果 SQLite 需要一個大於「sz」位元組的頁面快取項目,或者如果它需要超過 N 個項目,它會退而使用通用記憶體配置器。

3.3. 外觀記憶體配置器

SQLite 資料庫連線 會進行許多小型且短暫的記憶體配置。這最常發生在使用 sqlite3_prepare_v2() 編譯 SQL 陳述式時,但程度較低的情況下也會發生在使用 sqlite3_step() 執行 已準備好的陳述式 時。這些小型記憶體配置用於儲存表格和欄位名稱、剖析樹節點、個別查詢結果值和 B 樹游標物件等項目。因此,有許多呼叫會呼叫 malloc() 和 free(),呼叫次數多到 malloc() 和 free() 最終會使用分配給 SQLite 的 CPU 時間的很大一部分。

SQLite 版本 3.6.1(2008-08-06)引入了外觀記憶體配置器,以協助減少記憶體配置負載。在外觀配置器中,每個 資料庫連線 會預先配置一大段記憶體(通常在 60 到 120 千位元組的範圍內),並將該段記憶體分割成大小約為 100 到 1000 位元組的小型固定大小「時段」。這會成為外觀記憶體池。此後,與 資料庫連線 相關且不太大的記憶體配置會使用外觀池時段來滿足,而不是呼叫通用記憶體配置器。較大的配置會繼續使用通用記憶體配置器,外觀池時段全部簽出時發生的配置也是如此。但在許多情況下,記憶體配置夠小,而且未結案的配置夠少,新的記憶體要求可以從外觀池中滿足。

由於 lookaside 分配總是相同大小,因此分配和解除分配演算法非常快速。不需要合併相鄰的空閒槽位或搜尋特定大小的槽位。每個 資料庫連線 維護一個未使用的槽位的單向連結清單。分配請求只會提取此清單的第一個元素。解除分配只會將元素推回清單的前面。此外,每個 資料庫連線 都假設已經在單一執行緒中執行(已經有互斥鎖來強制執行這一點),因此不需要額外的互斥鎖來序列化對 lookaside 槽位空閒清單的存取。因此,lookaside 記憶體分配和解除分配非常快速。在 Linux 和 Mac OS X 工作站上的速度測試中,SQLite 顯示整體效能提升高達 10% 和 15%,具體取決於工作負載如何以及 lookaside 如何設定。

lookaside 記憶體池的大小有一個全域預設值,但也可以針對每個連線設定。若要在編譯時變更 lookaside 記憶體池的預設大小,請使用 -DSQLITE_DEFAULT_LOOKASIDE=SZ,N 選項。若要在啟動時變更 lookaside 記憶體池的預設大小,請使用 sqlite3_config() 介面

sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);

"sz" 參數是每個 lookaside 槽位的大小(以位元組為單位)。"cnt" 參數是每個資料庫連線的 lookaside 記憶體槽位總數。分配給每個 資料庫連線 的 lookaside 記憶體總量為 sz*cnt 位元組。

可以使用此呼叫變更個別 資料庫連線 "db" 的 lookaside 池

sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, pBuf, sz, cnt);

"pBuf" 參數是指向將用於 lookaside 記憶體池的記憶體空間的指標。如果 pBuf 為 NULL,則 SQLite 會使用 sqlite3_malloc() 取得記憶體池的空間。"sz" 和 "cnt" 參數分別是每個 lookaside 槽位的大小和槽位數。如果 pBuf 不為 NULL,則它必須指向至少 sz*cnt 位元組的記憶體。

僅當資料庫連線沒有未完成的 lookaside 分配時,才能變更 lookaside 組態。因此,應在使用 sqlite3_open() (或等效函式) 建立資料庫連線後,且在評估連線上的任何 SQL 陳述式之前,立即設定組態。

3.3.1. 雙大小 Lookaside

從 SQLite 版本 3.31.0 (2020-01-22) 開始,lookaside 支援兩個記憶體池,每個池都有不同大小的槽。小槽池使用 128 位元組槽,而大槽池使用 SQLITE_DBCONFIG_LOOKASIDE 指定的任何大小 (預設為 1200 位元組)。像這樣將池一分為二,可讓記憶體配置更常由 lookaside 涵蓋,同時將每個資料庫連線的堆積使用量從 120KB 減少到 48KB。

組態會持續使用 SQLITE_DBCONFIG_LOOKASIDE 或 SQLITE_CONFIG_LOOKASIDE 組態選項,如上所述,並帶有參數「sz」和「cnt」。用於 lookaside 的總堆積空間持續為 sz*cnt 位元組。但空間會在小槽 lookaside 和大槽 lookaside 之間配置,優先考量小槽 lookaside。槽的總數通常會超過「cnt」,因為「sz」通常遠大於 128 位元組的小槽大小。

預設 lookaside 組態已從 100 個 1200 位元組槽 (120KB) 變更為 40 個 1200 位元組槽 (48KB)。此空間最後會配置為 93 個 128 位元組槽和 30 個 1200 位元組槽。因此,可用的 lookaside 槽更多,但使用的堆積空間更少。

預設 lookaside 組態、小槽的大小,以及在小槽和大槽之間配置堆積空間的詳細資訊,都可能在各個版本之間變更。

3.4. 記憶體狀態

預設情況下,SQLite 會持續統計其記憶體使用量。這些統計資料有助於判斷應用程式實際需要多少記憶體。統計資料也可在高可靠度系統中使用,以判斷記憶體使用量是否接近或超過 Robson 證明 的限制,因此記憶體配置子系統容易發生故障。

大多數記憶體統計資料都是全域性的,因此統計資料的追蹤必須與 mutex 串行化。統計資料預設為開啟,但有一個選項可以將其停用。透過停用記憶體統計資料,SQLite 可避免在每次記憶體配置和解除配置時進入和離開 mutex。在 mutex 作業昂貴的系統中,這種節省是顯著的。若要停用記憶體統計資料,請在啟動時使用下列介面

sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);

「onoff」參數為 true 表示啟用記憶體統計資料追蹤,為 false 表示停用統計資料追蹤。

假設統計資料已啟用,可以使用下列常式存取統計資料

sqlite3_status(verb, &current, &highwater, resetflag);

「verb」引數決定要存取哪個統計資料。已定義 各種動詞。預計隨著 sqlite3_status() 介面的成熟,清單將會增加。選取參數的目前值會寫入整數「current」,最高歷史值會寫入整數「highwater」。如果 resetflag 為 true,則在呼叫傳回後,高水位會重設為目前值。

使用不同的介面來尋找與單一 資料庫連線 相關的統計資料

sqlite3_db_status(db, verb, &current, &highwater, resetflag);

這個介面類似,但它將 資料庫連線 的指標作為其第一個引數,並傳回關於該物件的統計資料,而不是關於整個 SQLite 函式庫的統計資料。sqlite3_db_status() 介面目前僅識別單一動詞 SQLITE_DBSTATUS_LOOKASIDE_USED,儘管未來可能會新增其他動詞。

每個連線的統計資料不使用全域變數,因此不需要 mutex 來更新或存取。因此,即使 SQLITE_CONFIG_MEMSTATUS 已關閉,每個連線的統計資料仍會繼續運作。

3.5. 設定記憶體使用量限制

可使用 sqlite3_soft_heap_limit64() 介面設定 SQLite 的一般用途記憶體配置器允許一次未完成的總記憶體量上限。如果嘗試配置超過軟性堆積限制所指定的記憶體,SQLite 會先嘗試釋放快取記憶體,然後繼續配置請求。只有在 記憶體統計資料 已啟用時,軟性堆積限制機制才會運作,而且如果 SQLite 函式庫是用 SQLITE_ENABLE_MEMORY_MANAGEMENT 編譯時間選項編譯,則運作最佳。

軟性堆積限制在這個意義上是「軟性」的:如果 SQLite 無法釋放足夠的輔助記憶體來維持在限制以下,它會繼續配置額外的記憶體並超過其限制。這是在理論上,使用額外的記憶體會比徹底失敗來得好。

自 SQLite 版本 3.6.1(2008-08-06)起,軟性堆積限制僅適用於一般用途記憶體配置器。軟性堆積限制不知道 頁面快取記憶體配置器旁觀記憶體配置器,也不會與它們互動。這個缺點很可能會在未來的版本中解決。

4. 針對記憶體配置失敗的數學保證

動態記憶體配置的問題,特別是記憶體配置器故障的問題,已經由 J. M. Robson 研究,並將結果發表為

J. M. Robson。「關於動態儲存配置的一些函式的界線」。計算機協會期刊,第 21 卷,第 8 期,1974 年 7 月,第 491-499 頁。

讓我們使用下列符號(類似但與 Robson 的符號不同)

N 記憶體配置系統所需的原始記憶體數量,以保證記憶體配置永遠不會失敗。
M 應用程式在任何時間點所檢查出的最大記憶體數量。
n 最大記憶體配置與最小記憶體配置的比率。我們假設每個記憶體配置大小都是最小記憶體配置大小的整數倍數。

Robson 證明了以下結果

N = M*(1 + (log2 n)/2) - n + 1

通俗地說,Robson 證明表明,為了保證無故障操作,任何記憶體配置器都必須使用大小為 N 的記憶體池,該記憶體池超過曾經使用過的最大記憶體數量 M,乘數取決於 n,即最大配置與最小配置的比率。換句話說,除非所有記憶體配置的大小完全相同 (n=1),否則系統需要訪問的記憶體量大於它一次使用的記憶體量。此外,我們看到,隨著最大配置與最小配置比率的增加,所需的剩餘記憶體量迅速增長,因此強烈建議將所有配置保持得盡可能接近相同的大小。

Robson 的證明是建設性的。他提供了一個演算法來計算一連串配置和取消配置操作,如果可用記憶體比 N 少一個位元組,這些操作將導致記憶體碎片化而導致配置失敗。而且,Robson 表明,如果可用記憶體為 N 或更多位元組,則冪次方首適記憶體配置器(例如由 memsys5 實作)永遠不會配置記憶體失敗。

Mn是應用程式的屬性。如果應用程式以Mn皆已知,或至少有已知的上限,且應用程式使用memsys5記憶體配置器,並使用SQLITE_CONFIG_HEAP提供N位元組的可用記憶體空間,則 Robson 證明應用程式中永遠不會有記憶體配置請求失敗。換句話說,應用程式開發人員可以選擇N的值,以保證永遠不會有對任何 SQLite 介面的呼叫傳回SQLITE_NOMEM。記憶體池永遠不會變得如此分散,以致於無法滿足新的記憶體配置請求。對於軟體故障可能造成人身傷害、身體傷害或無法取代的資料遺失的應用程式而言,這項屬性非常重要。

4.1. 計算和控制參數Mn

Robson 證明分別適用於 SQLite 使用的每個記憶體配置器

對於 memsys5 以外的配置器,所有記憶體配置大小相同。因此,n=1,所以N=M。換句話說,記憶體池不需要比任何特定時刻使用的最大記憶體量還要大。

在 SQLite 版本 3.6.1 中,頁快取記憶體的使用有點難以控制,不過後續版本已規劃機制,將使控制頁快取記憶體變得容易許多。在這些新機制推出之前,控制頁快取記憶體的唯一方法是使用cache_size pragma

安全關鍵應用程式通常會想要修改預設的快取記憶體配置,以便在 sqlite3_open() 期間配置初始快取記憶體緩衝區時,產生的記憶體配置不會太大,以致於強制 n 參數太大。為了控制 n,最好嘗試將最大的記憶體配置保持在 2 或 4 千位元組以下。因此,快取記憶體配置器的合理預設設定可能是下列任何一個

sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 32, 32);  /* 1K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 64, 32);  /* 2K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 32, 64);  /* 2K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 64, 64);  /* 4K */

另一種方法是先停用快取記憶體配置器

sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);

然後讓應用程式維護一個獨立的較大快取記憶體緩衝區池,可以在建立時將其分配給 資料庫連線。在一般情況下,應用程式只會有一個 資料庫連線,因此快取記憶體池可以包含一個大型緩衝區。

sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, aStatic, 256, 500);

快取記憶體配置器實際上是作為效能最佳化,而不是作為確保無故障記憶體配置的方法,因此,對於安全關鍵作業,完全停用快取記憶體配置器並非不合理。

一般用途記憶體配置器是最難管理的記憶體池,因為它支援配置各種大小。由於 nM 的倍數,我們希望將 n 保持在最小的可能值。這表示要將 memsys5 的最小配置大小保持在最大的可能值。在大部分應用程式中,快取記憶體配置器 能夠處理小型配置。因此,將 memsys5 的最小配置大小設定為快取配置最大大小的 2、4 甚至 8 倍是合理的。最小配置大小 512 是合理的設定。

除了保持 n 較小之外,也希望控制最大記憶體配置的大小。對一般用途記憶體配置器的龐大要求可能來自於多個來源

  1. 包含大型字串或 BLOB 的 SQL 表格列。
  2. 編譯成大型 已準備好的陳述式 的複雜 SQL 查詢。
  3. sqlite3_prepare_v2() 內部使用的 SQL 解析器物件。
  4. 資料庫連線 物件的儲存空間。
  5. 溢位到一般用途記憶體配置器的頁面快取記憶體配置。
  6. 用於新 資料庫連線 的暫存緩衝區配置。

最後兩個配置可透過適當地設定 頁面快取記憶體配置器暫存緩衝區記憶體配置器 來控制和/或消除,如上所述。 資料庫連線 物件所需的儲存空間在某種程度上取決於資料庫檔案檔名的長度,但在 32 位元系統上很少超過 2KB。(由於指標大小增加,64 位元系統需要更多空間。)每個解析器物件使用約 1.6KB 的記憶體。因此,上述第 3 到第 6 個元素很容易控制,以將最大記憶體配置大小保持在 2KB 以下。

如果應用程式設計為以小片段管理資料,則資料庫不應包含任何大型字串或 BLOB,因此上述第 1 個元素不應是一個因素。如果資料庫確實包含大型字串或 BLOB,應使用 增量 BLOB I/O 讀取它們,且包含大型字串或 BLOB 的列不應透過 增量 BLOB I/O 以外的任何方式更新。否則,sqlite3_step() 常式程式將需要在某個時間點將整列讀取到連續記憶體中,這將涉及至少一個大型記憶體配置。

大型記憶體配置的最後一個來源是存放 已準備好陳述式 的空間,這些陳述式是編譯複雜 SQL 作業的結果。SQLite 開發人員持續進行的工作正在減少此處所需的空間。但大型且複雜的查詢可能仍需要大小為數 KB 的 已準備好陳述式。目前唯一的解決方法是讓應用程式將複雜的 SQL 作業分解成兩個或更多個較小且較簡單的作業,並包含在個別的 已準備好陳述式 中。

綜合考量所有因素,應用程式通常應該能夠將其最大記憶體配置大小控制在 2K 或 4K 以下。這會產生 2 或 3 的 log2(n) 值。這會將 N 限制在 M 的 2 到 2.5 倍之間。

應用程式所需的一般用途記憶體最大量由以下因素決定:應用程式使用的同時開啟 資料庫連線已準備好陳述式 物件數量,以及 已準備好陳述式 的複雜性。對於任何特定應用程式,這些因素通常是固定的,而且可以使用 SQLITE_STATUS_MEMORY_USED 透過實驗方式來確定。典型的應用程式可能只使用約 40KB 的一般用途記憶體。這會產生約 100KB 的 N 值。

4.2. 延性破壞

如果 SQLite 中的記憶體配置子系統已設定為防故障操作,但實際記憶體使用量超過 Robson 證明 所設定的設計限制,SQLite 通常會繼續正常運作。頁快取記憶體配置器旁觀記憶體配置器 會自動故障轉移至 memsys5 一般用途記憶體配置器。通常情況下,即使 M 和/或 n 超過 Robson 證明 所施加的限制,memsys5 記憶體配置器仍會繼續運作,而不會產生碎片。Robson 證明 顯示,在這種情況下,記憶體配置可能會中斷並發生故障,但此類故障需要特別卑劣的配置和解除配置順序,而 SQLite 從未被觀察到遵循此類順序。因此,在實務上,通常情況下,Robson 所施加的限制可以大幅超出,而不會產生不良影響。

儘管如此,仍建議應用程式開發人員監控記憶體配置子系統的狀態,並在記憶體使用量接近或超過 Robson 限制時發出警報。這樣,應用程式將在故障發生前很久就提供充足的警告給操作員。SQLite 的 記憶體統計資料 介面提供應用程式所有必要的機制,以完成此任務的監控部分。

5. 記憶體介面的穩定性

更新:自 SQLite 版本 3.7.0 (2010-07-21) 起,所有 SQLite 記憶體配置介面都被視為穩定,並將在未來版本中獲得支援。