SQLite剖析:动态内存分配

2022-03-16 00:00:00 应用程序 内存 分配 分配器 缺省

SQLite通过动态内存分配来获取各种对象(例如数据库连接和SQL预处理语句)所需内存、建立数据库文件的内存Cache、以及保存查询结果。我们做了很多努力来让SQLite的动态内存分配子系统可靠、可预测、健壮并且高效。本文概述SQLite的动态内存分配,软件开发人员在使用SQLite时可以据此获得佳性能。

1、特性

SQLite内核和它的内存分配子系统提供以下特性:

(1)对内存分配失败的健壮处理。如果一个内存分配请求失败(即malloc()或realloc()返回NULL),SQLite将释放未关联的缓存页,然后重新进行分配请求。如果失败,SQLite返回SQLITE_NOMEM给应用程序。

(2)无内存泄漏。应用程序负责销毁已分配的任何对象。例如应用程序必须使用sqlite3_finalize()结束每个预处理SQL语句,使用sqlite3_close关闭每个数据库连接。只要应用程序配合,即使在内存分配失败或系统出错的情况下SQLite也绝不会泄漏内存。

(3)内存使用限制。sqlite3_soft_heap_limit64()机制可以让应用程序设置SQLite的内存使用限制。SQLite会从缓存中重用内存,而不是分配新的内存,以满足设置的限制。

(4)零分配选项。应用程序可以在启动时给SQLite提供几个大块内存的缓冲区,SQLite将用这些缓冲区作为它所有内存分配的需要,不再调用系统的malloc()和free()。

(5)应用程序提供内存分配器。应用程序在启动时可以给SQLite提供可选的内存分配器,以代替系统的malloc()和free()。

(6)对防止内存分配失败或堆内存出现碎片提供形式化的保证。SQLite能配置成保证不会出现内存分配失败或内存碎片。这个特性对长期运行、高可靠性的嵌入式系统至关重要,在这样的系统上一个内存分配错误可能会导致整个系统失效。

(7)内存使用统计。应用程序可以统计SQLite使用了多少内存,可以检测内存使用是否接近或超过设计限制。

(8)尽量少调用分配器。系统的malloc()和free()实现在很多系统上是低效的。SQLite通过尽量少地使用malloc()和free()来减少整个处理时间。

(9)开放式存取。可插拨的SQLite扩展模块或应用程序自己可以通过sqlite3_malloc(), sqlite3_realloc()和sqlite3_free()接口来访问SQLite使用的底层内存分配器。2、测试

测试基础设施通过使用一个特殊的检测内存分配器来验证SQLite没有错误地使用动态分配的内存。检测内存分配器通过编译时使用SQLITE_MEMDEBUG选项来激活,它比缺省的内存分配器更慢,因此不建议在产品中使用它。但是在测试时激活它,可以做以下检查:

(1)边界检查。检测内存分配器在每个内存分配的末尾放置哨兵值,以验证SQLite没有任何例程写出了分配的边界。

(2)释放后的内存使用。当每块内存被释放时,每个字节会填充一些无用的位模式。这可以确保内存在释放后绝不会留下曾经使用过的痕迹。

(3)从非malloc获取的内存的释放。每个来自于检测内存分配器的内存分配都含有一个哨兵值,以验证每个分配的释放来自于先前的malloc。

(4)未初始化的内存。检测内存分配器用一些无用的位模式初始化每块内存的分配,以确保用户不能对所分配内存的内容做任何假设。

无论是否使用检测内存分配器,SQLite都会跟踪当前已取出多少内存。有数百个测试脚本用于测试SQLite。在每个脚本的末尾,所有的对象被销毁,并有一段测试保证所有内存已释放。这就是检测内存泄漏的方法。注意内存泄漏的检测在任何时候都是大批量的进行,包括测试构建和产品构建过程中。每当一个开发者运行任意单个测试脚本,内存泄漏检测就会被激活。因此开发过程中引入的内存泄漏能够迅速地被检测到并修复。

SQLite使用一个特殊的、能模拟内存失败的内存分配器覆盖层来测试内存不足(OOM)的错误。覆盖层被插入到内存分配器和SQLite内核之间,它直接传递内存分配请求到底层的分配器,并把结果传回到请求者。覆盖层可设置让第N次内存分配失败。为了运行OOM测试,覆盖层首先设置次分配失败,并运行一些测试脚本以验证分配被正确捕捉和处理。然后覆盖层设置第二次分配失败并重复运行测试。失败点继续一次通知一个分配,直到整个测试过程完成并且没有出现内存分配错误。这样的完整测试流程运行两遍,遍覆盖层只设置第N次分配失败,第二遍覆盖层设置第N次和后续的分配失败。注意即使遇到OOM覆盖层,内存泄漏检测逻辑也继续工作,以验证SQLite即使遇到内存分配错误也不会泄漏内存。OOM覆盖层能与任何底层内存分配器一起工作,包括检测内存分配器(这时可验证OOM错误不会引入其他的内存使用错误)。

检测内存分配器和内存泄漏检测逻辑工作在整个SQLite测试套件上。其中TCL测试套件提供99%的语句测试覆盖,TH3测试提供的分支测试覆盖以保证无内存泄漏。因此SQLite的动态内存分配器可以在各种场合下正确地工作。

3、配置

3.1 可选的底层内存分配器

SQLite源代码包含几种不同的内存分配器模块,可以在编译时选择,或在启动时作为一个有限的扩展。

(1)缺省内存分配器

缺省情况下,SQLite使用C标准库中的malloc(), realloc()和free()例程来分配内存。实现中还对这些例程做一层薄的包装以提供一个memsize()函数返回一个现存分配的大小。memsize()也能地跟踪未归还内存的字节数,它能确定当一个分配被释放时有多少字节从未归还内存中移除。缺省内存分配器中的memsize()实现是在每个malloc()请求上多分配额外8个字节作为头部,并把分配的大小保存到这个8字节的头部。

在大多数应用程序中我们都建议使用缺省内存分配器,如果没有使用可选内存分配器的强制性需求,使用缺省的即可。

(2)调试内存分配器

如果SQLite使用SQLITE_MEMDEBUG编译时选项来编译,则使用一个不同的、对系统malloc(), realloc()和free()进行重型包装的内存分配器。重型包装器对每个分配请求多分配100字节的额外空间,用来在分配的末尾放置哨兵值。当一个分配被释放时,检查这些哨兵值以确保SQLite内核没有超出缓冲区的两端。当系统库来自GLIBC时,重型包装器也会使用GNU backtrace()函数来检查栈,并记录malloc()调用的祖先函数。当运行测试套件时,重型包装器还会记录当前测试用例的名称。这两个特性对跟踪内存泄漏是非常有用的。

重型包装器只用于SQLite的测试、分析和调试。它有显著的性能和内存开销,一般不用在终产品中。

(3)零分配内存分配器

当SQLite使用SQLITE_ENABLE_MEMSYS5选项编译时,会包含一个不使用malloc()的可选内存分配器。SQLite开发者称它为"memsys5"。即使被包含在版本中,缺省情况下memsys5也是被禁用的。应用程序必须在启动时调用下面的SQLite接口:

sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);

其中pBuf指向一个大的连续的内存块,SQLite使用它满足所有的内存分配需要。pBuf也可以指向一个静态数组或一段从其他应用程序特定机制获取的内存。szBuf为pBuf内存的字节数。mnReq为一次分配的小字节数。任何对sqlite3_malloc(N)的调用,当N小于mnReq时会向上舍入到mnReq。nmReq必须是2的幂。稍后我们会看到mnReq参数在Robson证明中对于减小n值和小内存需求是至关重要的。

memsys5分配器被设计用于嵌入式系统中,当然没有任何限制规定不得用于工作站上。szBuf通常在几百KB到几十MB之间,取决于系统需要和内存预算。memsys5使用的算法可以概括为“2的幂,命中”。所有内存分配请求的大小都舍入到2的幂,分配时使用pBuf中个足够大的空闲内存块。使用伙伴系统来合并相邻的空闲内存块。如果使用得当,本算法可以为避免内存碎片和内在崩溃提供数学保证(参考下面描述)。

(4)实验性内存分配器

从名称"memsys5"可以看出,可能还有其他可选的内存分配器,的确如此。缺省内存分配器称为"memsys1",调试内存分配器称为"memsys2"。如果SQLite使用SQLITE_ENABLE_MEMSYS3编译,则另外一个零分配内存分配器memsys3包含到版本中。它与memsys5类似,必须调用sqlite3_config(SQLITE_CONFIG_HEAP,...)来激活。Memsys3使用内存缓冲区来满足所有的内存分配需要。它与memsys5的区别是使用不同的内存分配算法,这个算法在实践中看起来工作得很好,但不能为避免内存碎片和内存崩溃提供数学保证。Memsys3是memsys5的前任,SQLite开发者相信memsys5比memsys3更好,所有需要零分配内存分配器的应用程序应该使用memsys5而不是memsys3。Memsys3在将来的SQLite版本中可能会移除。

在SQLite 3.6.1中,memsys4的代码还在源码树中,从3.6.5开始已移除。Memsys4尝试用mmap()获取内存,使用madvise()释放未使用的页给操作系统,以便它们能被其他进程使用。现在memsys4已被抛弃。Memsys6使用系统malloc()和free()获取需要的内存,它是一个聚合器。Memsys6只调用malloc()获取大容量的内存,然后把这些大容量的内存分割成多块小内存,以满足SQLite内核的需要。Memsys6用在malloc()实现低效的系统中。memsys6后面的思想是减小系统malloc()的调用次数。Memsys6只有使用SQLITE_ENABLE_MEMSYS6编译SQLite时才可用,并且在启动时需要调用:

sqlite3_config(SQLITE_CONFIG_CHUNKALLOC);

Memsys6在SQLite 3.6.1中添加,它是非常实验性的,从3.6.5开始也已移除。其他实验性的内存分配器可能会在将来的SQLite版本中加入,名称可能是memsys7, memsys8等。

(5)应用程序定义的内存分配器

应用程序可以在启动时提供自己的内存分配器给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中的方法就需要自己来关注串行化。

(6)内存分配器覆盖层

应用程序可在SQLite内核和底层内存分配器之间插入覆盖层。例如,OOM测试逻辑通过使用覆盖层可以模拟内存分配失败的情形。覆盖层使用以下接口来创建:

sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem);

该接口获取现存内存分配器的指针,并保存它以用来进行实际的内存分配。然后通过使用类似的sqlite3_config(SQLITE_CONFIG_MALLOC,...)把覆盖层被插入到现存内存分配器的地方。

(7)空操作内存分配器

如果SQLite使用SQLITE_ZERO_MALLOC选项来编译,缺省内存分配器将会被忽略,由一个桩内存分配器代替,它不会分配任何内存。任何对桩分配器的调用将返回没有可用内存的报告。这种空操作内存分配器只是作为一个占位符,以便SQLite能链接一些不使用malloc(), free()或realloc()的自定义内存分配器。带SQLITE_ZERO_MALLOC选项编译的应用程序在使用SQLite之前,需要使用sqlite3_config(),结合SQLITE_CONFIG_MALLOC或SQLITE_CONFIG_HEAP来指定新的可选内存分配器。

3.2 临时内存

SQLite偶尔需要一大块的“临时”内存来执行一些临时的计算。例如,当重新平衡一棵B-Tree时,需要使用临时内存。这些临时内存通常在10KB左右,用于一个单一的、短暂的函数调用。在早期的SQLite版本中,临时内存从处理器栈中获取,这在拥有大容量栈的工作站上可以很好地工作。但在只有小容量处理器栈(通常4K或8K)的嵌入式系统中,从栈中申请一个大的缓冲区会引起问题。因此,SQLite被修改为从堆上获取临时内存。临时内存分配器的设置方法如下:

sqlite3_config(SQLITE_CONFIG_SCRATCH, pBuf, sz, N);

其中pBuf指向一段连续的内存,SQLite用它来进行临时内存分配。这段连续内存至少要有sz*N字节的大小,"sz"参数是每次临时内存分配的大字节数,N是同时进行临时内存分配的大次数。"sz"参数值应该为大数据库页面的6倍左右,N应该为系统中运行线程数量的2倍左右。没有线程会一次请求超过两次的临时内存分配,因此N的值能确保足够的临时内存分配。如果临时内存设置没有提供足够的内存,SQLite将退回到使用正常的内存分配器来进行临时内存分配。缺省的设置是sz=0, N=0,表示使用正常的内存分配器作为缺省行为。

3.3 页缓存内存

在很多应用程序中,SQLite的数据库页缓存子系统会更频繁地使用动态内存分配,甚至频繁程度超过其他子系统的10倍。SQLite可以配置成从一个独立的、槽大小固定的内存池中进行页缓存内存的分配。这有两个优点:

(1)因为所有的分配是同样的大小,内存分配器可能工作的更快,分配器无需合并相邻空闲槽或查找大小合适的槽。所有未分配的内存槽存储在一个链表中,分配时摘下链表中的个内存槽,释放时直接把内存槽添加到链表的头部。

(2)由于只有一种分配大小,Robson证明中的n参数为1,并且分配器需要的整个内存空间(N)恰好等于使用的大内存(M)。没有额外的内存碎片开销,因此减少了内存需求量。这对页缓存内存特别重要,因为SQLite的大部分内存需求都来自于页缓存。

页缓存内存分配器缺省是禁用了,应用程序可在启动时打开它:

sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);

其中pBuf指向一段连续的内存,SQLite用它来进行页缓存内存的分配。这段连续内存至少要有sz*N字节的大小,"sz"参数是每次页缓存内存分配的字节数,N是可以进行分配的大次数。如果SQLite需要超过sz个字节的页缓存内存,或者需要超过N块的分配,则退回到使用通常的内存分配器。

3.4 后备内存分配器

SQLite数据库连接会进行许多小的、短期的内存分配。当用sqlite3_prepare_v2()编译SQL语句时这种情况常见。这些小的内存分配用来存储诸如表名和列名、解析树结点、单独的查询结果、和B-Tree游标对象。这会导致频繁地调用malloc()和free(),用掉分配给SQLite的大部分CPU时间片。

SQLite 3.6.1引入后备内存分配器来帮助减少内存分配负载。在后备分配器中,每个数据库连接预先分配一段大块的内存(通常50到100KB),然后把这个内存块分割成50到200字节固定大小的多个内存槽,从而变成后备内存池。数据库连接的小内存分配使用后备池中的一个槽,而大内存分配继续使用通用内存分配器。当后备池全部用完时也会转用通用内存分配器,不过在很多情况下后备池对小内存分配的使用是足够的。

因为后备内存分配总是同样的大小,因此分配和释放算法速度非常快,无需合并相邻空闲槽或查找大小合适的槽。每个数据库连接维护一个空闲槽的单链表。分配时直接摘下链表中的个内存槽,释放时直接把内存槽添加到链表的头部。此外,每个数据库连接已经运行在单个线程下(已经放置了互斥锁来强制做到这一点),无需额外的互斥锁来串行化后备槽列表的访问。因此,后备内存分配的分配和释放是非常快速的,在Linux和Max OS X工作站上进行速度测试,显示根据配置的后备内存负载,SQLite整个性能可以提高10%到15%。

后备内存池的大小有一个全局的缺省值,但可以配置成不同的值。只要在启动时使用以下接口:

sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);

其中sz为每个后备槽的字节数,缺省为100字节。cnt是每个数据库连接的后备内存槽总个数,缺省值为500个槽。显然每个数据库连接的后备内存大小为sz*cnt字节,缺省为50KB。注意这些缺省值针对SQLite 3.6.1,在将来版本中有可能会改变。

一个单独数据库连接的后备池可以更改,使用下面的调用:

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.5 内存状态

缺省情况下,SQLite统计它的内存使用情况。这些统计可以确定一个应用程序真正需要多少内存,也可以用在高可靠系统中以确定内存使用是否即将关闭或超过Robson证明的限制,从而导致内存分配子系统崩溃。许多内存统计是全局的,因此必须要互斥锁来串行化。统计缺省是打开的,但是可以禁用它,这样在内存分配或释放是可以避免使用互斥锁,因此节省开销。调用下面的接口:

sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);

"onoff"参数为true时激活内存统计跟踪,为false时禁用内存统计跟踪。如果统计是激活的,可以使用下面的例程来访问它们:

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

"verb"参数确定访问哪个统计信息,有各种各样的verb动词定义,可参考
http://sqlite.org/c3ref/c_status_malloc_count.html#sqlitestatusmemoryused。当前选择的值会写入到current整型参数中,历史高值会写入到highwater参数中。如果resetflag为true,则在调用返回时high-water标志会重置为当前选择的值。

对于单个数据库连接的统计,有不同的接口:

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

这个接口功能类似,只不过多一个数据库连接参数,并且返回的是这个连接的内存统计信息,而不是整个SQLite库。sqlite3_db_status()接口当前只识别一个动词
SQLITE_DBSTATUS_LOOKASIDE_USED,在将来可能会识别更多的动词。

每个连接的统计不使用全局变量,因此不需要互斥锁来访问和更新。即使SQLITE_CONFIG_MEMSTATUS关闭,每个连接的内存统计也会继续进行。

3.6 设置内存使用限制

sqlite3_soft_heap_limit64()接口用来设置通用内存分配器可分配的内存堆总量上限。如果分配的内存超过了这个弱的堆限制,SQLite将在继续分配请求之前释放缓存的内存。弱的堆限制机制只在内存统计激活的情况下才工作,并且如果编译时使用
SQLITE_ENABLE_MEMORY_MANAGEMENT,它能获得好的工作性能。

弱的堆限制之所以称为“弱(soft)”的,是因为如果SQLite不能释放足够的辅助内存来满足这个限制,它会继续分配额外的内存,并超过这个限制。这是基于使用额外内存比完全失败更好的理论。在SQLite 3.6.1中,弱的堆限制只能应用在通用内存分配器中,它不能和临时内存分配器、页缓存内存分配器、或后备内存分配器交互。在将来版本中会解决这个问题。

4、防止内存分配失败的数学保证

对于动态内存分配问题、内存分配失败问题,J.M.Robson进行过系统的研究,其结果发表在如下论文中:

J. M. Robson. "Bounds for Some Functions Concerning Dynamic Storage Allocation". Journal of the Association for Computing Machinery, Volume 21, Number 8, July 1974, pages 491-499.

我们使用下面的记号(与Robson的记号类似,但不完全等同):

N: 内存分配系统为了保证不会出现分配失败而需要的原始内存数量。

M: 应用程序曾经在任何时间点取出的大内存数量。

n: 大内存分配与小分配的比值。我们假设每个内存分配大小都是小分配大小的整数倍。

Robson证明了下面的结果:

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

通俗地讲,Robson证明表明,为了防止内存分配失败,任何内存分配器必须使用一个大小为N的内存池,它超过曾经使用的大内存M乘以一个取决于n的倍数。也就是说,除非所有的内存分配都是同样的大小,否则系统需要访问比曾经使用的更大的内存。此外我们看到,需要的剩余内存随着比值n的增加会迅速的增长,因此我们应该保持所有的内存分配尽可能大小相同。

Robson证明是构造性的。他提出一个算法来计算一个分配和释放操作系列由于内存碎片(可用内存大于1字节但小N字节)将导致分配失败。Robson还证明如果可用内存为N或更多的字节,一个“2的幂,命中”的内存分配器绝不会出现内存分配失败。

M和n值是应用程序的属性。如果创建应用程序时M和n值是已知的,或者至少知道上限值;并且如果应用程序使用memsys5内存分配器,通过SQLITE_CONFIG_HEAP提供N字节的可用内存空间,Robson证明在应用程序中不会出现内存分配请求失败。也就是说,应用程序开发者可以选择一个N值,以保证对SQLite任何接口的调用不会返回SQLITE_NOMEM,内存池也不会出现碎片以致于不能满足新的内存分配请求。这在哪些一个软件故障就会导致损坏或关键数据丢失的应用程序中至关重要。

4.1 计算和控制参数M和n

Robson证明可以用在SQLite的以下内存分配器中:

* 通用内存分配器(memsys5)

* 临时内存分配器

* 页缓存内存分配器

* 后备内存分配器

对除memsys5之外的其他内存分配器,所有的内存分配都是同样大小的,因此n=1,N=M。也就是说,内存池无需比任何时刻使用的内存大量还要大。

SQLite保证没有线程同时使用超过两个的临时内存槽,因此,如果应用程序分配线程数量两倍的临时内存槽,并且每个槽足够大,临时内存分配器不会溢出。临时内存分配大小的上限是大页面的6倍,因此很容易保证临时内存分配器不出现分配失败的操作。

在SQLite 3.6.1中,页缓存内存的使用比较难控制,控制页缓存内存的方式是使用cache_size pragma指令。在以后的SQLite版本中会使页缓存内存控制变得更容易。

安全悠关的应用程序通常会修改缺省的后备内存池配置,以致于当sqlite3_open()分配初始的后备内存缓冲区时,由于n参数太大而使得内存分配不够大。为了使n值能够得到控制,大内存分配好是在2或4KB以下。因此,后备内存分配器合理缺省值设置好是以下值之一:

  • 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 */

另外一种方法是在开始时禁用后备内存分配器:

qlite3_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很小,我们可能希望大内存分配大小在可控范围之内。通用内存分配器上的大分配请求可能有这么几个来源:

  • (1)SQL表中包含长字符串或BLOBs的行。
  • (2)编译成大的预处理语句对象的复杂SQL查询。
  • (3)sqlite3_prepare_v2()内部使用的SQL解析对象。
  • (4)数据库连接的存储空间。
  • (5)外流到通用内存分配器的临时内存分配。
  • (6)外流到通用内存分配器的页缓存内存分配。
  • (7)新数据库连接的后备内存分配。

通过适当地配置临时内存分配器、页缓存内存分配器和后备内存分配器,后三种情况可以控制或排除。数据库连接对象需要的存储空间取决于数据库文件名的长度范围,但是在32位系统上很少超过2KB(在64位系统上由于指针大小的增加而需要更多空间)。每个解析对象大概使用1.6KB的内存。因此,上面的情况(3)到(7)可以把大内存分配的大小控制在2KB以下。如果应用程序被设计成用来以小块的形式管理数据,则数据库不会包含任何长的字符串或BLOBs,这样情况(1)就不是一个因素了。如果数据库包含长的字符串或BLOBs,则应该用增量式BLOB I/O来读取它们,其更新也应该使用增量式BLOB I/O方法,而不用其他的方法。否则,sqlite3_step()将不得不在某一时刻读取整个行到连续的内存中,这涉及到至少一次大的内存分配。

内存分配的后一个来源是复杂SQL查询编译成的预处理语句对象。SQLite开发者正在进行的工作是减少这部分的内存空间需求。但是大的复杂SQL查询仍然需要几KB的预处理语句对象。目前的变通方案是把复杂SQL操作分离成多个小的简单的操作,以包含在各个单独的预处理语句对象中。

考虑所有的情况后,应用程序通常可以让大内存分配保持在2K或4K。这使log2(n)的值为2或3,限制N在M的2到2.5倍。

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

4.2 延性破坏

如果SQLite的内存分配子系统被配置成不会有内存分配失败,但实际内存使用超过Robson证明设置的限制,SQLite将继续正常地操作。临时内存分配器、页缓存内存分配器和后备内存分配器自动切换到memsys5通用内存分配器。通常memsys5内存分配器将继续执行分配功能,即使M或n超过设置的限制也不会有内存碎片。Robson证明表明在这种情况下一个内存分配有可能失败,但是这样的失败需要一种特殊的分配和释放顺序,在SQLite中没有这样的分配释放顺序。因此在实践中,Robson限制可以超过而不会出现坏的影响。

然而,应用程序开发者应该记住,要监控内存分配子系统的状态,当内存使用超过限制时发出警报,这样应用程序在失败前可以提供各种警告信息。SQLite的内存统计接口给应用程序提供了完成监控任务的所有机制。

5 内存接口的稳定性

在SQLite 3.6.1中,所有可选内存分配器和机制都是实验性的,还不完全稳定。从SQLite 3.7.0开始,所以这些接口都是稳定的。

来源 https://zhuanlan.zhihu.com/p/472608234

相关文章