SQLite中的动态内存分配(五)

返回:SQLite---系列文章目录

上一篇:SQLite中的原子提交(四)

下一篇:自己编译SQLite或将SQLite移植到新的操作系统(六)

​概述

SQLite使用动态内存分配来获得 用于存储各种对象的内存 (例如:数据库连接预准备语句)并构建 数据库文件的内存缓存,用于保存查询结果。 在制作动态内存分配子系统方面付出了很多努力 SQLite 可靠、可预测、稳健、安全、高效。

本文档概述了 SQLite的。目标受众是正在调整其 使用 SQLite 在苛刻的环境中实现最佳性能。 本文档中的任何内容都不是使用 SQLite 所需的知识。这 SQLite的默认设置和配置在大多数情况下都能很好地工作 应用。但是,本文档中包含的信息可能 对于正在调整 SQLite 以符合特殊 要求或在异常情况下运行。

1. 特点

SQLite 核心及其内存分配子系统提供 以下功能:

  • 对分配失败具有鲁棒性。 如果内存分配失败(也就是说, 如果 malloc() 或 realloc() 返回 NULL) 然后 SQLite 将优雅地恢复。SQLite 将首先尝试 若要从未固定的缓存页中释放内存,请重试分配 请求。 如果做不到这一点,SQLite 将停止什么 它正在执行并将SQLITE_NOMEM错误代码返回到应用程序,否则它将 在没有请求的内存的情况下凑合着过。

  • 没有内存泄漏。 应用程序负责销毁它分配的任何对象。 (例如,应用程序sqlite3_finalize必须在 每个数据库连接上的每个预准备语句sqlite3_close()。但只要 应用程序配合,SQLite永远不会泄漏内存。这是 即使面对内存分配失败或其他系统也是如此 错误。

  • 内存使用限制。 sqlite3_soft_heap_limit64() 机制允许应用程序 设置SQLite努力保持在以下的内存使用限制。SQLite的 将尝试重用其缓存中的内存,而不是分配新的内存 内存,因为它接近软限制。

  • **Zero-malloc 选项。**该应用程序可以选择为 SQLite 提供多个大容量内存缓冲区 在启动时,SQLite 将使用这些提供的缓冲区进行所有 它的内存分配需要,从不调用系统 malloc()或 free()。

  • **应用程序提供的内存分配器。**该应用程序可以为 SQLite 提供指向替代方案的指针 启动时的内存分配器。替代内存分配器 将用于代替系统 malloc() 和 free()。

  • **防止故障和碎片化。**可以对SQLite进行配置,以便受某些使用限制 下面详细介绍了它,它保证永远不会使内存分配失败 或对堆进行碎片化。 此属性对于长时间运行、高可靠性非常重要 内存分配错误可能导致的嵌入式系统 到整个系统故障。

  • **内存使用情况统计信息。**应用程序可以查看它们正在使用的内存量,并检测何时 内存使用量接近或超过设计边界。

  • 与内存调试器配合得很好。 SQLite中的内存分配结构化,因此标准 可以使用第三方内存调试器(例如 DMallocValgrind)来验证是否正确 内存分配行为。

  • **对分配器的最小调用。**系统 malloc()和 free()实现效率低下 在许多系统上。SQLite致力于减少整体处理时间 通过尽量减少其对 malloc()和 free()的使用。

  • 开放获取。 可插拔的SQLite扩展甚至应用程序本身都可以 访问相同的底层内存分配 SQLite 通过 sqlite3_malloc()、sqlite3_realloc()sqlite3_free() 接口使用的例程。

2. 测试

SQLite源代码树中的大多数代码都专门用于测试和验证。可靠性对SQLite很重要。 测试基础设施的任务之一是确保 SQLite不会滥用动态分配的内存,即SQLite 不会泄漏内存,并且 SQLite 响应 正确到动态内存分配失败。

测试基础结构验证 SQLite 不会滥用 使用专门检测的动态分配内存 内存分配器。已启用检测的内存分配器 在编译时使用 SQLITE_MEMDEBUG 选项。仪器化 内存分配器比默认内存分配器慢得多,并且 因此,不建议在生产中使用。但是当 在测试期间启用, 检测内存分配器执行以下检查:

  • **边界检查。**检测的内存分配器将哨兵值放在两端 以验证 SQLite 中没有任何内容写入 超出分配范围。

  • **释放后使用内存。**当每个内存块被释放时,每个字节都会被 无意义的位模式。这有助于确保没有内存 被释放后使用。

  • **释放未从 malloc 获得的内存。**来自检测内存分配器的每个内存分配都包含 用于验证释放的每个分配是否都到位的哨兵 来自先前的 malloc。

  • **未初始化的内存。**检测的内存分配器初始化每个内存分配 到无意义的位模式,以帮助确保用户不 关于分配内存内容的假设。

无论检测的内存分配器是否 使用时,SQLite 会跟踪当前签出的内存量。 有数百个测试脚本用于测试 SQLite。在 在每个脚本的末尾,所有对象都被销毁,并对 确保所有内存都已释放。记忆就是这样 检测到泄漏。请注意,内存泄漏检测在以下位置生效 在测试构建和生产构建期间的所有时间。每当 其中一位开发人员运行任何单独的测试脚本,内存泄漏 检测处于活动状态。因此,在开发过程中确实会出现内存泄漏 被快速检测和修复。

SQLite 对内存不足 (OOM) 错误的响应使用 可以模拟内存故障的专用内存分配器覆盖。 覆盖层是插入内存分配器之间的一层 以及 SQLite 的其余部分。覆盖层通过大多数内存分配 请求直接传递到底层分配器,并将 结果返回给请求者。但叠加层可以设置为 导致第 N 个内存分配失败。要运行 OOM 测试,覆盖 首先设置为在第一次分配尝试时失败。然后进行一些测试 运行脚本并验证是否正确捕获了分配 并处理。然后,叠加层设置为在第二个失败 分配,然后重复测试。故障点继续推进 一次分配一个,直到整个测试过程运行到 完成而不出现内存分配错误。这整个 测试序列运行两次。在第一次传递时, overlay 设置为仅第 N 个分配失败。在第二遍时, 叠加设置为第 N 次和所有后续分配失败。

请注意,内存泄漏检测逻辑甚至继续工作 使用 OOM 覆盖时。这将验证 SQLite 即使遇到内存分配错误,也不会泄漏内存。 另请注意,OOM 覆盖可以与任何底层内存一起使用 分配器,包括用于检查的检测内存分配器 用于内存分配误用。通过这种方式,可以验证 OOM 错误不会引起其他类型的内存使用错误。

最后,我们观察到检测的内存分配器和 内存泄漏检测器既适用于整个 SQLite 测试套件,又适用于整个 SQLite 测试套件 TCL 测试套件提供超过 99% 的声明测试覆盖率,并且 TH3 测试线束提供 100% 的分支测试覆盖率,无泄漏泄漏。这是 正确使用动态内存分配的有力证据 在SQLite中无处不在。

2.1. reallocarray()的使用

reallocarray() 接口是最近的一项创新(大约 2014 年) 来自 OpenBSD 社区,这些社区致力于防止 下一个 避免 32 位整数的"Heartbleed"错误 内存分配大小计算上的算术溢出。这 reallocarray() 函数同时具有 unit-size 和 count 参数。 分配足够的内存来容纳每个 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的 源代码经过审计,以确保携带所有大小的计算 out 也使用 64 位有符号整数。SQLite将 拒绝一次性分配超过 2GB 的内存。(共同点 使用,SQLite很少一次分配超过8KB的内存 因此,2GB 的分配限制不是负担。所以 64 位大小参数 为检测溢出提供了大量余量。同样的审计 验证所有大小的计算是否都以 64 位有符号整数的形式完成 还验证是否无法溢出 64 位整数 在计算过程中。

用于确保内存分配大小计算的代码审核 在每次 SQLite 发布之前,SQLite 中的 do not overflow 都会重复。

3. 配置

SQLite中的默认内存分配设置是合适的 适用于大多数应用。但是,具有不寻常或特别的应用程序 严格的要求可能希望将配置调整为更接近 使SQLite符合他们的需求。 编译时和启动时配置选项都可用。

3.1. 替代的低级内存分配器

SQLite源代码包括几种不同的内存分配 可以在编译时或在有限范围内选择的模块 在开始时间。

3.1.1. 默认内存分配器

默认情况下,SQLite 使用 malloc()、realloc() 和 free() 例程 从标准 C 库中获取内存分配需求。这些例程 被一个薄包装器包围,该包装器还提供"memsize()"函数 这将返回现有分配的大小。memsize() 函数 需要准确计算未完成的字节数 记忆;memsize() 确定要从未完成的字节中删除多少字节 释放分配时计数。默认分配器实现 memsize() 通过始终在每个 malloc() 请求上分配 8 个额外的字节和 将分配的大小存储在该 8 字节标头中。

对于大多数应用程序,建议使用默认内存分配器。 如果您没有迫切的使用替代内存的需求 分配器,然后使用默认值。

3.1.2. 调试内存分配器

如果使用 SQLITE_MEMDEBUG compile-time 选项编译 SQLite, 然后围绕系统 malloc()、realloc()、 和 free()。 繁重的包装器分配了大约 100 字节的额外空间 每次分配。额外的空间用于放置哨兵值 在分配的两端返回到 SQLite 核心。当一个 分配被释放, 检查这些哨兵以确保 SQLite 核心没有溢出 任一方向的缓冲区。当系统库为 GLIBC 时, heavy wrapper 还利用 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 编译时选项,则为 heap。在 此外,当进行分配或释放时,HeapValidate() 将是 如果 SQLite 是在启用 assert() 和 SQLITE_WIN32_MALLOC_VALIDATE 编译时选项的情况下编译的,则调用。

3.1.4. 零故障内存分配器

当使用 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 的值很重要,因此也很重要 罗布森证明中的尺寸要求。

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

memsys5 使用的算法可以称为"二次方, 第一次适合"。所有内存分配的大小 请求四舍五入为 2 的幂,请求得到满足 通过 pBuf 中足够大的第一个空闲插槽。相邻释放 使用伙伴系统合并分配。如果使用得当, 该算法提供了防止碎片和 细分,如下所述。

3.1.5. 实验记忆分配器

用于零 malloc 内存分配器的名称"memsys5"意味着 确实有几个额外的内存分配器可用 有。默认内存分配器为"memsys1"。调试 内存分配器是"memsys2"。这些已经涵盖。

如果SQLite是用SQLITE_ENABLE_MEMSYS3编译的那么另一个 零 malloc 内存分配器,类似于 memsys5,包含在 源代码树。必须激活 memsys3 分配器,如 memsys5 通过调用 sqlite3_configSQLITE_CONFIG_HEAP,...)。机芯3 使用提供的内存缓冲区作为所有内存分配的源。 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_configSQLITE_CONFIG_MALLOC,...)。

3.1.8. No-op 内存分配器存根

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

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

3.2. 页面缓存内存

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

SQLite可以配置为从以下位置进行页面缓存内存分配 固定大小的单独且独特的内存池 插槽。这可能有两个优点:

  • 由于分配的大小都相同,因此内存分配器可以 操作速度更快。分配器无需费心进行合并 相邻的空闲插槽或搜索插槽 大小合适。所有未分配的内存插槽都可以存储在 链表。分配包括从 列表。解除分配只是在列表的开头添加一个条目。

  • 对于单个分配大小,Robson 证明中的 n 参数为 1,分配器所需的总内存空间为 1 (N ) 正好等于使用的最大内存 (M)。 无需额外的内存来覆盖分段开销,因此 降低内存需求。这对于 页面缓存内存,因为页面缓存构成了最大的组件 SQLite的内存需求。

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

复制代码
sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);

pBuf 参数是指向连续字节范围的指针,该字节 SQLite 将用于页面缓存内存分配。缓冲区必须是 大小至少为 sz*N 字节。"sz"参数 是每个页面缓存分配的大小。N 是最大值 可用分配的数量。

如果 SQLite 需要大于"sz"字节的页面缓存条目或 如果它需要超过 N 个条目,则回退到使用 通用内存分配器。

3.3. Lookaside 内存分配器

SQLite数据库连接使许多 小而短的内存分配。 这最常发生在使用 sqlite3_prepare_v2() 编译 SQL 语句时,但在使用 sqlite3_step() 运行准备好的语句时,这种情况的程度也较小。这些小内存 分配用于保存表的名称等内容 和列、解析树节点、单个查询结果值、 和 B-Tree 光标对象。因此,有 对 malloc() 和 free() 的大量调用 - malloc() 和 free() 最终使用了分配的 CPU 时间的很大一部分 到 SQLite。

SQLite 版本 3.6.1 (2008-08-06) 引入了 Lookaside 内存分配器 帮助减少内存分配负载。在后备分配器中, 每个数据库连接都预分配一个大块内存 (通常在 60 到 120 KB 的范围内)并划分该块 最多成小的固定大小的"插槽",每个插槽大约100到1000字节。这 成为 Lookaside 内存池。此后,内存分配 与数据库连接相关联,并且不会太大 满意地使用其中一个后备池插槽,而不是通过调用 通用内存分配器。更大的分配继续 使用通用内存分配器,以及发生的分配 当后备池插槽全部签出时。 但在许多情况下,内存 拨款足够少,而且很少有未完成的 可以从后台满足新的内存请求 池。

由于后备分配的大小始终相同,因此分配 解除分配算法非常快。没有 需要合并相邻的空闲插槽或搜索插槽 特定大小。每个数据库连接都维护一个单一链接 未使用的插槽列表。分配请求只需拉取第一个 元素。解除分配只是将元素推回 列表的前面。 此外,假定每个数据库连接已经是 在单个线程中运行(中已经有互斥锁 地方来强制执行此操作),因此不需要额外的互斥 序列化对 Lookaside 插槽空闲列表的访问。 因此,后备内存 分配和解除分配非常快。在速度测试中 Linux 和 Mac OS X 工作站,SQLite 表现出整体性能 改进高达 10% 和 15%,具体取决于工作负载如何 并配置了 lookaway。

后备内存池的大小具有全局默认值 但也可以逐个连接进行配置。 要更改后备内存池的默认大小,请执行以下操作: 编译时,使用 -DSQLITE_DEFAULT_LOOKASIDE=SZ,N 选项。 要更改后备内存池的默认大小,请执行以下操作: start-time,使用 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"参数是指向内存空间的指针,该指针将是 用于 Lookaside 内存池。如果 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 字节)。像这样将池一分为二 允许 Lookaside 更频繁地覆盖内存分配 同时减少每个数据库连接堆的使用 从 120KB 降至 48KB。

配置继续使用SQLITE_DBCONFIG_LOOKASIDE或 SQLITE_CONFIG_LOOKASIDE配置选项,如上所述, 使用参数"sz"和"cnt"。用于 Lookaside 仍然是 sz*cnt 字节。但是空间是分配的 在小插槽 lookaside 和大槽 lookaside 之间,带有 优先考虑小插槽外观。总数 插槽通常会超过"CNT",因为"SZ"通常很多 大于 128 字节的小插槽大小。

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

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

3.4. 内存状态

默认情况下,SQLite 会保留有关其内存使用情况的统计信息。这些 统计数据有助于确定内存量 应用程序确实需要。统计数据也可用于 高可靠性系统确定 如果内存使用量接近或超过限制 [的 Robson 证明](#的 Robson 证明),因此内存分配子系统为 容易发生故障。

大多数内存统计信息都是全局的,因此跟踪 必须使用互斥锁序列化统计信息。统计数据被翻转 默认情况下处于打开状态,但存在禁用它们的选项。通过禁用 记忆统计, SQLite 避免在每个内存分配上输入和保留互斥锁 和解除分配。在以下系统上,这种节省可能很明显 互斥操作成本高昂。要禁用内存统计信息,请 在启动时使用以下接口:

复制代码
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);

"onoff"参数为 true 以启用内存跟踪 statistics 和 false 禁用统计信息跟踪。

假设启用了统计信息,则可以使用以下例程 要访问它们,请执行以下操作:

复制代码
sqlite3_status(verb, &current, &highwater, resetflag);

"verb"参数确定访问的统计信息。 定义了各种动词。这 随着 sqlite3_status() 接口的成熟,列表预计会增长。 所选参数的当前值写入整数 "当前"和最高历史价值 写入整数"highwater"。如果 resetflag 为 true,则 通话后,高水位线将重置为当前值 返回。

使用不同的接口来查找与 单一数据库连接

复制代码
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-08-06)开始, 软堆限制仅适用于 通用内存分配器。软堆限制不知道 关于或与之互动 PageCache 内存分配器Lookaside 内存分配器。 此缺陷可能会在将来的版本中得到解决。

4. 针对内存分配失败的数学保证

动态内存分配问题,特别是 内存分配器故障问题,已通过以下方式进行研究 J. M. Robson和结果发表为:

JM罗布森。"有关动态的某些函数的边界 存储分配"。 协会杂志 Computing Machinery,第 21 卷,第 8 期,1974 年 7 月, 第491-499页。

让我们使用以下表示法(类似于但不完全相同 罗布森的符号):

|-------|------------------------------------------------|
| N | 内存分配系统所需的原始内存量 为了保证内存分配不会失败。 |
| M | 应用程序签出的最大内存量 在任何时间点。 |
| n | 最大内存分配与最小内存分配的比率。我们假设 每个内存分配大小都是最小内存的整数倍 分配大小。 |

罗布森证明了以下结果:

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

通俗地说,罗布森证明表明,为了保证 无故障操作,任何内存分配器都必须使用内存池 大小 N 超过有史以来的最大内存量 将 M 用一个乘数,该乘数取决于 n , 最大与最小分配大小的比率。在其他 字,除非所有内存分配的大小完全相同 (n=1) 则系统需要访问比实际更多的内存 一次性使用。此外,我们看到盈余的数量 所需的内存随着最大与最小之比的快速增长而迅速增长 分配增加,因此有强烈的动机保留所有 分配尽可能接近相同的大小。

罗布森的证明是建设性的。 他提供了一种用于计算分配序列的算法 以及由于以下原因导致分配失败的解除分配操作 内存碎片(如果可用) 内存多达一个字节 小于 N 。 而且,Robson 展示了一个 2 次幂的首次拟合内存分配器 (例如由 memsys5 实现)永远不会使内存分配失败 前提是可用内存为 N 个字节或更多字节。

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

4.1. 计算和控制参数 Mn

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

对于 memsys5 以外的分配器, 所有内存分配的大小都相同。因此,n =1 因此 N =M。换句话说,内存池需要 不大于任何给定时刻使用的最大内存量。

页面缓存内存的使用在以下情况下更难控制 SQLite 版本 3.6.1,但计划在随后的 该版本将使控制页面缓存内存变得更加容易。 在引入这些新机制之前,唯一的办法 要控制 Pagecache,内存将使用 cache_size 编译指示

安全关键型应用程序通常需要修改 默认 Lookaside 内存配置,以便当初始 Lookaside 内存缓冲区在 sqlite3_open() 期间分配 生成的内存分配不会太大,以至于强制 n 参数过大。为了控制 n, 最好尽量将最大内存分配保持在 2 或 4 以下 千 字节。因此,后台的合理默认设置 内存分配器可能是以下任一项:

cpp 复制代码
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 */

另一种方法是最初禁用后备内存 分配器:

cpp 复制代码
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);

然后让应用程序维护一个单独的池 Lookaside 内存缓冲区,它可以在创建数据库连接时将其分发给数据库连接。在一般情况下,应用程序只会 具有单个数据库连接,因此后备内存池 可以由单个大缓冲区组成。

cpp 复制代码
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. 为新数据库连接分配的备用缓冲区。

最后两个分配可以通过以下方式控制和/或消除 配置 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 以下。这 给出对数2(n ) 的值为 2 或 3。这将 将 N 限制为 M 的 2 到 2.5 倍。

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

4.2. 延展性失效

如果配置了 SQLite 中的内存分配子系统 用于无击穿操作,但实际内存使用量超过 由罗布森证明设定的设计极限,SQLite通常会继续 才能正常运行。 pagecache 内存分配器后备内存分配器自动故障转移 到 Memsys5 通用内存分配器。它通常是 memsys5 内存分配器将继续运行的情况 即使 M 和/或 n 超过限值也不会碎裂 由罗布森证明强加。罗布森证明表明它是 内存分配可能会中断并在此过程中失败 情况,但这样的失败需要特别 卑鄙的分配和解除分配序列 - 一个序列 从未观察到 SQLite 跟随。所以在实践中通常是 罗布森施加的限制可以超出 相当大的利润,没有不良影响。

尽管如此,应用程序开发人员仍应进行监控 内存分配子系统的状态,并在以下情况下发出警报 内存使用量接近或超过 Robson 限制。这样, 该应用将为操作人员提供丰富的预警井 在失败之前。 SQLite的内存统计接口为应用程序提供了 完成监控部分所需的所有机制 这个任务。

5. 内存接口的稳定性

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

相关推荐
Yz987627 分钟前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发
苏-言39 分钟前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
Ljw...1 小时前
索引(MySQL)
数据库·mysql·索引
菠萝咕噜肉i1 小时前
超详细:Redis分布式锁
数据库·redis·分布式·缓存·分布式锁
长风清留扬1 小时前
一篇文章了解何为 “大数据治理“ 理论与实践
大数据·数据库·面试·数据治理
OpsEye1 小时前
MySQL 8.0.40版本自动升级异常的预警提示
数据库·mysql·数据库升级
Ljw...1 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
远歌已逝4 小时前
维护在线重做日志(二)
数据库·oracle
qq_433099405 小时前
Ubuntu20.04从零安装IsaacSim/IsaacLab
数据库
Dlwyz5 小时前
redis-击穿、穿透、雪崩
数据库·redis·缓存