Small. Fast. Reliable.
Choose any three.
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位大小参数为检测溢出提供了很大的空间。

在每个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_MEMDEBUG编译SQLite编译时选项,然后在系统malloc(),realloc()和free()周围使用不同的繁重包装器。繁重的包装器每次分配都会分配大约100个字节的额外空间。多余的空间用于将哨兵值放置在返回到SQLite核心的分配的两端。释放分配后,将检查这些标记以确保SQLite内核没有在任何方向上溢出缓冲区。当系统库为GLIBC时,繁重的包装器还利用GNU backtrace()函数检查堆栈并记录malloc()调用的祖先函数。运行SQLite测试套件时,繁重的包装器还会记录当前测试用例的名称。后两个功能对于跟踪测试套件检测到的内存泄漏源很有用。

设置SQLITE_MEMDEBUG时使用的重包装器还可以确保在将分配返回给调用者之前,每个新分配都被无用数据填充。一旦分配是免费的,它就会再次被废话数据所填充。这两个动作有助于确保SQLite核心不会对新分配的内存状态做出假设,并且确保释放内存后不使用内存分配。

SQLITE_MEMDEBUG使用的重包装器仅适用于SQLite的测试,分析和调试。繁重的包装程序具有显着的性能和内存开销,因此可能不应该在生产中使用。

3.1.3。Win32本机内存分配器

如果使用Windows SQLite_WIN32_MALLOC 编译时选项为Windows编译了SQLite ,则在HeapAlloc(),HeapReAlloc()和HeapFree()周围使用不同的瘦包装器。精简包装使用配置的SQLite堆,如果使用SQLITE_WIN32_HEAP_CREATE编译时选项,则它将与默认进程堆不同。此外,在分配或释放分配时,如果在启用assert()和SQLITE_WIN32_MALLOC_VALIDATE编译时选项的情况下编译SQLite,则将调用HeapValidate() 。

3.1.4。零分配内存分配器

当使用SQLITE_ENABLE_MEMSYS5选项编译SQLite时,构建中将包含一个不使用malloc()的备用内存分配器。SQLite开发人员将此替代内存分配器称为“ memsys5”。即使将其包含在构建中,memsys5也默认情况下处于禁用状态。要启用memsys5,应用程序必须在启动时调用以下SQLite接口:

sqlite3_configSQLITE_CONFIG_HEAP,pBuf,szBuf,mnReq);

在上面的调用中,pBuf是指向大量连续内存空间的指针,SQLite将使用该内存空间来满足其所有内存分配需求。pBuf可能指向静态数组,或者它可能是从某些其他特定于应用程序的机制获得的内存。szBuf是一个整数,它是pBuf指向的内存空间的字节数。mnReq是另一个整数,它是分配的最小大小。N小于mnReq的对sqlite3_malloc(N)的任何调用将四舍五入为mnReq。mnReq必须为2的幂。稍后我们将看到mnReq参数对于减小n的值非常重要,因此对于减小Robson证明中的最小内存大小要求也很重要。

memsys5分配器旨在用于嵌入式系统,尽管没有什么可以阻止其在工作站上使用。szBuf通常在几百KB到几十MB之间,具体取决于系统要求和内存预算。

memsys5使用的算法可以称为“二乘幂,优先拟合”。将所有内存分配请求的大小四舍五入为2的幂,并且该请求由pBuf中足够大的第一个空闲插槽满足。使用伙伴系统合并相邻的释放分配。适当使用时,此算法可提供防止碎片和击穿的数学保证,如下文进一步所述

3.1.5。实验内存分配器

用于零malloc内存分配器的名称“ memsys5”表示存在多个可用的内存分配器,并且确实存在。默认的内存分配器是“ memsys1”。调试内存分配器是“ memsys2”。这些已经被覆盖。

如果使用SQLITE_ENABLE_MEMSYS3编译SQLite ,则源树中将包含另一个类似于memsys5的零malloc内存分配器。与memsys5一样,memsys3分配器必须通过调用sqlite3_configSQLITE_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_configSQLITE_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_configSQLITE_CONFIG_GETMALLOC,pOldMem);

接口以获取指向现有内存分配器的指针。现有的分配器由叠加层保存,并用作后备以进行实际的内存分配。然后,使用sqlite3_configSQLITE_CONFIG_MALLOC,...),将 覆盖层插入现有内存分配器的位置,如上所述

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

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

无操作内存分配器本身没有用。它仅作为占位符存在,因此SQLite可以在其标准库中可能没有malloc(),free()或realloc()的系统上链接一个内存分配器。在开始使用SQLite之前,使用SQLITE_ZERO_MALLOC编译的应用程序将需要与SQLITE_CONFIG_MALLOCSQLITE_CONFIG_HEAP一起使用sqlite3_config()来指定新的备用内存分配器。

3.2。页面缓存存储器

在大多数应用程序中,SQLite中的数据库页面缓存子系统使用的动态分配内存要比SQLite的所有其他部分所组合的内存更多。看到数据库页面缓存消耗的内存比其余SQLite的总和多10倍以上,这并不罕见。

可以将SQLite配置为从固定大小的插槽的单独且不同的内存池中进行页面缓存内存分配。这可以具有两个优点:

默认情况下,页面缓存内存分配器是禁用的。应用程序可以在启动时启用它,如下所示:

sqlite3_configSQLITE_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%,具体取决于工作负载的配置方式和后备方式。

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

sqlite3_configSQLITE_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)开始,后备支持两个内存池,每个内存池具有不同的大小插槽。小插槽池使用128字节的插槽,大插槽池使用SQLITE_DBCONFIG_LOOKASIDE指定的任何大小 (默认为1200字节)。像这样将池分成两部分,可以使内存分配更经常地被后备覆盖,同时将每个数据库连接堆的使用量从120KB减少到48KB。

如上所述,配置继续使用带有参数“ sz”和“ cnt”的SQLITE_DBCONFIG_LOOKASIDE或SQLITE_CONFIG_LOOKASIDE配置选项。用于后备的总堆空间仍然是sz * cnt字节。但是,空间是在小时隙后备空间和大时隙后备空间之间分配的,优先考虑小时隙后备空间。插槽的总数通常会超过“ cnt”,因为“ sz”通常比128字节的小插槽大小大得多。

默认的后备配置已从每个1200字节(120KB)的100个插槽更改为每个1200字节(48KB)的40个插槽。该空间最终分配为93个插槽,每个插槽128个字节,每个30个插槽,每个1200字节。因此,可以使用更多后备插槽,但使用的堆空间要少得多。

默认的后备配置,小插槽的大小以及如何在小插槽和大插槽之间分配堆空间的详细信息都可能从一个发行版更改到另一个发行版。

3.4。内存状态

默认情况下,SQLite会保留有关其内存使用情况的统计信息。这些统计信息有助于确定应用程序真正需要多少内存。该统计信息还可用于高可靠性系统中,以确定内存使用情况是否接近或超过Robson证明的限制,因此内存分配子系统易于崩溃。

大多数内存统计信息都是全局的,因此必须使用互斥锁对统计信息进行跟踪。默认情况下,统计信息是打开的,但是存在禁用它们的选项。通过禁用内存统计信息,SQLite避免了在每个内存分配和释放过程中进入和留下互斥量。在互斥操作昂贵的系统上,这种节省是显而易见的。要禁用内存统计信息,请在启动时使用以下界面:

sqlite3_configSQLITE_CONFIG_MEMSTATUS,onoff);

“ onoff”参数为true表示启用内存统计信息跟踪,值为false表示禁用统计信息跟踪。

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

sqlite3_status动词,&current,&highwater,resetflag);

“动词”参数确定要访问的统计信息。有不同的动词定义。随着sqlite3_status()接口的成熟,该列表有望增长。所选参数的当前值被写入整数“ current”,最高历史值被写入整数“ highwater”。如果resetflag为true,则在调用返回后将高水位标记重置为当前值。

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

sqlite3_db_status(db,动词,&current,&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 Robson)。“有关动态存储分配的某些功能的界限”。 计算机协会杂志,1974年7月,第21卷,第8期,第491-499页。

让我们使用以下表示法(与Robson的表示法相似但不相同):

ñ 内存分配系统为保证没有内存分配失败而需要的原始内存量。
中号 应用程序在任何时间点都签出的最大内存量。
ñ 最大内存分配与最小内存分配的比率。我们假设每个内存分配大小都是最小内存分配大小的整数倍。

Robson证明以下结果:

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

通俗地讲,Robson证明表明,为了保证无故障运行,任何内存分配器都必须使用大小为N的内存池,该内存池超过一个乘数的最大使用量M,该乘数取决于n,即最大的比率到最小的分配大小。换句话说,除非所有内存分配的大小都完全相同(n = 1),否则系统需要访问的存储空间将超过一次使用的存储空间。此外,我们看到,随着最大分配与最小分配的比率增加,所需的剩余内存量迅速增长,因此强烈希望将所有分配保持在尽可能近的相同大小。

罗布森的证明是建设性的。他提供了一种算法,用于计算分配和释放操作的序列,如果可用内存比N少一个字节,则会导致由于内存碎片而导致分配失败。而且,罗布森(Robson)显示,如果可用内存为N个或更多字节,则2次幂的第一适配内存分配器(例如由memsys5实现)将永远不会使内存分配失败。

Mn是应用程序的属性。如果以Mn都已知或至少具有已知上限的方式构造应用程序,并且如果该应用程序使用memsys5内存分配器并使用SQLITE_CONFIG_HEAP提供了N字节的可用内存空间, 则罗布森证明没有内存分配请求将永远不会在应用程序内失败。换句话说,应用程序开发人员可以为N选择一个值,该值将确保对任何SQLite接口的调用都不会返回SQLITE_NOMEM。。内存池永远不会变得如此零散,以致无法满足新的内存分配请求。对于软件故障可能导致人身伤害,物理伤害或不可替代数据丢失的应用程序,这是重要的属性。

4.1。计算和控制参数Mn

Robson证明分别适用于SQLite使用的每个内存分配器:

对于除memsys5之外的分配器,所有内存分配大小均相同。因此,Ñ = 1,因此Ñ =中号。换句话说,内存池不必大于任何给定时刻正在使用的最大内存量。

在SQLite 3.6.1版中,页面缓存内存的使用在某种程度上更难控制,尽管计划为后续发行版设计机制,这将使控制页面缓存内存变得更加容易。在引入这些新机制之前,控制pagecache内存的唯一方法是使用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);

后备内存分配器实际上旨在实现性能优化,而不是确保无故障内存分配的方法,因此完全禁用后备内存分配器以进行安全性至关重要的操作并非没有道理。

通用内存分配器是最难管理的内存池,因为它支持大小不同的分配。由于 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至7以将最大存储器分配大小保持在2KB以下。

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

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

考虑所有因素,应用程序通常应能够将其最大内存分配大小保持在2K或4K以下。这给出log 2n)的值为2或3。这会将N限制为M的2到2.5倍之间。

应用程序所需的最大通用内存量由以下因素确定,这些因素包括应用程序使用多少个同时打开的 数据库连接准备好的语句对象,以及准备好的语句的复杂性。对于任何给定的应用程序,这些因素通常是固定的,可以使用SQLITE_STATUS_MEMORY_USED通过实验确定。一个典型的应用程序可能只使用大约40KB的通用内存。N的值约为100KB。

4.2。韧性破坏

如果将SQLite中的内存分配子系统配置为无故障运行,但是实际内存使用量超过了Robson证明所设置的设计限制,则SQLite通常将继续正常运行。页面缓存内存分配器后备内存分配器会自动故障转移到memsys5通用内存分配器。通常情况下,即使M和/或n超过Robson证明施加的限制,memsys5内存分配器也将继续运行而不会产生碎片。在罗布森证明显示了在这种情况下内存分配有可能崩溃并失败,但是这种失败需要特别卑鄙的分配和取消分配序列-从未观察到遵循SQLite的序列。因此,在实践中,通常情况下,可以大幅度超出Robson施加的限制,而不会产生不良影响。

但是,建议应用程序开发人员监视内存分配子系统的状态,并在内存使用率接近或超过Robson限制时发出警报。这样,该应用程序将在故障发生之前为操作员提供丰富的警告。SQLite的内存统计信息接口为应用程序提供了完成此任务的监视部分所需的所有机制。

5.内存接口的稳定性

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