標籤:
SQLite通過動態記憶體分配來擷取各種對象(例如資料庫連接和SQL預先處理語句)所需記憶體、建立資料庫檔案的記憶體Cache、以及儲存查詢結果。我們做了很多努力來讓SQLite的動態記憶體分配子系統可靠、可預測、健壯並且高效。本文概述SQLite的動態記憶體分配,軟體開發人員在使用SQLite時可以據此獲得最佳效能。
1、特性
SQLite核心和它的記憶體配置子系統提供以下特性:
(1)對記憶體配置失敗的健壯處理。如果一個記憶體配置請求失敗(即malloc()或realloc()返回NULL),SQLite將釋放未關聯的快取頁面,然後重新進行分配請求。如果失敗,SQLite返回SQLITE_NOMEM給應用程式。
(2)無記憶體流失。應用程式負責銷毀已指派的任何對象。例如應用程式必須使用sqlite3_finalize()結束每個預先處理SQL語句,使用sqlite3_close關閉每個資料庫連接。只要應用程式配合,即使在記憶體配置失敗或系統出錯的情況下SQLite也絕不會泄漏記憶體。
(3)記憶體使用量限制。sqlite3_soft_heap_limit64()機制可以讓應用程式設定SQLite的記憶體使用量限制。SQLite會從緩衝中重用記憶體,而不是分配新的記憶體,以滿足設定的限制。
(4)零分配選項。應用程式可以在啟動時給SQLite提供幾個大塊記憶體的緩衝區,SQLite將用這些緩衝區作為它所有記憶體配置的需要,不再調用系統的malloc()和free()。
(5)應用程式提供記憶體 Clerk。應用程式在啟動時可以給SQLite提供可選的記憶體 Clerk,以代替系統的malloc()和free()。
(6)對防止記憶體配置失敗或堆記憶體出現片段提供形式化的保證。SQLite能配置成保證不會出現記憶體配置失敗或記憶體片段。這個特性對長期運行、高可靠性的嵌入式系統至關重要,在這樣的系統上一個記憶體配置錯誤可能會導致整個系統失效。
(7)記憶體使用量統計。應用程式可以統計SQLite使用了多少記憶體,可以檢測記憶體使用量是否接近或超過設計限制。
(8)盡量少調用分配器。系統的malloc()和free()實現在很多系統上是低效的。SQLite通過盡量少地使用malloc()和free()來減少整個處理時間。
(9)開放式存取。可插撥的SQLite擴充模組或應用程式自己可以通過sqlite3_malloc(), sqlite3_realloc()和sqlite3_free()介面來訪問SQLite使用的底層記憶體 Clerk。
2、測試
測試基礎設施通過使用一個特殊的檢測記憶體 Clerk來驗證SQLite沒有錯誤地使用動態分配的記憶體。檢測記憶體 Clerk通過編譯時間使用SQLITE_MEMDEBUG選項來啟用,它比預設的記憶體 Clerk更慢,因此不建議在產品中使用它。但是在測試時啟用它,可以做以下檢查:
(1)邊界檢查。檢測記憶體 Clerk在每個記憶體配置的末尾放置哨兵值,以驗證SQLite沒有任何常式寫出了分配的邊界。
(2)釋放後的記憶體使用量。當每塊記憶體被釋放時,每個位元組會填充一些無用的位元模式。這可以確保記憶體在釋放後絕不會留下曾經使用過的痕迹。
(3)從非malloc擷取的記憶體的釋放。每個來自於檢測記憶體 Clerk的記憶體配置都含有一個哨兵值,以驗證每個分配的釋放來自於先前的malloc。
(4)未初始化的記憶體。檢測記憶體 Clerk用一些無用的位元模式初始化每塊記憶體的分配,以確保使用者不能對所分配記憶體的內容做任何假設。
無論是否使用檢測記憶體 Clerk,SQLite都會跟蹤當前已取出多少記憶體。有數百個測試指令碼用於測試SQLite。在每個指令碼的末尾,所有的對象被銷毀,並有一段測試保證所有記憶體已釋放。這就是檢測記憶體流失的方法。注意記憶體流失的檢測在任何時候都是大批量的進行,包括測試構建和產品構建過程中。每當一個開發人員運行任意單個測試指令碼,記憶體流失檢測就會被啟用。因此開發過程中引入的記憶體流失能夠迅速地被檢測到並修複。
SQLite使用一個特殊的、能類比記憶體失敗的記憶體 Clerk覆蓋層來測試記憶體不足(OOM)的錯誤。覆蓋層被插入到記憶體 Clerk和SQLite核心之間,它直接傳遞記憶體配置請求到底層的分配器,並把結果傳回到要求者。覆蓋層可設定讓第N次記憶體配置失敗。為了運行OOM測試,覆蓋層首先設定第一次分配失敗,並運行一些測試指令碼以驗證分配被正確捕捉和處理。然後覆蓋層設定第二次分配失敗並重複運行測試。失敗點繼續一次通知一個分配,直到整個測試過程完成並且沒有出現記憶體配置錯誤。這樣的完整測試流程運行兩遍,第一遍覆蓋層只設定第N次分配失敗,第二遍覆蓋層設定第N次和後續的分配失敗。注意即使遇到OOM覆蓋層,記憶體流失檢測邏輯也繼續工作,以驗證SQLite即使遇到記憶體配置錯誤也不會泄漏記憶體。OOM覆蓋層能與任何底層記憶體 Clerk一起工作,包括檢測記憶體 Clerk(這時可驗證OOM錯誤不會引入其他的記憶體使用量錯誤)。
檢測記憶體 Clerk和記憶體流失檢測邏輯工作在整個SQLite測試套件上。其中TCL測試套件提供99%的語句測試覆蓋,TH3測試提供100%的分支測試覆蓋以保證無記憶體流失。因此SQLite的動態記憶體分配器可以在各種場合下正確地工作。
3、配置
3.1 可選的底層記憶體 Clerk
SQLite原始碼包含幾種不同的記憶體 Clerk模組,可以在編譯時間選擇,或在啟動時作為一個有限的擴充。
(1)預設記憶體 Clerk
預設情況下,SQLite使用C標準庫中的malloc(), realloc()和free()常式來分配記憶體。實現中還對這些常式做一層薄的封裝以提供一個memsize()函數返回一個現存分配的大小。memsize()也能精確地跟蹤未歸還記憶體的位元組數,它能確定當一個分配被釋放時有多少位元組從未歸還記憶體中移除。預設記憶體 Clerk中的memsize()實現是在每個malloc()請求上多分配額外8個位元組作為頭部,並把分配的大小儲存到這個8位元組的頭部。
在大多數應用程式中我們都建議使用預設記憶體 Clerk,如果沒有使用可選記憶體 Clerk的強制性需求,使用預設的即可。
(2)調試記憶體 Clerk
如果SQLite使用SQLITE_MEMDEBUG編譯時間選項來編譯,則使用一個不同的、對系統malloc(), realloc()和free()進行重型封裝的記憶體 Clerk。重型封裝器對每個分配請求多分配100位元組的額外空間,用來在分配的末尾放置哨兵值。當一個分配被釋放時,檢查這些哨兵值以確保SQLite核心沒有超出緩衝區的兩端。當系統庫來自GLIBC時,重型封裝器也會使用GNU backtrace()函數來檢查棧,並記錄malloc()調用的祖先函數。當運行測試套件時,重型封裝器還會記錄當前測試案例的名稱。這兩個特性對跟蹤記憶體流失是非常有用的。
重型封裝器只用於SQLite的測試、分析和調試。它有顯著的效能和記憶體開銷,一般不用在最終產品中。
(3)零分配記憶體 Clerk
當SQLite使用SQLITE_ENABLE_MEMSYS5選項編譯時間,會包含一個不使用malloc()的可選記憶體 Clerk。SQLite開發人員稱它為"memsys5"。即使被包含在版本中,預設情況下memsys5也是被禁用的。應用程式必須在啟動時調用下面的SQLite介面:
sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);
其中pBuf指向一個大的連續的記憶體塊,SQLite使用它滿足所有的記憶體配置需要。pBuf也可以指向一個靜態數組或一段從其他應用程式特定機制擷取的記憶體。szBuf為pBuf記憶體的位元組數。mnReq為一次分配的最小位元組數。任何對sqlite3_malloc(N)的調用,當N小於mnReq時會向上舍入到mnReq。nmReq必須是2的冪。稍後我們會看到mnReq參數在Robson證明中對於減小n值和最小記憶體需求是至關重要的。
memsys5分配器被設計用於嵌入式系統中,當然沒有任何限制規定不得用於工作站上。szBuf通常在幾百KB到幾十MB之間,取決於系統需要和記憶體預算。memsys5使用的演算法可以概括為“2的冪,首次命中”。所有記憶體配置請求的大小都舍入到2的冪,分配時使用pBuf中第一個足夠大的空閑記憶體塊。使用夥伴系統來合并相鄰的空閑記憶體塊。如果使用得當,本演算法可以為避免記憶體片段和內在崩潰提供數學保證(參考下面描述)。
(4)實驗性記憶體 Clerk
從名稱"memsys5"可以看出,可能還有其他可選的記憶體 Clerk,的確如此。預設記憶體 Clerk稱為"memsys1",調試記憶體 Clerk稱為"memsys2"。如果SQLite使用SQLITE_ENABLE_MEMSYS3編譯,則另外一個零分配記憶體 Clerkmemsys3包含到版本中。它與memsys5類似,必須調用sqlite3_config(SQLITE_CONFIG_HEAP,...)來啟用。Memsys3使用記憶體緩衝區來滿足所有的記憶體配置需要。它與memsys5的區別是使用不同的記憶體配置演算法,這個演算法在實踐中看起來工作得很好,但不能為避免記憶體片段和記憶體崩潰提供數學保證。Memsys3是memsys5的前任,SQLite開發人員相信memsys5比memsys3更好,所有需要零分配記憶體 Clerk的應用程式應該使用memsys5而不是memsys3。Memsys3在將來的SQLite版本中可能會移除。
在SQLite 3.6.1中,memsys4的代碼還在源碼樹中,從3.6.5開始已移除。Memsys4嘗試用mmap()擷取記憶體,使用madvise()釋放未使用的頁給作業系統,以便它們能被其他進程使用。現在memsys4已被拋棄。Memsys6使用系統malloc()和free()擷取需要的記憶體,它是一個彙總器。Memsys6隻調用malloc()擷取大容量的記憶體,然後把這些大容量的記憶體分割成多塊小記憶體,以滿足SQLite核心的需要。Memsys6用在malloc()實現低效的系統中。memsys6後面的思想是減小系統malloc()的調用次數。Memsys6隻有使用SQLITE_ENABLE_MEMSYS6編譯SQLite時才可用,並且在啟動時需要調用:
sqlite3_config(SQLITE_CONFIG_CHUNKALLOC);
Memsys6在SQLite 3.6.1中添加,它是非常實驗性的,從3.6.5開始也已移除。其他實驗性的記憶體 Clerk可能會在將來的SQLite版本中加入,名稱可能是memsys7, memsys8等。
(5)應用程式定義的記憶體 Clerk
應用程式可以在啟動時提供自己的記憶體 Clerk給SQLite。為了讓SQLite使用新的記憶體 Clerk,應用程式要調用:
sqlite3_config(SQLITE_CONFIG_MALLOC, pMem);
其中pMem指向一個sqlite3_mem_methods對象,這個結構定義了應用程式特定的記憶體 Clerk介面。sqlite3_mem_methods對象只是包含一系列函數指標的結構,指向自訂的各種記憶體配置函數。在多線程應用程式中,若且唯若啟用SQLITE_CONFIG_MEMSTATUS時sqlite3_mem_methods才是序列化的。如果SQLITE_CONFIG_MEMSTATUS禁用,sqlite3_mem_methods中的方法就需要自己來關注序列化。
(6)記憶體 Clerk覆蓋層
應用程式可在SQLite核心和底層記憶體 Clerk之間插入覆蓋層。例如,OOM測試邏輯通過使用覆蓋層可以類比記憶體配置失敗的情形。覆蓋層使用以下介面來建立:
sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem);
該介面擷取現存記憶體 Clerk的指標,並儲存它以用來進行實際的記憶體配置。然後通過使用類似的sqlite3_config(SQLITE_CONFIG_MALLOC,...)把覆蓋層被插入到現存記憶體 Clerk的地方。
(7)空操作記憶體 Clerk
如果SQLite使用SQLITE_ZERO_MALLOC選項來編譯,預設記憶體 Clerk將會被忽略,由一個樁記憶體 Clerk代替,它不會分配任何記憶體。任何對樁分配器的調用將返回沒有可用記憶體的報告。這種空操作記憶體 Clerk只是作為一個預留位置,以便SQLite能連結一些不使用malloc(), free()或realloc()的自訂記憶體 Clerk。帶SQLITE_ZERO_MALLOC選項編譯的應用程式在使用SQLite之前,需要使用sqlite3_config(),結合SQLITE_CONFIG_MALLOC或SQLITE_CONFIG_HEAP來指定新的可選記憶體 Clerk。
3.2 臨時記憶體
SQLite偶爾需要一大塊的“臨時”記憶體來執行一些臨時的計算。例如,當重新平衡一棵B-Tree時,需要使用臨時記憶體。這些臨時記憶體通常在10KB左右,用於一個單一的、短暫的函數調用。在早期的SQLite版本中,臨時記憶體從處理器棧中擷取,這在擁有大容量棧的工作站上可以很好地工作。但在只有小容量處理器棧(通常4K或8K)的嵌入式系統中,從棧中申請一個大的緩衝區會引起問題。因此,SQLite被修改為從堆上擷取臨時記憶體。臨時記憶體 Clerk的設定方法如下:
sqlite3_config(SQLITE_CONFIG_SCRATCH, pBuf, sz, N);
其中pBuf指向一段連續的記憶體,SQLite用它來進行臨時記憶體配置。這段連續記憶體至少要有sz*N位元組的大小,"sz"參數是每次臨時記憶體配置的最大位元組數,N是同時進行臨時記憶體配置的最大次數。"sz"參數值應該為最大資料庫頁面的6倍左右,N應該為系統中運行線程數量的2倍左右。沒有線程會一次請求超過兩次的臨時記憶體配置,因此N的值能確保足夠的臨時記憶體配置。如果臨時記憶體設定沒有提供足夠的記憶體,SQLite將退回到使用正常的記憶體 Clerk來進行臨時記憶體配置。預設的設定是sz=0, N=0,表示使用正常的記憶體 Clerk作為預設行為。
3.3 頁緩衝記憶體
在很多應用程式中,SQLite的資料庫頁緩衝子系統會更頻繁地使用動態記憶體分配,甚至頻繁程度超過其他子系統的10倍。SQLite可以配置成從一個獨立的、槽大小固定的記憶體池中進行頁緩衝記憶體的分配。這有兩個優點:
(1)因為所有的分配是同樣的大小,記憶體 Clerk可能工作的更快,分配器無需合并相鄰空閑槽或尋找大小合適的槽。所有未分配的記憶體槽儲存在一個鏈表中,分配時摘下鏈表中的第一個記憶體槽,釋放時直接把記憶體槽添加到鏈表的頭部。
(2)由於只有一種分配大小,Robson證明中的n參數為1,並且分配器需要的整個記憶體空間(N)恰好等於使用的最大記憶體(M)。沒有額外的記憶體片段開銷,因此減少了記憶體需求量。這對頁緩衝記憶體特別重要,因為SQLite的大部分記憶體需求都來自於頁緩衝。
頁緩衝記憶體 Clerk預設是禁用了,應用程式可在啟動時開啟它:
sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);
其中pBuf指向一段連續的記憶體,SQLite用它來進行頁緩衝記憶體的分配。這段連續記憶體至少要有sz*N位元組的大小,"sz"參數是每次頁緩衝記憶體配置的位元組數,N是可以進行分配的最大次數。如果SQLite需要超過sz個位元組的頁緩衝記憶體,或者需要超過N塊的分配,則退回到使用通常的記憶體 Clerk。
3.4 後備記憶體 Clerk
SQLite資料庫連接會進行許多小的、短期的記憶體配置。當用sqlite3_prepare_v2()編譯SQL語句時這種情況最常見。這些小的記憶體配置用來儲存諸如表名和列名、解析樹結點、單獨的查詢結果、和B-Tree遊標對象。這會導致頻繁地調用malloc()和free(),用掉分配給SQLite的大部分CPU時間片。
SQLite 3.6.1引入後備記憶體 Clerk來協助減少記憶體配置負載。在後備分配器中,每個資料庫連接預先分配一段大塊的記憶體(通常50到100KB),然後把這個記憶體塊分割成50到200位元組固定大小的多個記憶體槽,從而變成後備記憶體池。資料庫連接的小記憶體配置使用後備池中的一個槽,而大記憶體配置繼續使用通用記憶體 Clerk。當後備池全部用完時也會轉用通用記憶體 Clerk,不過在很多情況下後備池對小記憶體配置的使用是足夠的。
因為後備記憶體配置總是同樣的大小,因此分配和釋放演算法速度非常快,無需合并相鄰空閑槽或尋找大小合適的槽。每個資料庫連接維護一個空閑槽的單鏈表。分配時直接摘下鏈表中的第一個記憶體槽,釋放時直接把記憶體槽添加到鏈表的頭部。此外,每個資料庫連接已經運行在單個線程下(已經放置了互斥鎖來強製做到這一點),無需額外的互斥鎖來序列化後備槽列表的訪問。因此,後備記憶體配置的分配和釋放是非常快速的,在Linux和Max OS X工作站上進行速度測試,顯示根據配置的後備記憶體負載,SQLite整個效能可以提高10%到15%。
後備記憶體池的大小有一個全域的預設值,但可以配置成不同的值。只要在啟動時使用以下介面:
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);
其中sz為每個後備槽的位元組數,預設為100位元組。cnt是每個資料庫連接的後備記憶體槽總個數,預設值為500個槽。顯然每個資料庫連接的後備記憶體大小為sz*cnt位元組,預設為50KB。注意這些預設值針對SQLite 3.6.1,在將來版本中有可能會改變。
一個單獨資料庫連接的後備池可以更改,使用下面的調用:
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, pBuf, sz, cnt);
其中pBuf指向後備記憶體池空間。如果pBuf為NULL,SQLite將使用sqlite3_malloc()來擷取需要的記憶體池空間。sz和cnt是每個後備槽的大小和槽的總數。如果pBuf不是NULL,則必須指向至少sz*cnt位元組的空間。
資料庫連接上的後備記憶體池配置只有在記憶體還沒分配出去的情況下才能更改。因此,配置的設定應該在用sqlite3_open()(或其他建立函數)建立資料庫連接後,而在執行任何SQL語句前立刻進行。
3.5 記憶體狀態
預設情況下,SQLite統計它的記憶體使用量情況。這些統計可以確定一個應用程式真正需要多少記憶體,也可以用在高可靠系統中以確定記憶體使用量是否即將關閉或超過Robson證明的限制,從而導致記憶體配置子系統崩潰。許多記憶體統計是全域的,因此必須要互斥鎖來序列化。統計預設是開啟的,但是可以禁用它,這樣在記憶體配置或釋放是可以避免使用互斥鎖,因此節省開銷。調用下面的介面:
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);
"onoff"參數為true時啟用記憶體統計跟蹤,為false時禁用記憶體統計跟蹤。如果統計是啟用的,可以使用下面的常式來訪問它們:
sqlite3_status(verbsqlite3_status(verb, ¤t, &highwater, resetflag);
"verb"參數確定訪問哪個統計資訊,有各種各樣的verb動詞定義,可參考http://sqlite.org/c3ref/c_status_malloc_count.html#sqlitestatusmemoryused。當前選擇的值會寫入到current整型參數中,曆史最高值會寫入到highwater參數中。如果resetflag為true,則在調用返回時high-water標誌會重設為當前選擇的值。
對於單個資料庫連接的統計,有不同的介面:
sqlite3_db_status(db, verb, ¤t, &highwater, resetflag);
這個介面功能類似,只不過多一個資料庫連接參數,並且返回的是這個串連的記憶體統計資訊,而不是整個SQLite庫。sqlite3_db_status()介面當前只識別一個動詞SQLITE_DBSTATUS_LOOKASIDE_USED,在將來可能會識別更多的動詞。
每個串連的統計不使用全域變數,因此不需要互斥鎖來訪問和更新。即使SQLITE_CONFIG_MEMSTATUS關閉,每個串連的記憶體統計也會繼續進行。
3.6 設定記憶體使用量限制
sqlite3_soft_heap_limit64()介面用來設定通用記憶體 Clerk可分配的記憶體堆總量上限。如果分配的記憶體超過了這個弱的堆限制,SQLite將在繼續分配請求之前釋放緩衝的記憶體。弱的堆限制機制只在記憶體統計啟用的情況下才工作,並且如果編譯時間使用SQLITE_ENABLE_MEMORY_MANAGEMENT,它能獲得最好的工作效能。
弱的堆限制之所以稱為“弱(soft)”的,是因為如果SQLite不能釋放足夠的輔助記憶體來滿足這個限制,它會繼續分配額外的記憶體,並超過這個限制。這是基於使用額外記憶體比完全失敗更好的理論。在SQLite 3.6.1中,弱的堆限制只能應用在通用記憶體 Clerk中,它不能和臨時記憶體 Clerk、頁緩衝記憶體 Clerk、或後備記憶體 Clerk互動。在將來版本中會解決這個問題。
4、防止記憶體配置失敗的數學保證
對於動態記憶體分配問題、記憶體配置失敗問題,J.M.Robson進行過系統的研究,其結果發表在如下論文中:
J. M. Robson. "Bounds for Some Functions Concerning Dynamic Storage Allocation". Journal of the Association for Computing Machinery, Volume 21, Number 8, July 1974, pages 491-499.
我們使用下面的記號(與Robson的記號類似,但不完全等同):
N: 記憶體配置系統為了保證不會出現分配失敗而需要的原始記憶體數量。
M: 應用程式曾經在任何時間點取出的最大記憶體數量。
n: 最大記憶體配置與最小分配的比值。我們假設每個記憶體配置大小都是最小分配大小的整數倍。
Robson證明了下面的結果:
N = M*(1 + (log2 n)/2) - n + 1
通俗地講,Robson證明表明,為了防止記憶體配置失敗,任何記憶體 Clerk必須使用一個大小為N的記憶體池,它超過曾經使用的最大記憶體M乘以一個取決於n的倍數。也就是說,除非所有的記憶體配置都是同樣的大小,否則系統需要訪問比曾經使用的更大的記憶體。此外我們看到,需要的剩餘記憶體隨著比值n的增加會迅速的增長,因此我們應該保持所有的記憶體配置儘可能大小相同。
Robson證明是構造性的。他提出一個演算法來計算一個分配和釋放操作系列由於記憶體片段(可用記憶體大於1位元組但小N位元組)將導致分配失敗。Robson還證明如果可用記憶體為N或更多的位元組,一個“2的冪,首次命中”的記憶體 Clerk絕不會出現記憶體配置失敗。
M和n值是應用程式的屬性。如果建立應用程式時M和n值是已知的,或者至少知道上限值;並且如果應用程式使用memsys5記憶體 Clerk,通過SQLITE_CONFIG_HEAP提供N位元組的可用記憶體空間,Robson證明在應用程式中不會出現記憶體配置請求失敗。也就是說,應用程式開發人員可以選擇一個N值,以保證對SQLite任何介面的調用不會返回SQLITE_NOMEM,記憶體池也不會出現片段以致於不能滿足新的記憶體配置請求。這在哪些一個軟體故障就會導致損壞或關鍵資料丟失的應用程式中至關重要。
4.1 計算和控制參數M和n
Robson證明可以用在SQLite的以下記憶體 Clerk中:
* 通用記憶體 Clerk(memsys5)
* 臨時記憶體 Clerk
* 頁緩衝記憶體 Clerk
* 後備記憶體 Clerk
對除memsys5之外的其他記憶體 Clerk,所有的記憶體配置都是同樣大小的,因此n=1,N=M。也就是說,記憶體池無需比任何時刻使用的記憶體最大量還要大。
SQLite保證沒有線程同時使用超過兩個的臨時記憶體槽,因此,如果應用程式分配線程數量兩倍的臨時記憶體槽,並且每個槽足夠大,臨時記憶體 Clerk不會溢出。臨時記憶體配置大小的上限是最大頁面的6倍,因此很容易保證臨時記憶體 Clerk不出現分配失敗的操作。
在SQLite 3.6.1中,頁緩衝記憶體的使用比較難控制,唯一控制頁緩衝記憶體的方式是使用cache_size pragma指令。在以後的SQLite版本中會使頁緩衝記憶體控制變得更容易。
安全悠關的應用程式通常會修改預設的後備記憶體池配置,以致於當sqlite3_open()分配初始的後備記憶體緩衝區時,由於n參數太大而使得記憶體配置不夠大。為了使n值能夠得到控制,最大記憶體配置最好是在2或4KB以下。因此,後備記憶體 Clerk合理預設值設定最好是以下值之一:
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 */
另外一種方法是在開始時禁用後備記憶體 Clerk:
qlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);
然後讓應用程式維護一個單獨的大的後備記憶體緩衝池,在建立資料庫連接時分配給它們。通常情況下,應用程式只有單個資料庫連接,這樣後備記憶體池可以由單個的大緩衝區組成。
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, aStatic, 256, 500);
後備記憶體 Clerk主要是用於效能最佳化,而不是防止記憶體配置失敗。因此對安全悠關的應用程式完全禁用後備記憶體 Clerk是合理的。
通用記憶體 Clerk是最難管理記憶體池的,因為它支援不同大小的分配。因為n是在M上的倍數,我們需要讓n儘可能地小,並保持memsys5的最小分配大小儘可能地大。在很多應用程式中,後備記憶體 Clerk能夠處理小塊記憶體的分配。因此設定memsys5的最小分配大小為後備記憶體最大分配的2,4或8倍是合理的,512位元組的最小分配大小設定比較合理。
此外為了讓n很小,我們可能希望最大記憶體配置大小在可控範圍之內。通用記憶體 Clerk上的大分配請求可能有這麼幾個來源:
(1)SQL表中包含長字串或BLOBs的行。
(2)編譯成大的預先處理語句對象的複雜SQL查詢。
(3)sqlite3_prepare_v2()內部使用的SQL解析對象。
(4)資料庫連接的儲存空間。
(5)外流到通用記憶體 Clerk的臨時記憶體配置。
(6)外流到通用記憶體 Clerk的頁緩衝記憶體配置。
(7)新資料庫連接的後備記憶體配置。
通過適當地配置臨時記憶體 Clerk、頁緩衝記憶體 Clerk和後備記憶體 Clerk,最後三種情況可以控制或排除。資料庫連接對象需要的儲存空間取決於資料庫檔案名的長度範圍,但是在32位系統上很少超過2KB(在64位系統上由於指標大小的增加而需要更多空間)。每個解析對象大概使用1.6KB的記憶體。因此,上面的情況(3)到(7)可以把最大記憶體配置的大小控制在2KB以下。如果應用程式被設計成用來以小塊的形式管理資料,則資料庫不會包含任何長的字串或BLOBs,這樣情況(1)就不是一個因素了。如果資料庫包含長的字串或BLOBs,則應該用增量式BLOB I/O來讀取它們,其更新也應該使用增量式BLOB I/O方法,而不用其他的方法。否則,sqlite3_step()將不得不在某一時刻讀取整個行到連續的記憶體中,這涉及到至少一次大的記憶體配置。
記憶體配置的最後一個來源是複雜SQL查詢編譯成的預先處理語句對象。SQLite開發人員進行中的工作是減少這部分的記憶體空間需求。但是大的複雜SQL查詢仍然需要幾KB的預先處理語句對象。目前的變通方案是把複雜SQL操作分離成多個小的簡單的操作,以包含在各個單獨的預先處理語句對象中。
考慮所有的情況後,應用程式通常可以讓最大記憶體配置保持在2K或4K。這使log2(n)的值為2或3,限制N在M的2到2.5倍。
應用程式需要的一次通用記憶體配置最大值取決於同時有多少個開啟的資料庫連接、預先處理語句對象,以及預先處理語句對象的複雜性。對任何給定的應用,這些因素通常是固定的,並且可以實驗性地使用SQLITE_STATUS_MEMORY_USE來確定。一個典型的應用程式可能只使用40KB的通用記憶體,這使得N的值大約為100KB。
4.2 延性破壞
如果SQLite的記憶體配置子系統被配置成不會有記憶體配置失敗,但實際記憶體使用量超過Robson證明設定的限制,SQLite將繼續正常地操作。臨時記憶體 Clerk、頁緩衝記憶體 Clerk和後備記憶體 Clerk自動切換到memsys5通用記憶體 Clerk。通常memsys5記憶體 Clerk將繼續執行分配功能,即使M或n超過設定的限制也不會有記憶體片段。Robson證明表明在這種情況下一個記憶體配置有可能失敗,但是這樣的失敗需要一種特殊的分配和釋放順序,在SQLite中沒有這樣的分配釋放順序。因此在實踐中,Robson限制可以超過而不會出現壞的影響。
然而,應用程式開發人員應該記住,要監控記憶體配置子系統的狀態,當記憶體使用量超過限制時發出警報,這樣應用程式在失敗前可以提供各種警告資訊。SQLite的記憶體統計介面給應用程式提供了完成監控任務的所有機制。
5、記憶體介面的穩定性
在SQLite 3.6.1中,所有可選記憶體 Clerk和機制都是實驗性的,還不完全穩定。從SQLite 3.7.0開始,所以這些介面都是穩定的。
SQLite剖析之動態記憶體分配