RussianLDP Рейтинг@Mail.ru
WebMoney: 
WMZ Z294115950220 
WMR R409981405661 
WME E134003968233 
Visa 
4274 3200 2453 6495 

Small. Fast. Reliable.
Choose any three.
Динамическое выделение памяти в 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 попытается снова использовать память от своих кэшей вместо того, чтобы ассигновать новую память, когда это приближается к мягкому пределу.

  • Нулевой-malloc выбор. Применение может произвольно предоставить SQLite несколько буферов памяти большого объема при запуске, и SQLite будет тогда использовать обеспеченные буфера для всех его потребностей выделения памяти и никогда не вызывать системные malloc() или free().

  • Поставляемые применением распределители памяти. Приложение может предоставить SQLite указатели на альтернативных распределителей памяти во время запуска. Альтернативный распределитель памяти будет использоваться вместо системных malloc() и free().

  • Доказательство против фрагментации. SQLite может формироваться так, чтобы согласно использованию определенных ограничений, детализированных ниже, это, как гарантируют, никогда не потерпит неудачу при выделении памяти или фрагментирует кучу. Это важно для продолжительной работы встроенных систем высокой надежности, где ошибка распределения памяти могла бы способствовать полному системному отказу.

  • Статистика использования памяти. Запросы видят, сколько памяти они используют и обнаруживают, когда использование памяти приближается или превышает границы дизайна.

  • Работает хорошо с отладчиками памяти. Выделение памяти в SQLite структурировано так, чтобы стандартные сторонние отладчики памяти (такие как dmalloc или valgrind) могли использоваться, чтобы проверить правильное поведение выделения памяти.

  • Минимальные вызовы распределителя. Системные 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. Наложение передает большинство запросов выделения памяти прямо основному распределителю и возвращает назад результаты. Но наложение может в тестовых целях заставить заданное выделение памяти терпеть неудачу.

Обратите внимание на то, что логика обнаружения утечки памяти продолжает работать, даже когда наложение OOM используется. Это проверяет, что SQLite не упускает память, даже когда это сталкивается с ошибками распределения памяти. Отметьте также, что наложение OOM может работать с любым основным распределителем памяти, включая инструментованный распределитель, который проверяет на неправильное употребление выделения памяти. Таким образом, проверяется, что ошибки OOM не вызывают другие виды ошибок использования памяти.

Наконец, мы замечаем, что инструментованный распределитель памяти и датчик утечки памяти работают по всему набору тестов SQLite и TCL test suite, обеспечивая более, чем 99% тестовое покрытие и что тест TH3 обеспечивает 100% тестовое покрытие без утечек. Это убедительные доказательства, что динамическое выделение памяти используется правильно везде в SQLite.

2.1. Использование reallocarray()

reallocarray() это недавние инновации (приблизительно 2014) от сообщества OpenBSD, которые исходят из усилий предотвратить следующий "heartbleed" bug, избегая 32-битного арифметического переполнения целого числа на вычислениях размера выделения памяти. У функции 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 происходят, используя тонкие функции обертки, которые берут в качестве размера signed 64-bit integer. Исходный код SQLite проверен, чтобы гарантировать, что все вычисления размера выполняются, используя 64-битные целые числа со знаком. SQLite откажется ассигновать больше, чем приблизительно 2 ГБ памяти. Широко использующийся, SQLite редко когда-либо ассигнует больше, чем приблизительно 8 КБ памяти за один раз, таким образом, предел распределения в 2 ГБ не проблема. Таким образом, параметр с 64-bit signed integer обеспечивает запас высоты для обнаружения переполнения. Тот же самый аудит, который проверяет, что все вычисления размера сделаны как 64-битные целые числа со знаком также, проверяет, что невозможно переполнить 64-битное целое число во время вычисления.

Проверки соответствия программы спецификациям гарантируют, что вычисления размера выделения памяти не переполняются в SQLite и повторяются для каждого выпуска SQLite.

3. Настройка

Параметры настройки выделения памяти по умолчанию в SQLite подходят для большинства ситуаций. Однако, приложения с необычными или особенно строгими требованиями могут хотеть приспособить конфигурацию, чтобы более тесно выровнять SQLite к их потребностям. Параметры времени компиляции и времени выполнения доступны.

3.1. Альтернативные распределители памяти низкого уровня

Исходный код SQLite включает несколько различных модулей выделения памяти, которые могут быть отобраны во время компиляции или ограниченно во время запуска.

3.1.1. Распределитель памяти по умолчанию

По умолчанию SQLite использует malloc(), realloc() и free() из стандартной библиотеки для C для выделения памяти. Этот установленный порядок окружен тонкой оберткой, которая также обеспечивает функцию "memsize()", которая возвратит размер существующего распределения. Функция memsize() необходима, чтобы провести точный подсчет числа байтов выдающейся памяти, memsize() определяет, сколько байтов удалить, когда распределение освобождено. Распределитель по умолчанию осуществляет memsize(), всегда ассигнуя 8 дополнительных байтов на каждом запросе malloc() и храня размер распределения в том 8-байтовом заголовке.

Распределитель памяти по умолчанию рекомендуют для большинства приложений. Если у вас нет потребности использовать альтернативный распределителя памяти, то используйте умолчание.

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 native

Если SQLite собран для Windows с выбором времени компиляции SQLITE_WIN32_MALLOC, другая тонкая обертка используется вокруг HeapAlloc(), HeapReAlloc() и HeapFree(). Тонкая обертка использует формируемую кучу SQLite, которая будет отличаться от стандартной "кучи" процесса, если выбор времени компиляции SQLITE_WIN32_HEAP_CREATE применен. Кроме того, когда распределение сделано или освобождено, HeapValidate() вызывается, если SQLite будет собран с assert() и опцией сборки SQLITE_WIN32_MALLOC_VALIDATE.

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 это integer, целое число, которое является числом байтов памяти, на которое указывает pBuf. mnReq, другое целое число, которое является минимальным размером распределения. Любой вызов sqlite3_malloc(N), где N меньше mnReq will будет округлено к mnReq. mnReq должен быть степенью двух. Мы будем видеть позже, что параметр mnReq важен в сокращении n и следовательно требования размера минимальной памяти в Robson proof.

Распределитель memsys5 разработан для использования во встроенных системах, хотя нет ничего, чтобы предотвратить его использование на рабочих станциях. szBuf, как правило, от нескольких сотен килобайт до нескольких дюжин мегабайт, в зависимости от требований к памяти и системных требований.

Алгоритм, используемый memsys5, можно назвать "power-of-two, first-fit". Размеры всех запросов выделения памяти округляются к степени двух, запрос удовлетворен первым свободным слотом в pBuf, который является достаточно большим. Смежные освобожденные отчисления соединены, используя систему. Когда используется соответственно, этот алгоритм обеспечивает математические гарантии от фрагментации, как описано далее.

3.1.5. Экспериментальные распределители памяти

Имя "memsys5" используемое для нулевого-malloc распределителя памяти подразумевает, что есть несколько дополнительных доступных распределителей памяти, и они действительно есть. Распределитель памяти по умолчанию "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 действительно просто структура, содержащая указатели на функции, чтобы осуществить различные примитивы выделения памяти.

В многопоточном применении преобразовывается в последовательную форму доступ к sqlite3_mem_methods если и только если включен SQLITE_CONFIG_MEMSTATUS. Если 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, должно будет использовать sqlite3_config() вместе с SQLITE_CONFIG_MALLOC или SQLITE_CONFIG_HEAP, чтобы определить нового альтернативного распределителя памяти прежде, чем начать использовать SQLite.

3.2. Кэш-память страницы

В большинстве приложений подсистема кэша страницы базы данных в SQLite использует более динамично ассигнованную память, чем все другие части объединенного SQLite. Весьма обычно видеть, что кэш страницы базы данных потребляет более чем в 10 раз больше памяти, чем остальная часть SQLite.

SQLite может формироваться, чтобы сделать отчисления кэш-памяти страницы из отдельного и отличного пула памяти фиксированного размера. У этого может быть два преимущества:

  • Поскольку отчисления все одинакового размер, распределитель памяти может действовать намного быстрее. Все неассигнованные места памяти могут быть сохранены в связанном списке. Распределение заключается в удалении первого входа из списка. Освобождение просто добавляет вход в начало списка.

  • С единственным размером распределения n параметр в Robson proof = 1, и пространство общей памяти, требуемое распределителем (N), точно равно используемому (M) максимуму памяти. Никакая дополнительная память не требуется, чтобы покрывать фрагментацию, таким образом уменьшая требования к памяти. Это особенно важно для кэш-памяти страницы, так как кэш страницы составляет самый большой компонент потребностей в памяти SQLite.

Распределитель кэш-памяти страницы выключен по умолчанию. Приложение может позволить его во время запуска следующим образом:

sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);

pBuf это указатель на смежный диапазон байтов, которые SQLite будет использовать для отчислений кэш-памяти страницы. Буфер должен быть, по крайней мере, sz*N байт в размере. "sz" это размер каждого распределения кэша страницы. N это максимальное количество доступных отчислений.

Если SQLite нужна запись кэша страницы, которая больше, чем "sz" байт или если ему нужны больше, чем N записей, он отступает к использованию распределителя памяти общего назначения.

3.3. Сохраняющий распределитель памяти

Соединения с БД делают много маленьких и недолгих выделений памяти. Это происходит обычно, собирая SQL-операторы, используя sqlite3_prepare_v2(), но также в меньшей степени, управляя подготовленными запросами, используя sqlite3_step(). Эти маленькие выделения памяти используются, чтобы держать такие вещи, как названия таблиц и колонок, узлов дерева синтаксического анализа, значения результатов отдельного запроса и объектов курсора B-дерева. Есть следовательно много обращений к malloc() и free(), в итоге они занимают значительную долю процессорного времени, назначенного SQLite.

SQLite version 3.6.1 (2008-08-06) представил сохраняющего распределителя памяти, чтобы помочь уменьшить нагрузку выделения памяти. В сохраняющем распределителе каждое соединение с БД предварительно ассигнует единственный большой кусок памяти (как правило, в диапазоне 60-120 килобайт) и делит тот кусок на маленькие, фиксированного размера, "места" приблизительно по 100-1000 байтов каждое. Это становится сохраняющим пулом памяти. После того выделения памяти, связанные с соединением с БД, которые не являются слишком большими, удовлетворены, используя одно из сохраняющих мест пула, а не вызвав распределителя памяти общего назначения. Большие отчисления продолжают использовать распределителя памяти общего назначения, также, как и отчисления, которые происходят, когда все сохраняющие места заняты. Но во многих случаях, выделения памяти достаточно маленькие, так что новые запросы памяти могут быть удовлетворены из сохраняющего пула.

Поскольку сохраняющие отчисления всегда имеют тот же самый размер, распределение и алгоритмы освобождения очень быстры. Нет никакой потребности соединить смежные свободные слоты или искать место конкретного размера. Каждое соединение с БД ведет отдельно-связанный-список неиспользуемых слотов. Запросы распределения просто тянут первый элемент этого списка. Освобождение просто выдвигает элемент назад в начало списка. Кроме того, каждое соединение с БД, как предполагается, уже работает в одном потоке (есть mutexes уже есть), таким образом, никакой дополнительный mutexing не требуется, чтобы преобразовывать в последовательную форму доступ к сохраняющему месту в списке свободных слотов. Следовательно, сохраняющие выделения памяти и ее освобождение очень быстры. В тестах на скорость на рабочих станциях 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 version 3.31.0 (2020-01-22), хранение поддерживает два пула памяти, каждый с различным размером места. Пул маленького места использует 128-байтовые места, большой использует любой размер, который определяется SQLITE_DBCONFIG_LOOKASIDE (по умолчанию 1200 байт). Разделение пула на два позволяет выделениям памяти быть покрытыми хранением чаще, в то же время уменьшая использование кучи соединения с базой данных от 120 КБ вниз к 48 КБ.

Конфигурация продолжает использовать параметры конфигурации SQLITE_DBCONFIG_LOOKASIDE или SQLITE_CONFIG_LOOKASIDE, как описано выше, с параметрами "sz" и "cnt". Полное пространство heap, используемое для хранения, продолжает быть sz*cnt байтами. Но место выделено между хранением маленького места и хранением большого места с предпочтением, отданным хранению маленького места. Общее количество мест будет обычно превышать "cnt", так как "sz", как правило, намного больше, чем маленький размер слота в 128 байт.

Конфигурация хранения по умолчанию изменилась от 100 мест по 1200 байт каждое (120 КБ) на 40 мест по 1200 байт каждое (48 КБ). Это пространство заканчивает тем, что было ассигновано 93 места по 128 байт каждое и 30 мест по 1200 байт каждое. Таким образом, больше сохраняющих мест доступно, но намного меньше пространства "кучи" используется.

Конфигурация хранения по умолчанию, размер маленьких мест, и детали того, как пространство "кучи" ассигнуется между маленькими местами и большими местами, все подлежит изменению от одного выпуска до следующего.

3.4. Состояние памяти

По умолчанию SQLite держит статистику по своему использованию памяти. Эти статистические данные полезны, чтобы определить, в каком количестве памяти приложение действительно нуждается. Статистика может также использоваться в системе высокой надежности, чтобы определить, близко подходит ли использование памяти или превышает пределы Robson proof и следовательно что подсистема выделения памяти склонна к расстройству.

Большинство статистических данных памяти глобально, и поэтому отслеживание статистики должно быть преобразовано в последовательную форму с mutex. Статистические данные включены по умолчанию, но выбор существует, чтобы отключить их. Отключая статистику памяти, SQLite избегает входить и оставлять mutex на каждом выделении памяти и освобождении. Сбережения могут быть большими на системах, где операции mutex дорогие. Чтобы отключить статистику памяти, следующий интерфейс используется во время запуска:

sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);

Параметр "onoff" = true, чтобы включить отслеживанию статистики памяти и false, чтобы выключить.

Если статистики позволено, следующий установленный порядок может использоваться, чтобы получить доступ к ней:

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

Аргумент "verb" определяет, к какой статистической величине получают доступ. Есть разные verb. Список, как ожидают, вырастет в соответствии с совершенствованием sqlite3_status(). Текущее значение выбранного параметра написано в целое число "current", самое большое в "highwater". Если resetflag = true, то высшая точка перезагружается вниз к текущему значению после возврата.

Различный интерфейс используется, чтобы счесть статистику связанную с соединением БД:

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

Этот интерфейс подобен за исключением того, что он берет указатель на срединение БД как его первый аргумент и возвращает статистику о том одном объекте, а не обо всей библиотеке SQLite. sqlite3_db_status() в настоящее время признает только единственный verb SQLITE_DBSTATUS_LOOKASIDE_USED, хотя дополнительные могут быть добавлены в будущем.

Статистические данные для каждого подключения не используют глобальные переменные и следовательно не требуют, чтобы mutexes обновил или получил доступ. Следовательно, статистические данные для каждого подключения продолжают функционировать, даже если SQLITE_CONFIG_MEMSTATUS = off.

3.5. Установление пределов использования памяти

sqlite3_soft_heap_limit64() может использоваться, чтобы установить верхнюю границу общей суммы выдающейся памяти, что распределитель памяти общего назначения для SQLite будет вообще использовать. Если попытки будут предприняты, чтобы ассигновать больше памяти, чем указано мягким пределом кучи, то SQLite сначала попытается освободить кэш-память прежде, чем продолжить запрос распределения. Мягкий механизм предела кучи работает только, если статистические данные памяти позволены, и он работает лучше всего, если библиотека SQLite собрана с выбором времени компиляции SQLITE_ENABLE_MEMORY_MANAGEMENT.

Мягкий предел кучи "мягкий" в этом смысле: если SQLite не в состоянии освободить достаточно вспомогательной памяти, чтобы остаться ниже предела, это ассигнует дополнительную память и превышает свой предел. Это происходит в соответствии с теорией, что лучше использовать дополнительную память, чем потерпеть неудачу напрямую.

С SQLite version 3.6.1 (2008-08-06) мягкий предел кучи относится только к распределителю памяти общего назначения. Мягкий предел кучи не знает о распределителе памяти кэша страниц или сохраняющем распределителе памяти.

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 Отношение самого большого выделения памяти к самому маленькому. Мы предполагаем, что каждый размер выделения памяти это integer, кратный самому маленькому размеру выделения памяти.

Robson доказывает следующий результат:

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

В разговорной речи доказательство Робсона показывает, что, чтобы гарантировать надежность, любой распределитель памяти должен использовать пул памяти размера N, который превышает максимальный объем когда-либо использованной памяти M множителем, который зависит от n, отношения самого большого к самому маленькому размеру распределения. Другими словами, если все выделения памяти не имеют точно того же самого размера (n=1), системе нужен доступ к большей памяти, чем это будет когда-либо использовать. Кроме того, мы видим, что сумма избыточной требуемой памяти растет быстро как отношение самых больших к наименьшим запросам, таким образом есть сильный стимул держать все отчисления максимально близко к тому же самому размеру.

Доказательство Робсона конструктивно. Он предоставляет алгоритм для вычисления последовательности распределения и операций по освобождению, которые приведут к неудаче распределения из-за фрагментации памяти, если доступная память составляет на один байт меньше, чем N. Робсон показывает, что распределитель памяти power-of-two first-fit (такой, как осуществлен memsys5) никогда не будет сбоить при выделении памяти при условии, что доступная память N или больше байт.

Значения M и n это свойства приложения. Если приложение построено таким способом, что M и n известны, или по крайней мере есть их верхние границы, и если приложение использует распределителя памяти memsys5 и предоставлено N байт доступного пространства памяти, используя SQLITE_CONFIG_HEAP, Робсон доказывает, что никакой запрос выделения памяти никогда не будет терпеть неудачу в приложении. Разработчик приложений может выбрать значение N, которое гарантирует, что никакой вызов любого интерфейса SQLite никогда не будет возвращать SQLITE_NOMEM. Пул памяти никогда не будет становиться так фрагментированным, что новый запрос выделения памяти не может быть удовлетворен. Это важно для систем, где программный сбой мог нанести повреждения, физический вред или потерю незаменимых данных.

4.1. Вычисление и управление параметрами M и n

Доказательство Робсона применяется отдельно к каждому из распределителей памяти, используемых SQLite:

Для распределителей кроме memsys5 все выделения памяти имеют тот же самый размер. Следовательно, n=1 и поэтому N=M. Другими словами, пул памяти должны быть не больше, чем самый большой объем памяти в использовании в любой момент.

Использованием кэша страниц несколько более трудно управлять в версии SQLite version 3.6.1, хотя механизмы планируются на последующие версии, которые сделают управление этой памятью намного легче. До введения этих новых механизмов единственный способ управлять памятью кэша страниц использует 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);

Сохраняющий распределитель памяти действительно предназначается как исполнительная оптимизация, таким образом, весьма разумно полностью отключить распределитель, ведающий распределением памяти для критических по отношению к безопасности операций.

Распределитель универсального ЗУ это самый трудный пул памяти, потому что он поддерживает отчисления переменных размеров. Так как n это множитель M, причем мы хотим держать n как можно меньше. Это приводит доводы в пользу хранения минимального размера распределения для memsys5 как можно больше. В большинстве ситуаций сохраняющий распределитель памяти в состоянии обращаться с маленькими отчислениями. Таким образом, разумно установить минимальный размер распределения для memsys5 в 2, 4 или даже в 8 раз от максимального размера сохраняющего распределения. Минимальный размер распределения 512 является разумным урегулированием.

В дополнение к сохранению n маленьким стоит держать размер самых больших выделений памяти под контролем. Большие запросы распределителю памяти общего назначения могли бы прибыть из нескольких источников:

  1. Строки таблицы SQL, которые содержат большие последовательности или BLOB.
  2. Сложные SQL-запросы, которые собирают большие подготовленные запросы.
  3. Объекты анализатора SQL, используемые внутренне sqlite3_prepare_v2().
  4. Пространство памяти для объектов соединения с базой данных.
  5. Отчисления кэш-памяти страницы, которые переполняются в распределителе памяти общего назначения.
  6. Отчисления буфера предыстории на новые соединения с базой данных.

Последними двумя отчислениями можно управлять и/или устранить их, формируя распределитель памяти кэша страниц и сохраняющий распределитель памяти соответственно, как описано выше. Пространство памяти, требуемое для объектов соединения с базой данных, зависит в некоторой степени от длины имени файла файла базы данных, но редко превышает 2 КБ на 32-битных системах. Больше пространства требуется на 64-битных системах из-за увеличенного размера указателей. Каждый объект анализатора использует приблизительно 1.6 КБ памяти. Таким образом элементами 3 - 6 выше можно легко управлять, чтобы держать максимальный размер выделения памяти ниже 2 КБ.

Если приложение разработано, чтобы управлять данными в маленьких частях, то база данных никогда не должна содержать большие последовательности или BLOB, а значит элемент 1 выше не должен быть фактором. Если база данных действительно содержит большие последовательности или BLOB, они должны быть прочитаны, используя incremental BLOB I/O, строки, которые содержат большие последовательности или BLOB, никогда не должны быть обновлены каким-либо образом, кроме incremental BLOB I/O. Иначе sqlite3_step() должен будет прочитать всю строку в смежную память в какой-то момент, а это включит по крайней мере одно распределение памяти большой емкости.

Заключительный источник отчислений памяти большой емкости это место, чтобы держать подготовленные запросы, которые следуют из компилирования сложных операций SQL. Продолжающаяся работа разработчиков SQLite уменьшает сумму пространства, требуемого здесь. Но выполнение больших запросов и сложные запросы могли бы все еще потребовать подготовленных запросов, которые составляют несколько килобайтов в размере. Единственная работа в данный момент для приложения это делить сложные операции SQL на две или больше меньших и более простых операции, содержавшиеся в отдельных подготовленных запросах.

Учитывая все обстоятельства, запросы обычно должны быть в состоянии держать свой максимальный размер выделения памяти ниже 2K или 4K. Это дает значение log2(n) 2 или 3. Это ограничит N между 2 и 2.5*M.

Максимальная сумма памяти общего назначения, необходимой приложению, определяется такими факторами, как сколько одновременно открыто соединений с БД и сколько объектов подготовленных запросов использовано. Для любого данного применения эти факторы обычно фиксируются и могут быть определены экспериментально, используя SQLITE_STATUS_MEMORY_USED. Типовое приложение могло бы только использовать приблизительно 40 КБ памяти общего назначения. Это дает N около 100KB.

4.2. Податливая неудача

Если подсистемы выделения памяти в SQLite формируются для надежности, но фактическое использование памяти превышает пределы дизайна, установленные Robson proof, SQLite будет обычно продолжать работать. Распределитель памяти кэша страниц и сохраняющий распределитель памяти автоматически откатываются к распределителю памяти общего назначения memsys5. Обычно имеет место, что memsys5 продолжит функционировать без фрагментации, даже если M и/или n превысят ограничения Robson proof. Robson proof показывает, что для выделения памяти возможно сломаться и потерпеть неудачу при этом обстоятельстве, но такая неудача требует особенно сложной последовательности отчислений и освобождения, такой пока не было. Так на практике обычно имеет место, что ограничения, наложенные Робсоном, могут быть превышены значительно без вредного воздействия.

Тем не менее, разработчиков приложений убеждают контролировать состояние подсистем выделения памяти и поднять тревогу, когда использование памяти приближается или превышает пределы Робсона. Таким образом применение предоставит операторам предупреждение заранее. Интерфейсы memory statistics SQLite предоставляют применению весь механизм, необходимый, чтобы закончить контролирующую часть этой задачи.

5. Стабильность интерфейсов памяти

Обновлено: С SQLite version 3.7.0 (2010-07-21) все интерфейсы выделения памяти SQLite считают стабильными и поддерживаются в будущих выпусках.