小巧。快速。可靠。
任選三項。

記憶體映射 I/O

SQLite 存取和更新資料庫磁碟檔案的預設機制是 sqlite3_io_methods VFS 物件的 xRead() 和 xWrite() 方法。這些方法通常實作為「read()」和「write()」系統呼叫,導致作業系統在核心緩衝快取和使用者空間之間複製磁碟內容。

版本 3.7.17(2013-05-20)開始,SQLite 可以選擇使用記憶體映射 I/O 和 sqlite3_io_methods 上的新 xFetch() 和 xUnfetch() 方法直接存取磁碟內容。

使用記憶體映射 I/O 有優點也有缺點。優點包括

  1. 許多作業,特別是 I/O 密集作業,可以更快,因為無需在核心空間和使用者空間之間複製內容。

  2. SQLite 函式庫可能需要較少的 RAM,因為它與作業系統頁面快取共用頁面,而且不總是需要它自己的工作頁面副本。

但也有缺點

  1. 記憶體映射檔案上的 I/O 錯誤無法由 SQLite 捕捉和處理。相反地,I/O 錯誤會導致訊號,如果應用程式沒有捕捉到,就會導致程式崩潰。

  2. 作業系統必須具有統一的緩衝快取,才能讓記憶體映射 I/O 擴充功能正確運作,特別是在兩個處理序存取同一個資料庫檔案,而且一個處理序使用記憶體映射 I/O 而另一個沒有使用的情況下。並非所有作業系統都有統一的緩衝快取。在一些宣稱具有統一緩衝快取的作業系統中,實作有錯誤,可能會導致資料庫損毀。

  3. 效能並非總是會隨著記憶體映射 I/O 而提升。事實上,可以建構使用記憶體映射 I/O 會降低效能的測試案例。

  4. Windows 無法截斷記憶體映射檔案。因此,在 Windows 上,如果 VACUUMauto_vacuum 等作業嘗試縮減記憶體映射資料庫檔案的大小,縮減大小的嘗試會靜默失敗,在資料庫檔案的結尾留下未使用的空間。此問題不會造成資料遺失,而未使用的空間會在資料庫下次擴充時再次使用。但是,如果 3.7.0 之前的 SQLite 版本對此類資料庫執行 PRAGMA integrity_check,它會(錯誤地)報告資料庫毀損,原因是結尾處的未使用的空間。或者,如果 3.7.0 之前的 SQLite 版本寫入資料庫,而資料庫結尾處仍有未使用的空間,它可能會使該未使用的空間無法存取,且在下次 VACUUM 之前無法重複使用。

由於潛在的缺點,記憶體映射 I/O 預設為停用。若要啟用記憶體映射 I/O,請使用 mmap_size pragma,並將 mmap_size 設定為一些大數字,通常是 256MB 或更大,具體取決於您的應用程式可以保留多少位址空間。其餘的會自動執行。PRAGMA mmap_size 陳述式在不支援記憶體映射 I/O 的系統上會是靜默的無操作。

記憶體映射 I/O 的運作方式

若要使用舊版的 xRead() 方法讀取資料庫內容的頁面,SQLite 首先會配置一個頁面大小的堆疊記憶體區塊,然後呼叫 xRead() 方法,這會導致資料庫頁面內容複製到新配置的堆疊記憶體中。這至少會涉及整個頁面的複製。

但是,如果 SQLite 想存取資料庫檔案的頁面,且記憶體映射 I/O 已啟用,它會先呼叫 xFetch() 方法。xFetch() 方法會要求作業系統傳回所要求頁面的指標(如果可能)。如果已將所要求的頁面映射到應用程式位址空間,或可以將其映射到應用程式位址空間,則 xFetch 會傳回該頁面的指標供 SQLite 使用,而無需複製任何內容。略過複製步驟就是記憶體映射 I/O 更快的原因。

SQLite 沒有假設 xFetch() 方法會運作。如果對 xFetch() 的呼叫傳回 NULL 指標(表示要求的頁面目前未對應到應用程式的位址空間),SQLite 會在不聲不響的情況下改用 xRead()。只有在 xRead() 也失敗時才會回報錯誤。

在更新資料庫檔案時,SQLite 會在修改頁面之前,先將頁面內容複製到堆疊記憶體中。這有兩個原因。首先,資料庫的變更不應在交易提交之前對其他程序可見,因此變更必須在私人記憶體中進行。其次,SQLite 使用唯讀記憶體對應來防止應用程式中的雜散指標覆寫和損毀資料庫檔案。

在完成所有需要的變更後,xWrite() 會用來將內容移回資料庫檔案中。因此,使用記憶體對應的 I/O 沒有顯著改變資料庫變更的效能。記憶體對應的 I/O 主要對查詢有益。

設定記憶體對應的 I/O

「mmap_size」是 SQLite 一次會嘗試對應到程序位址空間的資料庫檔案的最大位元組數。mmap_size 會個別套用於每個資料庫檔案,因此程序位址空間可能使用的總量是 mmap_size 乘上開啟的資料庫檔案數量。

若要啟用記憶體對應的 I/O,應用程式可以將 mmap_size 設定為一些較大的值。例如

PRAGMA mmap_size=268435456;

若要停用記憶體對應的 I/O,只要將 mmap_size 設定為零即可

PRAGMA mmap_size=0;

如果 mmap_size 設定為 N,則所有目前的實作會對應資料庫檔案的前 N 個位元組,並對超過 N 個位元組的任何內容使用舊版的 xRead() 呼叫。如果資料庫檔案小於 N 個位元組,則會對應整個檔案。在未來,新的作業系統介面理論上可以對應檔案中除了前 N 個位元組以外的區域,但目前沒有這樣的實作。

mmap_size 會使用「PRAGMA mmap_size」陳述句針對每個資料庫檔案個別設定。mmap_size 的一般預設值為零,表示預設會停用記憶體對應 I/O。不過,mmap_size 的預設值可以在編譯時使用 SQLITE_DEFAULT_MMAP_SIZE 巨集,或在啟動時使用 sqlite3_config(SQLITE_CONFIG_MMAP_SIZE,...) 介面來增加。

SQLite 也會針對 mmap_size 保留一個嚴格的上限。嘗試將 mmap_size 增加到這個嚴格的上限以上(使用 PRAGMA mmap_size)會自動將 mmap_size 上限設為嚴格的上限。如果嚴格的上限為零,則記憶體對應 I/O 會無法使用。嚴格的上限可以在編譯時使用 SQLITE_MAX_MMAP_SIZE 巨集來設定。如果 SQLITE_MAX_MMAP_SIZE 設為零,則用於實作記憶體對應 I/O 的程式碼會從建置中省略。在某些平台(例如:OpenBSD)上,嚴格的上限會自動設為零,因為這些平台由於缺乏統一的緩衝快取,所以記憶體對應 I/O 無法使用。

如果 mmap_size 的嚴格上限在編譯時不為零,它仍然可以在啟動時使用 sqlite3_config(SQLITE_CONFIG_MMAP_SIZE,X,Y) 介面來減少或歸零。X 和 Y 參數都必須是 64 位元有號整數。X 參數是程式的 mmap_size 預設值,Y 是新的嚴格上限。嚴格的上限無法使用 SQLITE_CONFIG_MMAP_SIZE 增加到超過其編譯時設定值,但可以減少或歸零。