概述
SQLite 使用动态内存分配来获取存储各种对象(例如:数据库连接和准备好的语句)的内存,并构建数据库文件的内存缓存并保存查询结果。为了使 SQLite 的动态内存分配子系统可靠、可预测、健壮、安全和高效,人们付出了很多努力。
本文档概述了 SQLite 中的动态内存分配。目标受众是正在调整 SQLite 的使用以在要求苛刻的环境中实现最佳性能的软件工程师。本文档中的任何内容都不是使用 SQLite 所需的知识。SQLite 的默认设置和配置适用于大多数应用程序。但是,本文档中包含的信息可能对调整 SQLite 以符合特殊要求或在异常情况下运行的工程师有用。
一、特点
SQLite 核心及其内存分配子系统提供以下功能:
对分配失败具有鲁棒性。 如果内存分配失败(也就是说,如果 malloc() 或 realloc() 返回 NULL),那么 SQLite 将正常恢复。SQLite 将首先尝试从未固定的缓存页面中释放内存,然后重试分配请求。如果做不到这一点,SQLite 要么停止它正在做的事情并将 SQLITE_NOMEM错误代码返回给应用程序,要么它将在没有请求的内存的情况下凑合。
没有内存泄漏。 应用程序负责销毁它分配的任何对象。(例如,应用程序必须在每个准备好的语句上使用sqlite3_finalize()并在每个 数据库连接上使用 sqlite3_close() 。)但只要应用程序合作,SQLite 就永远不会泄漏内存。即使面对内存分配失败或其他系统错误也是如此。
内存使用限制。sqlite3_soft_heap_limit64()机制允许应用程序设置内存使用限制,SQLite 努力保持在该限制之下 。SQLite 将尝试重用其缓存中的内存,而不是在接近软限制时分配新内存。
零 malloc 选项。 应用程序可以选择在启动时为 SQLite 提供多个大容量内存缓冲区,然后 SQLite 将使用这些提供的缓冲区来满足其所有内存分配需求,并且永远不会调用系统 malloc() 或 free()。
应用程序提供的内存分配器。 应用程序可以在启动时为 SQLite 提供指向替代内存分配器的指针。备用内存分配器将用于代替系统 malloc() 和 free()。
防止故障和碎片化。 可以配置 SQLite,以便根据下面详述的某些使用限制,保证内存分配永远不会失败或堆碎片。此属性对于长期运行、高可靠性的嵌入式系统很重要,在这些系统中,内存分配错误可能会导致整个系统故障。
内存使用统计。 应用程序可以看到他们正在使用多少内存,并检测内存使用何时接近或超过设计边界。
与内存调试器配合良好。 SQLite 中的内存分配是结构化的,因此可以使用标准的第三方内存调试器(例如dmalloc或 valgrind)来验证正确的内存分配行为。
对分配器的调用最少。 系统 malloc() 和 free() 实现在许多系统上效率低下。SQLite 通过最小化 malloc() 和 free() 的使用来努力减少整体处理时间。
开放访问。 可插入的 SQLite 扩展甚至应用程序本身都可以通过 sqlite3_malloc()、sqlite3_realloc()和sqlite3_free()接口访问 SQLite 使用的相同底层内存分配例程。
2.测试
SQLite 源代码树中的大部分代码纯粹用于 测试和验证。可靠性对 SQLite 很重要。测试基础设施的任务之一是确保 SQLite 不会滥用动态分配的内存,SQLite 不会泄漏内存,以及 SQLite 正确响应动态内存分配失败。
测试基础设施通过使用专门检测的内存分配器来验证 SQLite 不会滥用动态分配的内存。使用SQLITE_MEMDEBUG选项在编译时启用检测内存分配器。检测内存分配器比默认内存分配器慢得多,因此不建议在生产中使用它。但是在测试期间启用时,检测内存分配器会执行以下检查:
边界检查。 检测内存分配器在每个内存分配的两端放置哨兵值,以验证 SQLite 中没有任何内容写入分配范围之外。
释放后使用内存。 当释放每个内存块时,每个字节都会被无意义的位模式覆盖。这有助于确保在释放后不再使用任何内存。
释放不是从 malloc 获得的内存。 来自检测内存分配器的每个内存分配都包含用于验证释放的每个分配是否来自先前的 malloc 的哨兵。
未初始化的内存。 检测内存分配器将每个内存分配初始化为无意义的位模式,以帮助确保用户不对分配内存的内容做出任何假设。
无论是否使用检测内存分配器,SQLite 都会跟踪当前检出的内存量。有数百个测试脚本用于测试 SQLite。在每个脚本结束时,所有对象都被销毁,并进行测试以确保所有内存都已被释放。这就是检测内存泄漏的方式。请注意,在测试构建期间和生产构建期间,内存泄漏检测始终有效。每当其中一位开发人员运行任何单独的测试脚本时,内存泄漏检测就会激活。因此,在开发过程中出现的内存泄漏会被快速检测和修复。
SQLite 对内存不足 (OOM) 错误的响应使用可以模拟内存故障的专用内存分配器覆盖进行测试。覆盖层是插入内存分配器和 SQLite 其余部分之间的层。覆盖层将大多数内存分配请求直接传递给底层分配器,并将结果传递回请求者。但是可以设置overlay导致第N次内存分配失败。要运行 OOM 测试,覆盖首先设置为在第一次分配尝试时失败。然后运行一些测试脚本并验证分配是否被正确捕获和处理。然后覆盖设置为在第二次分配时失败并重复测试。失败点继续一次推进一个分配,直到整个测试过程运行完成而没有遇到内存分配错误。整个测试序列运行两次。在第一遍中,覆盖设置为仅在第 N 次分配时失败。在第二遍中,覆盖设置为在第 N 次和所有后续分配中失败。
请注意,即使正在使用 OOM 覆盖,内存泄漏检测逻辑也会继续工作。这验证了 SQLite 即使在遇到内存分配错误时也不会泄漏内存。另请注意,OOM 覆盖可以与任何底层内存分配器一起使用,包括检查内存分配滥用的检测内存分配器。这样就验证了OOM错误不会引发其他类型的内存使用错误。
最后,我们观察到检测内存分配器和内存泄漏检测器都在整个 SQLite 测试套件上工作,TCL 测试套件提供超过 99% 的语句测试覆盖率,TH3测试工具提供100% 的分支测试覆盖率 ,没有泄漏. 这是动态内存分配在 SQLite 中被正确使用的有力证据。
2.1. reallocarray()的使用
reallocarray() 接口是 OpenBSD 社区最近的一项创新(大约 2014 年),它通过避免内存分配大小计算上的 32 位整数算术溢出来防止下一个“heartbleed”错误。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 位大小参数为检测溢出提供了大量空间。
用于确保内存分配大小计算不会在 SQLite 中溢出的代码审计在每个 SQLite 版本之前重复进行。
三、配置
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() 周围使用一个不同的、重型包装器。heavy wrapper 在每次分配时分配大约 100 字节的额外空间。额外的空间用于在返回给 SQLite 核心的分配的两端放置标记值。释放分配后,将检查这些哨兵以确保 SQLite 核心没有在任一方向上溢出缓冲区。当系统库是 GLIBC 时,重型包装器还使用 GNU backtrace() 函数来检查堆栈并记录 malloc() 调用的祖先函数。在运行 SQLite 测试套件时,heavy wrapper 还会记录当前测试用例的名称。后两个功能对于跟踪测试套件检测到的内存泄漏源非常有用。
设置SQLITE_MEMDEBUG时使用的重型包装器还确保在将分配返回给调用者之前,每个新分配都填充了无意义的数据。一旦分配空闲,它就会再次充满无意义的数据。这两个操作有助于确保 SQLite 核心不会对新分配内存的状态做出假设,并且内存分配在释放后不会被使用。
SQLITE_MEMDEBUG使用的重型包装器仅供在 SQLite 的测试、分析和调试期间使用。重型包装器具有显着的性能和内存开销,可能不应该在生产中使用。
3.1.3. Win32 本机内存分配器
如果使用SQLITE_WIN32_MALLOC编译时选项为 Windows 编译 SQLite ,则在 HeapAlloc()、HeapReAlloc() 和 HeapFree() 周围使用不同的薄包装器。瘦包装器使用配置的 SQLite 堆,如果使用SQLITE_WIN32_HEAP_CREATE编译时选项,它将与默认进程堆不同。此外,当进行或释放分配时,如果 SQLite 是在启用 assert() 和 SQLITE_WIN32_MALLOC_VALIDATE编译时选项的情况下编译的,则将调用 HeapValidate()。
3.1.4. 零 malloc 内存分配器
当使用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 必须是二的幂。稍后我们将看到,mnReq 参数对于减少n的值以及Robson 证明中的最小内存大小要求很重要。
memsys5 分配器设计用于嵌入式系统,尽管没有什么可以阻止它在工作站上的使用。szBuf 通常在几百 KB 到几十兆字节之间,具体取决于系统要求和内存预算。
memsys5 使用的算法可以称为“二次幂,首次拟合”。所有内存分配请求的大小四舍五入为 2 的幂,请求由 pBuf 中第一个足够大的空闲槽满足。相邻的释放分配使用伙伴系统合并。如果使用得当,该算法可提供防止碎片化和故障的数学保证,如下所述。
3.1.5. 实验内存分配器
用于零 malloc 内存分配器的名称“memsys5”暗示有几个额外的内存分配器可用,而且确实有。默认内存分配器是“memsys1”。调试内存分配器是“memsys2”。这些已经涵盖了。
如果 SQLite 是用SQLITE_ENABLE_MEMSYS3编译的,那么另一个零 malloc 内存分配器,类似于 memsys5,包含在源代码树中。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合并中。各个应用程序可以在启动时向 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 ,...) 如上所述插入覆盖层以代替现有的内存分配器。
3.1.8. 无操作内存分配器存根
如果 SQLite 是使用SQLITE_ZERO_MALLOC选项编译的,那么默认内存分配器将被省略并替换为从不分配任何内存的存根内存分配器。对存根内存分配器的任何调用都会报告没有内存可用。
空操作内存分配器本身没有用。它仅作为占位符存在,以便 SQLite 有一个内存分配器可以链接到标准库中可能没有 malloc()、free() 或 realloc() 的系统。使用SQLITE_ZERO_MALLOC编译的应用程序需要在开始使用 SQLite 之前将sqlite3_config()与SQLITE_CONFIG_MALLOC或 SQLITE_CONFIG_HEAP一起使用以指定新的替代内存分配器。
3.2. 页面缓存内存
在大多数应用程序中,SQLite 中的数据库页面缓存子系统使用的动态分配内存比 SQLite 的所有其他部分加起来还要多。数据库页面缓存消耗的内存比其余 SQLite 的总和多 10 倍并不罕见。
SQLite 可以配置为从一个独立且不同的固定大小插槽内存池进行页面缓存内存分配。这有两个好处:
因为分配的大小都相同,所以内存分配器可以运行得更快。分配器无需费心合并相邻的空闲槽或搜索合适大小的槽。所有未分配的内存槽都可以存储在链表中。分配包括从列表中删除第一个条目。取消分配只是在列表的开头添加一个条目。
对于单个分配大小,Robson 证明中的n参数 为 1,分配器所需的总内存空间 ( N ) 恰好等于使用的最大内存 ( M )。不需要额外的内存来覆盖碎片开销,从而减少内存需求。这对于页面缓存尤其重要,因为页面缓存构成了 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-Tree 游标对象等内容。因此有很多调用 malloc() 和 free() - 如此多的调用以至于 malloc() 和 free() 最终使用了分配给 SQLite 的 CPU 时间的很大一部分。
SQLite版本 3.6.1 (2008-08-06) 引入了后备内存分配器以帮助减少内存分配负载。在后备分配器中,每个数据库连接预分配一个大的内存块(通常在 60 到 120 KB 的范围内)并将该块分成大约 100 到 1000 字节的固定大小的小“槽”。这成为后备内存池。此后,与数据库连接相关的内存分配使用后备池槽之一而不是通过调用通用内存分配器来满足不太大的内存。更大的分配继续使用通用内存分配器,当后备池槽都被签出时发生的分配也是如此。但在许多情况下,内存分配足够小,而且未完成的内存也很少,新的内存请求可以从后备池中得到满足。
因为后备分配总是相同的大小,所以分配和释放算法非常快。无需合并相邻的空闲插槽或搜索特定大小的插槽。每个数据库连接都维护一个未使用槽的单链表。分配请求只是拉取此列表的第一个元素。释放只是将元素推回到列表的前面。此外,每个数据库连接假定已经在单个线程中运行(已经有互斥锁来强制执行此操作),因此不需要额外的互斥锁来序列化对后备槽空闲列表的访问。因此,后备内存分配和释放非常快。在 Linux 和 Mac OS X 工作站上的速度测试中,SQLite 的整体性能提升高达 10% 和 15%,具体取决于工作负载的配置方式和 lookaside 的配置方式。
后备内存池的大小具有全局默认值,但也可以在逐个连接的基础上进行配置。要在编译时更改后备内存池的默认大小,请使用 -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-01-22) 开始,lookaside 支持两个内存池,每个内存池具有不同大小的插槽。小槽池使用 128 字节槽,大槽池使用SQLITE_DBCONFIG_LOOKASIDE指定的任何大小 (默认为 1200 字节)。像这样将池一分为二允许内存分配更频繁地被旁视覆盖,同时将每个数据库连接的堆使用量从 120KB 减少到 48KB。
配置继续使用 SQLITE_DBCONFIG_LOOKASIDE 或 SQLITE_CONFIG_LOOKASIDE 配置选项,如上所述,参数为“sz”和“cnt”。用于后备的总堆空间仍然是 sz*cnt 字节。但是空间分配在small-slot lookaside和big-slot lookaside之间,优先考虑small-slot lookaside。槽的总数通常会超过“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”参数为 true 以启用内存统计跟踪,为 false 以禁用统计跟踪。
假设启用了统计信息,可以使用以下例程访问它们:
sqlite3_status(verb, ¤t, &highwater, resetflag);
“动词”参数确定访问的统计信息。定义了各种动词。随着sqlite3_status()接口的成熟,该列表预计会增加。所选参数的当前值写入整数“current”,历史最高值写入整数“highwater”。如果 resetflag 为真,则在调用返回后将高水位线重置为当前值。
一个不同的界面用于查找与单个数据库连接相关的统计信息:
sqlite3_db_status(db, verb, ¤t, &highwater, resetflag);
这个接口是相似的,只是它接受一个指向数据库连接的指针作为它的第一个参数,并返回关于那个对象的统计信息,而不是关于整个 SQLite 库的统计信息。sqlite3_db_status()接口目前只能识别单个动词SQLITE_DBSTATUS_LOOKASIDE_USED,但将来可能会添加其他动词。
每个连接的统计信息不使用全局变量,因此不需要互斥锁来更新或访问。因此,即使 SQLITE_CONFIG_MEMSTATUS已关闭,每个连接的统计信息仍会继续运行。
3.5. 设置内存使用限制
sqlite3_soft_heap_limit64()接口可用于设置 SQLite 的通用内存分配器一次允许未完成内存总量的上限。如果尝试分配的内存超过软堆限制指定的内存,则 SQLite 将首先尝试释放缓存内存,然后再继续分配请求。软堆限制机制仅在启用内存统计信息时有效,并且如果使用SQLITE_ENABLE_MEMORY_MANAGEMENT 编译时选项编译SQLite 库,则该机制效果最佳。
软堆限制在这个意义上是“软”的:如果 SQLite 不能释放足够的辅助内存以保持在限制以下,它会继续分配额外的内存并超过其限制。这发生在使用额外内存比彻底失败更好的理论下。
从 SQLite版本 3.6.1 (2008-08-06) 开始,软堆限制仅适用于通用内存分配器。软堆限制不知道页面缓存内存分配器或后备内存分配器,也不会与之交互。此缺陷可能会在未来的版本中得到解决。
4.内存分配失败的数学保证
JM Robson 研究了动态内存分配问题,特别是内存分配器故障问题,结果发表如下:
JM罗布森。“有关动态存储分配的某些功能的界限”。 计算机协会杂志,第 21 卷,第 8 期,1974 年 7 月,第 491-499 页。
让我们使用以下符号(与 Robson 的符号相似但不完全相同):
N The amount of raw memory needed by the memory allocation system in order to guarantee that no memory allocation will ever fail. M The maximum amount of memory that the application ever has checked out at any point in time. n The ratio of the largest memory allocation to the smallest. We assume that every memory allocation size is an integer multiple of the smallest memory allocation size.
罗布森证明了以下结果:
N = M *(1 + (log 2 n )/2) - n + 1
通俗地说,Robson 证明表明,为了保证无故障操作,任何内存分配器都必须使用大小为N的内存池,该内存池超过了乘数M所使用的最大内存量,该乘数取决于n,最大的比率到最小的分配大小。换句话说,除非所有内存分配的大小都完全相同(n = 1),否则系统需要访问比它一次使用的更多的内存。此外,我们看到随着最大分配与最小分配之比的增加,所需的剩余内存量迅速增长,因此有强烈的动机使所有分配尽可能接近相同的大小。
Robson 的证明是建设性的。他提供了一种算法,用于计算一系列分配和释放操作,如果可用内存比N少一个字节,则会由于内存碎片导致分配失败。并且,Robson 表明,只要可用内存为N或更多字节,二次幂首次适配内存分配器(例如由memsys5实现)永远不会使内存分配失败。
值M和n是应用程序的属性。如果应用程序以M和 n已知的方式构造,或者至少已知上限,并且如果应用程序使用memsys5内存分配器并使用SQLITE_CONFIG_HEAP提供N字节的可用内存空间, 那么 Robson 证明:应用程序中没有内存分配请求会失败。换句话说,应用程序开发人员可以为N选择一个值,该值将保证对任何 SQLite 接口的调用都不会返回SQLITE_NOMEM. 内存池永远不会碎片化到无法满足新的内存分配请求。对于软件故障可能导致伤害、身体伤害或不可替代数据丢失的应用程序,这是一个重要属性。
4.1. 计算和控制参数M和n
Robson 证明分别适用于 SQLite 使用的每个内存分配器:
对于memsys5以外的分配器,所有内存分配的大小都相同。因此,n = 1 ,因此N = M。换句话说,内存池不需要大于在任何给定时刻使用的最大内存量。
在 SQLite 版本 3.6.1 中,页面缓存内存的使用有点难以控制,尽管后续版本计划了一些机制,这将使控制页面缓存内存变得更加容易。在引入这些新机制之前,控制页面缓存内存的唯一方法是使用cache_size pragma。
安全关键应用程序通常希望修改默认后备内存配置,以便在sqlite3_open()期间分配初始后备内存缓冲区时,生成的内存分配不会大到强制n 参数太大。为了控制n,最好尝试将最大内存分配保持在 2 或 4 KB 以下。因此,后备内存分配器的合理默认设置可能是以下任何一项:
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);
后备内存分配器的真正目的是作为性能优化,而不是作为确保无故障内存分配的方法,因此完全禁用后备内存分配器以进行安全关键操作并非不合理。
通用内存分配器是最难管理的内存池,因为它支持不同大小的分配。由于 n是M的乘数,我们希望n尽可能小。这主张将 memsys5的最小分配大小保持尽可能大。在大多数应用程序中, 后备内存分配器能够处理小的分配。因此,将memsys5的最小分配大小设置为后备分配最大大小的 2、4 甚至 8 倍是合理的。最小分配大小 512 是一个合理的设置。
为了进一步保持n较小,人们希望控制最大内存分配的大小。对通用内存分配器的大请求可能来自几个来源:
- 包含大字符串或 BLOB 的 SQL 表行。
- 编译成大型准备语句的复杂 SQL 查询。
- sqlite3_prepare_v2()内部使用的 SQL 解析器对象。
- 数据库连接对象的存储空间。
- 溢出到通用内存分配器中的页面缓存内存分配。
- 新数据库连接的后备缓冲区分配。
如上所述,可以通过适当配置页面缓存内存分配器和后备内存分配器来控制和/或消除最后两个分配。数据库连接对象所需的存储空间在一定程度上取决于数据库文件的文件名长度,但在 32 位系统上很少超过 2KB。(由于指针的大小增加,在 64 位系统上需要更多空间。)每个解析器对象使用大约 1.6KB 的内存。因此,可以轻松控制上面的元素 3 到 6,以将最大内存分配大小保持在 2KB 以下。
如果应用程序旨在管理小块数据,则数据库不应包含任何大字符串或 BLOB,因此上面的元素 1 不应成为一个因素。如果数据库确实包含大字符串或 BLOB,则应使用 增量 BLOB I/O读取它们,并且不应通过增量 BLOB I/ O以外的任何方式更新包含大字符串或 BLOB 的行。否则, sqlite3_step()例程将需要在某个时刻将整行读入连续内存,这将涉及至少一次大内存分配。
大量内存分配的最终来源是用于保存由编译复杂 SQL 操作产生的准备好的语句的空间。SQLite 开发人员正在进行的工作正在减少此处所需的空间量。但是大型和复杂的查询可能仍然需要几千字节大小的准备好的语句。目前唯一的解决方法是让应用程序将复杂的 SQL 操作分解为两个或多个包含在单独准备好的语句中的更小和更简单的操作。
综合考虑,应用程序通常应该能够将其最大内存分配大小保持在 2K 或 4K 以下。这给出了 2 或 3 的 log 2 ( n ) 值。这会将N限制在M的 2 到 2.5 倍之间。
应用程序所需的最大通用内存量取决于以下因素:应用程序使用的同时打开 的数据库连接和准备语句对象的数量,以及准备语句的复杂性。对于任何给定的应用程序,这些因素通常是固定的,可以使用SQLITE_STATUS_MEMORY_USED通过实验确定。一个典型的应用程序可能只使用大约 40KB 的通用内存。这给出了大约 100KB的N值。
4.2. 延性破坏
如果 SQLite 中的内存分配子系统配置为无故障运行,但实际内存使用量超过Robson 证明设置的设计限制,SQLite 通常会继续正常运行。页缓存内存分配器 和后备内存分配器自动故障转移到memsys5通用内存分配器。通常情况下,即使M和/或n超过Robson 证明强加的限制, memsys5内存分配器将继续运行而不会产生碎片。罗布森证明表明在这种情况下内存分配有可能崩溃并失败,但这种失败需要一个特别卑鄙的分配和释放序列——SQLite 从未观察到遵循的序列。所以在实践中,通常情况下 Robson 施加的限制可以超出相当大的范围而不会产生不良影响。
然而,应用程序开发人员被告知要监视内存分配子系统的状态,并在内存使用量接近或超过 Robson 限制时发出警报。这样,该应用程序将在故障发生前为操作员提供充分的警告。SQLite的内存统计接口为应用程序提供了完成此任务的监视部分所需的所有机制。
5.内存接口的稳定性
更新:从 SQLite 版本 3.7.0 (2010-07-21) 开始,所有 SQLite 内存分配接口都被认为是稳定的,并将在未来的版本中得到支持。