小巧、快速、可靠。
三选二。
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 年)的一项创新,它源于避免在内存分配大小计算中发生 32 位整数算术溢出,从而防止下一个 "心脏出血" 漏洞 的努力。reallocarray() 函数具有单元大小和计数参数。要分配足以容纳每个大小为 X 字节的 N 个元素的数组的内存,可以调用 "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 中的默认内存分配设置适用于大多数应用程序。但是,具有特殊或特别严格要求的应用程序可能希望调整配置,以使 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 编译时选项,则该堆将与默认进程堆不同。此外,当进行分配或释放时,如果 SQLite 使用 assert() 启用并使用 SQLITE_WIN32_MALLOC_VALIDATE 编译时选项,则会调用 HeapValidate()。

3.1.4. 零 malloc 内存分配器

当 SQLite 使用 SQLITE_ENABLE_MEMSYS5 选项编译时,构建中包含了一个不使用 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 中第一个足够大的空闲插槽来满足请求。相邻的已释放分配使用伙伴系统合并。如果使用得当,此算法可以提供针对碎片和故障的数学保证,如下所述 below

3.1.5. 实验性内存分配器

用于零 malloc 内存分配器的名称“memsys5”意味着有几个其他可用的内存分配器,实际上确实有。默认内存分配器是“memsys1”。调试内存分配器是“memsys2”。这些已经介绍过了。

如果 SQLite 使用 SQLITE_ENABLE_MEMSYS3 编译,则源代码树中包含另一个类似于 memsys5 的零 malloc 内存分配器。memsys3 分配器与 memsys5 一样,必须通过调用 sqlite3_config(SQLITE_CONFIG_HEAP,...) 来激活。Memsys3 使用作为其所有内存分配源的内存缓冲区。memsys3 和 memsys5 之间的区别在于,memsys3 使用不同的内存分配算法,该算法在实践中似乎效果很好,但没有提供针对内存碎片和故障的数学保证。Memsys3 是 memsys5 的前身。SQLite 开发人员现在认为 memsys5 优于 memsys3,并且所有需要零 malloc 内存分配器的应用程序都应优先使用 memsys5 而不是 memsys3。Memsys3 被认为是实验性的和已弃用的,并且可能会在 SQLite 的未来版本中从源代码树中删除。

Memsys4 和 memsys6 是大约 2007 年引入的实验性内存分配器,随后在 2008 年左右从源代码树中删除,因为很明显它们没有增加任何新价值。

在 SQLite 的未来版本中可能会添加其他实验性内存分配器。可以预期这些将被称为 memsys7、memsys8 等。

3.1.6. 应用程序定义的内存分配器

新的内存分配器不必是 SQLite 源代码树的一部分,也不必包含在 sqlite3.c amalgamation 中。各个应用程序可以在启动时向 SQLite 提供自己的内存分配器。

要使 SQLite 使用新的内存分配器,应用程序只需调用

sqlite3_config(SQLITE_CONFIG_MALLOC, pMem);

在上面的调用中,pMem 是指向 sqlite3_mem_methods 对象的指针,该对象定义了特定于应用程序的内存分配器的接口。sqlite3_mem_methods 对象实际上只是一个结构,其中包含指向函数的指针,用于实现各种内存分配原语。

在多线程应用程序中,只有在启用了 SQLITE_CONFIG_MEMSTATUS 时,才会对 sqlite3_mem_methods 的访问进行序列化。如果禁用了 SQLITE_CONFIG_MEMSTATUS,则 sqlite3_mem_methods 中的方法必须自行处理其序列化需求。

3.1.7. 内存分配器覆盖

应用程序可以在 SQLite 内核和底层内存分配器之间插入层或“覆盖”。例如,SQLite 的 内存不足测试逻辑 使用一个可以模拟内存分配失败的覆盖。

可以通过使用

sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem);

接口来获取指向现有内存分配器的指针。覆盖会保存现有分配器,并将其用作回退来执行实际的内存分配。然后,使用 sqlite3_config(SQLITE_CONFIG_MALLOC,...) 将覆盖插入到现有内存分配器的位置,如 above 所述。

3.1.8. 无操作内存分配器存根

如果 SQLite 使用 SQLITE_ZERO_MALLOC 选项编译,则 默认内存分配器 将被省略并替换为一个存根内存分配器,该分配器永远不会分配任何内存。对存根内存分配器的任何调用都会报告没有可用内存。

无操作内存分配器本身没有用。它只作为占位符存在,以便 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年8月6日)引入了备用内存分配器,以帮助减少内存分配负载。在备用内存分配器中,每个 数据库连接 预先分配一个大的内存块(通常在 60 到 120 千字节之间),并将该块划分为大小固定的“槽”,每个槽大约 100 到 1000 字节。这成为备用内存池。此后,与 数据库连接 相关的并且不太大的内存分配将使用备用内存池中的一个槽来满足,而不是调用通用内存分配器。较大的分配将继续使用通用内存分配器,当备用内存池中的所有槽都被占用时,分配也会使用通用内存分配器。但在许多情况下,内存分配足够小,并且未完成的分配数量足够少,因此新的内存请求可以从备用内存池中得到满足。

由于备用内存分配的大小始终相同,因此分配和释放算法非常快。无需合并相邻的空闲槽或搜索特定大小的槽。每个 数据库连接 都维护一个未使用的槽的单向链表。分配请求只需取出此列表的第一个元素。释放操作只需将元素重新推入列表的前面。此外,假设每个 数据库连接 已经在单个线程中运行(已经存在互斥锁来强制执行此操作),因此不需要额外的互斥锁来序列化对备用内存槽空闲列表的访问。因此,备用内存分配和释放非常快。在 Linux 和 Mac OS X 工作站上的速度测试中,SQLite 显示出高达 10% 和 15% 的整体性能提升,具体取决于工作负载以及备用内存的配置方式。

备用内存池的大小有一个全局默认值,但也可以在每个连接的基础上进行配置。要在编译时更改备用内存池的默认大小,请使用 -DSQLITE_DEFAULT_LOOKASIDE=SZ,N 选项。要在启动时更改备用内存池的默认大小,请使用 sqlite3_config() 接口。

sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);

“sz”参数是每个备用内存槽的大小(以字节为单位)。“cnt”参数是每个数据库连接的备用内存槽的总数。分配给每个 数据库连接 的备用内存总量为 sz*cnt 字节。

可以使用以下调用更改单个 数据库连接 “db”的备用内存池。

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.3.1. 双大小备用内存

从 SQLite 3.31.0 版本(2020年1月22日)开始,备用内存支持两个内存池,每个内存池都有不同大小的槽。小槽池使用 128 字节的槽,大槽池使用由 SQLITE_DBCONFIG_LOOKASIDE 指定的大小(默认为 1200 字节)。像这样将池分成两部分,可以更频繁地通过备用内存来满足内存分配,同时将每个数据库连接的堆使用量从 120KB 降至 48KB。

配置继续使用上面描述的 SQLITE_DBCONFIG_LOOKASIDE 或 SQLITE_CONFIG_LOOKASIDE 配置选项,以及参数“sz”和“cnt”。用于备用内存的总堆空间仍然是 sz*cnt 字节。但该空间分配在小槽备用内存和大槽备用内存之间,优先使用小槽备用内存。槽的总数通常会超过“cnt”,因为“sz”通常远大于 128 字节的小槽大小。

默认备用内存配置已从 100 个 1200 字节的槽(120KB)更改为 40 个 1200 字节的槽(48KB)。此空间最终将分配为 93 个 128 字节的槽和 30 个 1200 字节的槽。因此,可用的备用内存槽更多,但使用的堆空间更少。

默认备用内存配置、小槽的大小以及如何在小槽和大槽之间分配堆空间的细节,在每个版本之间都可能发生变化。

3.4. 内存状态

默认情况下,SQLite 会跟踪其内存使用情况的统计信息。这些统计信息有助于确定应用程序真正需要的内存量。统计信息还可以用于高可靠性系统中,以确定内存使用量是否接近或超过 Robson 证明 的限制,因此内存分配子系统可能发生故障。

大多数内存统计信息是全局的,因此统计信息的跟踪必须与互斥锁同步。统计信息默认情况下是打开的,但存在一个选项可以禁用它们。通过禁用内存统计信息,SQLite 避免了在每次内存分配和释放时进入和离开互斥锁。在互斥锁操作代价高昂的系统上,这种节省可能是明显的。要禁用内存统计信息,请在启动时使用以下接口。

sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);

“onoff”参数为真表示启用内存统计信息的跟踪,为假表示禁用统计信息的跟踪。

假设启用了统计信息,可以使用以下例程访问它们。

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

“verb”参数确定访问哪个统计信息。定义了 各种动词。随着 sqlite3_status() 接口的成熟,此列表预计会增长。所选参数的当前值将写入整数“current”,历史最高值将写入整数“highwater”。如果 resetflag 为真,则在调用返回后,高水位标记将重置为当前值。

使用不同的接口查找与单个 数据库连接 相关的统计信息。

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

此接口类似,只是它以指向 数据库连接 的指针作为其第一个参数,并返回有关该对象的统计信息,而不是有关整个 SQLite 库的统计信息。sqlite3_db_status() 接口目前只识别一个动词 SQLITE_DBSTATUS_LOOKASIDE_USED,但将来可能会添加其他动词。

每个连接的统计信息不使用全局变量,因此不需要互斥锁来更新或访问。因此,即使 SQLITE_CONFIG_MEMSTATUS 被关闭,每个连接的统计信息仍将继续工作。

3.5. 设置内存使用限制

sqlite3_soft_heap_limit64() 接口可用于设置 SQLite 通用内存分配器一次允许挂起的内存总量的上限。如果尝试分配的内存超过软堆限制,则 SQLite 将首先尝试释放缓存内存,然后再继续执行分配请求。只有在启用了 内存统计信息 时,软堆限制机制才有效,并且如果 SQLite 库使用 SQLITE_ENABLE_MEMORY_MANAGEMENT 编译时选项进行编译,则效果最佳。

软堆限制在以下意义上是“软的”:如果 SQLite 无法释放足够的辅助内存以保持在限制以下,它将继续分配额外的内存并超出其限制。这是基于这样的理论:使用额外的内存比完全失败更好。

从 SQLite 3.6.1 版本(2008年8月6日)开始,软堆限制仅适用于通用内存分配器。软堆限制不知道或不与 页面缓存内存分配器备用内存分配器 交互。此缺陷可能会在将来的版本中解决。

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 字节或更多,则 2 的幂次方首位适配内存分配器(例如由 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 版本中,控制 pagecache 内存的使用相对困难,尽管后续版本计划引入一些机制来简化 pagecache 内存的控制。在这些新机制引入之前,控制 pagecache 内存的唯一方法是使用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. 溢出到通用内存分配器的 pagecache 内存分配。
  6. 用于新数据库连接的备用缓冲区分配。

最后两个分配可以通过配置pagecache 内存分配器备用内存分配器(如上所述)来控制和/或消除。用于数据库连接对象的存储空间在一定程度上取决于数据库文件的文件名长度,但在 32 位系统上很少超过 2KB。(由于指针大小增加,64 位系统需要更多空间。)每个解析器对象使用大约 1.6KB 的内存。因此,上面第 3 到第 6 个元素可以轻松地控制,以将最大内存分配大小保持在 2KB 以下。

如果应用程序设计为以小块数据进行管理,那么数据库永远不应该包含任何大型字符串或 BLOB,因此上面第 1 个元素不应该成为因素。如果数据库确实包含大型字符串或 BLOB,则应使用增量 BLOB I/O读取它们,并且包含大型字符串或 BLOB 的行不应该以任何其他方式更新,除了增量 BLOB I/O。否则,sqlite3_step()例程需要在某个时刻将整行读取到连续内存中,这将涉及至少一次大型内存分配。

大型内存分配的最后一个来源是用于保存编译复杂 SQL 操作生成的预处理语句的空间。SQLite 开发人员正在进行的工作正在减少此处所需的空间量。但是,大型和复杂的查询可能仍然需要大小为几千字节的预处理语句。目前唯一的解决方法是应用程序将复杂的 SQL 操作分解成两个或多个包含在单独预处理语句中的更小、更简单的操作。

综上所述,应用程序通常能够将其最大内存分配大小保持在 2K 或 4K 以下。这使得log2(n)的值为 2 或 3。这将限制NM的 2 到 2.5 倍之间。

应用程序所需的通用内存最大量取决于诸如应用程序使用多少个同时打开的数据库连接预处理语句对象以及预处理语句的复杂性等因素。对于任何给定的应用程序,这些因素通常是固定的,并且可以使用SQLITE_STATUS_MEMORY_USED通过实验确定。一个典型的应用程序可能只需要大约 40KB 的通用内存。这使得N的值约为 100KB。

4.2. 延性失效

如果 SQLite 中的内存分配子系统配置为无故障操作,但实际内存使用量超过了Robson 证明设置的设计限制,则 SQLite 通常会继续正常运行。pagecache 内存分配器备用内存分配器会自动故障转移到memsys5通用内存分配器。并且通常情况下,即使M和/或n超过了Robson 证明施加的限制,memsys5内存分配器也会继续工作而不会出现碎片。 Robson 证明表明,在这种情况下,内存分配可能会发生故障并失败,但这种失败需要一个特别卑鄙的分配和释放序列——SQLite 从未观察到遵循的序列。因此,在实践中,通常情况下,Robson 施加的限制可以被大幅超过而不会产生不良影响。

尽管如此,应用程序开发人员还是被告诫要监控内存分配子系统状态,并在内存使用量接近或超过 Robson 限制时发出警报。通过这种方式,应用程序将在故障发生之前为操作人员提供充足的预警。内存统计SQLite 的接口为应用程序提供了完成此任务的监控部分所需的所有机制。

5. 内存接口的稳定性

更新:从 SQLite 3.7.0(2010-07-21)版本开始,所有 SQLite 内存分配接口都被认为是稳定的,并将得到未来版本的支持。