Динамическое выделение памяти в 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 будет пытаться повторно использовать память из своих кэшей, а не выделять новую.
Режим без 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_MEMDEBUG. Он значительно медленнее распределителя памяти по умолчанию, поэтому его не рекомендуется использовать в рабочей среде. Однако при включении во время тестирования инструментированный распределитель памяти выполняет следующие проверки:
Проверка границ. Инструментированный распределитель памяти размещает контрольные значения в начале и конце каждого выделенного блока, чтобы проверить, не записывает ли код SQLite данные за его границами.
Использование памяти после освобождения. При освобождении каждого блока памяти все его байты перезаписываются бессмысленным шаблоном битов. Это помогает убедиться, что память не используется после освобождения.
Освобождение памяти, не полученной через malloc. Каждый блок памяти, выделенный инструментированным распределителем, содержит контрольные значения, позволяющие проверить, что каждый освобожденный блок ранее был получен через malloc.
Неинициализированная память. Инструментированный распределитель памяти инициализирует каждый выделенный блок бессмысленным шаблоном битов, помогая убедиться, что пользователь не полагается на содержимое выделенной памяти.
Независимо от того, используется ли инструментированный распределитель памяти, SQLite отслеживает объем памяти, который в данный момент занят. Для тестирования SQLite используется несколько сотен тестовых сценариев. В конце каждого сценария все объекты уничтожаются и проверяется, освобождена ли вся память. Так обнаруживаются утечки памяти. Обратите внимание, что обнаружение утечек памяти выполняется всегда — как при тестовых, так и при рабочих сборках. Каждый раз, когда один из разработчиков запускает отдельный тестовый сценарий, обнаружение утечек памяти активно. Поэтому утечки памяти, возникающие при разработке, быстро обнаруживаются и устраняются.
Реакция SQLite на ошибки нехватки памяти (OOM) тестируется с помощью специальной надстройки над распределителем памяти, которая имитирует сбои выделения памяти. Надстройка — это слой, расположенный между распределителем памяти и остальной частью SQLite. Она передает большинство запросов на выделение памяти непосредственно базовому распределителю и возвращает результаты вызывающей стороне. Однако надстройку можно настроить так, чтобы N-е выделение памяти завершилось сбоем. Для запуска теста OOM надстройку сначала настраивают на сбой при первой попытке выделения. Затем запускают тестовый сценарий и проверяют, что сбой выделения был корректно обнаружен и обработан. После этого надстройку настраивают на сбой при втором выделении и повторяют тест. Точка сбоя сдвигается на одно выделение за раз, пока вся тестовая процедура не завершится без ошибок выделения памяти. Вся последовательность тестов выполняется дважды. В первом проходе надстройка настроена на сбой только при N-м выделении. Во втором проходе она настроена на сбой при N-м и всех последующих выделениях.
Обратите внимание, что логика обнаружения утечек памяти продолжает работать и при использовании надстройки OOM. Это подтверждает, что SQLite не допускает утечек памяти даже при ошибках выделения памяти. Также обратите внимание, что надстройка OOM может работать с любым базовым распределителем памяти, в том числе с инструментированным распределителем, который проверяет правильность использования памяти. Таким образом подтверждается, что ошибки OOM не вызывают другие виды ошибок использования памяти.
Наконец, отметим, что инструментированный распределитель памяти и средство обнаружения утечек памяти работают на протяжении всего набора тестов SQLite, набор тестов TCL обеспечивает покрытие операторов более 99%, а тестовый стенд TH3 обеспечивает 100% покрытие ветвей без утечек памяти. Это убедительно доказывает, что динамическое выделение памяти корректно используется во всех частях SQLite.
2.1. Использование reallocarray()
Интерфейс reallocarray() — это недавняя разработка (примерно 2014 года) сообщества OpenBSD, возникшая в ходе работы над предотвращением следующей уязвимости «heartbleed» путем исключения переполнения 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 стремится решать. Для предотвращения проблем все внутренние выделения памяти SQLite выполняются с помощью тонких функций-оберток, принимающих размер в виде знакового 64-битного целого числа. Исходный код SQLite проверяется, чтобы убедиться, что все вычисления размеров также выполняются с использованием знаковых 64-битных целых чисел. SQLite откажется выделять за один раз более примерно 2 ГБ памяти. (При обычном использовании SQLite редко выделяет за один раз более примерно 8 КБ памяти, поэтому ограничение в 2 ГБ не создает проблем.) Таким образом, параметр размера в 64 бита обеспечивает большой запас для обнаружения переполнений. Та же проверка, которая подтверждает, что все вычисления размеров выполняются со знаковыми 64-битными целыми числами, также доказывает невозможность переполнения 64-битного целого числа в ходе вычислений.
Проверки кода, гарантирующие отсутствие переполнения при вычислении размеров выделяемой памяти в SQLite, повторяются перед каждым выпуском SQLite.
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 скомпилирована с параметром компиляции SQLITE_MEMDEBUG, системные malloc(), realloc() и free() оборачиваются другим, более сложным слоем. Этот слой выделяет около 100 байт дополнительного пространства для каждого блока. Дополнительное пространство используется для размещения контрольных значений на обоих концах блока, возвращаемого ядру SQLite. При освобождении блока эти значения проверяются, чтобы убедиться, что ядро SQLite не вышло за его границы ни в одну из сторон. Если системная библиотека — GLIBC, этот слой также использует функцию GNU backtrace() для анализа стека и записи функций-предков вызова malloc(). При запуске набора тестов SQLite слой также записывает имя текущего тестового случая. Эти две последние возможности полезны для поиска источника утечек памяти, обнаруженных набором тестов.
Сложный слой, используемый при включенном параметре SQLITE_MEMDEBUG, также заполняет каждое новое выделение бессмысленными данными перед возвратом вызывающей стороне. Сразу после освобождения блок снова заполняется бессмысленными данными. Эти действия помогают убедиться, что ядро SQLite не делает предположений о состоянии только что выделенной памяти и что выделенные блоки не используются после освобождения.
Сложный слой, используемый при включенном параметре SQLITE_MEMDEBUG, предназначен только для тестирования, анализа и отладки SQLite. Он существенно снижает производительность и увеличивает расход памяти, поэтому, вероятно, не должен использоваться в рабочей среде.
3.1.3. Собственный распределитель памяти Win32
Если 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 — это целое число, задающее размер участка памяти, на который указывает pBuf, в байтах. mnReq — еще одно целое число, задающее минимальный размер выделения. Любой вызов sqlite3_malloc(N), где N меньше mnReq, будет округлен до mnReq. Значение mnReq должно быть степенью двойки. Далее мы увидим, что параметр mnReq важен для уменьшения значения n и, следовательно, минимального требования к объему памяти в доказательстве Робсона.
Распределитель memsys5 предназначен для встраиваемых систем, хотя ничто не мешает использовать его на рабочих станциях. Размер szBuf обычно составляет от нескольких сотен килобайт до нескольких десятков мегабайт — в зависимости от требований системы и бюджета памяти.
Алгоритм, используемый memsys5, можно назвать «первый подходящий блок степенного размера». Размер всех запросов на выделение памяти округляется до ближайшей степени двойки, а запрос удовлетворяется первым свободным блоком в 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 считается экспериментальным и устаревшим, и, вероятно, будет удален из дерева исходных файлов в одном из будущих выпусков 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 в доказательстве Робсона равен 1, а общий объем памяти, необходимый распределителю (N), в точности равен максимальному объему используемой памяти (M). Для покрытия накладных расходов на фрагментацию дополнительная память не требуется, что сокращает потребность в памяти. Это особенно важно для памяти кэша страниц, поскольку кэш страниц составляет наибольшую часть потребностей SQLite в памяти.
Распределитель памяти кэша страниц по умолчанию отключен. Приложение может включить его при запуске следующим образом:
sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);
Параметр pBuf — это указатель на непрерывный диапазон байтов, который SQLite будет использовать для выделения памяти кэша страниц. Размер буфера должен составлять не менее sz*N байт. Параметр "sz" задает размер каждого выделения для кэша страниц. N — максимальное число доступных выделений.
Обратите внимание, что каждый блок памяти кэша страниц больше размера страницы базы данных. Если размер страницы базы данных равен Z, каждое выделение памяти кэша страниц будет иметь размер Z+Y байт, где Y — константа, зависящая от параметров компиляции и целевого процессора. Для конкретной платформы и сборки SQLite значение Y можно определить с помощью параметра SQLITE_CONFIG_PCACHE_HDRSZ интерфейса sqlite3_config().
Если SQLite требуется запись кэша страниц размером более "sz" байт или больше N записей, она использует универсальный распределитель памяти.
3.3. Распределитель памяти lookaside
Подключения к базе данных SQLite выполняют множество небольших выделений памяти с коротким сроком жизни. Чаще всего это происходит при компиляции операторов SQL с помощью sqlite3_prepare_v2(), но также, в меньшей степени, при выполнении подготовленных операторов с помощью sqlite3_step(). Эти небольшие блоки памяти используются для хранения, например, имен таблиц и столбцов, узлов дерева разбора, отдельных значений результатов запроса и объектов курсора B-дерева. В результате выполняется множество вызовов malloc() и free() — настолько много, что malloc() и free() начинают отнимать значительную часть процессорного времени, выделенного SQLite.
В версии SQLite 3.6.1 (2008-08-06) появился распределитель памяти lookaside, помогающий снизить нагрузку на распределение памяти. При использовании распределителя lookaside каждое подключение к базе данных заранее выделяет один большой участок памяти (обычно от 60 до 120 килобайт) и делит его на небольшие слоты фиксированного размера — примерно от 100 до 1000 байт каждый. Так образуется пул памяти lookaside. После этого выделения памяти, связанные с подключением к базе данных и не слишком большие по размеру, удовлетворяются одним из слотов пула lookaside, а не вызовом универсального распределителя памяти. Для больших выделений по-прежнему используется универсальный распределитель памяти, как и в случаях, когда все слоты пула lookaside заняты. Однако во многих случаях выделения памяти достаточно малы, а одновременно занятых слотов достаточно мало, чтобы удовлетворить новые запросы на выделение из пула lookaside.
Поскольку выделения lookaside всегда имеют одинаковый размер, алгоритмы выделения и освобождения памяти работают очень быстро. Не нужно объединять соседние свободные слоты или искать слот определенного размера. Каждое подключение к базе данных поддерживает односвязный список неиспользуемых слотов. Запросы на выделение просто извлекают первый элемент этого списка. При освобождении элемент просто помещается в начало списка. Кроме того, предполагается, что каждое подключение к базе данных уже выполняется в одном потоке (для этого используются имеющиеся мьютексы), поэтому для сериализации доступа к списку свободных слотов lookaside дополнительные мьютексы не требуются. Следовательно, выделение и освобождение памяти lookaside выполняются очень быстро. В тестах производительности на рабочих станциях под Linux и Mac OS X общая производительность SQLite повышалась на 10–15% в зависимости от конфигурации рабочей нагрузки и lookaside.
Размер пула памяти lookaside имеет глобальное значение по умолчанию, но также может настраиваться отдельно для каждого подключения. Чтобы изменить размер пула памяти lookaside по умолчанию во время компиляции, используйте параметр -DSQLITE_DEFAULT_LOOKASIDE=SZ,N. Чтобы изменить размер пула памяти lookaside по умолчанию при запуске, используйте интерфейс sqlite3_config():
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);
Параметр "sz" задает размер в байтах каждого слота lookaside. Параметр "cnt" задает общее число слотов памяти lookaside для каждого подключения к базе данных. Общий объем памяти lookaside, выделяемый для каждого подключения к базе данных, равен sz*cnt байт.
Пул lookaside для отдельного подключения к базе данных "db" можно изменить с помощью следующего вызова:
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, pBuf, sz, cnt);
Параметр "pBuf" — это указатель на область памяти, которая будет использоваться в качестве пула памяти lookaside. Если pBuf равен NULL, SQLite самостоятельно выделит память для пула с помощью sqlite3_malloc(). Параметры "sz" и "cnt" задают соответственно размер каждого слота lookaside и число слотов. Если pBuf не равен NULL, он должен указывать как минимум на sz*cnt байт памяти.
Конфигурацию lookaside можно изменить только в том случае, если для подключения к базе данных нет незавершенных выделений lookaside. Поэтому конфигурацию следует задавать сразу после создания подключения к базе данных с помощью sqlite3_open() (или эквивалентной функции) и до выполнения каких-либо операторов SQL для этого подключения.
3.3.1. Lookaside с двумя размерами
Начиная с версии SQLite 3.31.0 (2020-01-22), механизм lookaside поддерживает два пула памяти, каждый с блоками разного размера. В пуле малых блоков используются блоки размером 128 байт, а в пуле больших блоков — блоки размера, указанного параметром SQLITE_DBCONFIG_LOOKASIDE (по умолчанию 1200 байт). Такое разделение пула позволяет чаще удовлетворять запросы на выделение памяти за счёт lookaside и одновременно сократить использование кучи на одно соединение с базой данных со 120 КБ до 48 КБ.
Для настройки по-прежнему используются параметры SQLITE_DBCONFIG_LOOKASIDE или SQLITE_CONFIG_LOOKASIDE, описанные выше, с параметрами "sz" и "cnt". Общий объём памяти кучи, используемый lookaside, по-прежнему равен sz*cnt байт. Однако память распределяется между lookaside для малых и больших блоков, причём предпочтение отдаётся lookaside для малых блоков. Общее количество блоков обычно превышает "cnt", поскольку значение "sz" обычно намного больше размера малого блока — 128 байт.
Конфигурация lookaside по умолчанию изменена: вместо 100 блоков по 1200 байт каждый (120 КБ) теперь используется 40 блоков по 1200 байт каждый (48 КБ). В итоге эта память выделяется под 93 блока по 128 байт и 30 блоков по 1200 байт. Таким образом, доступно больше блоков lookaside, но при этом используется значительно меньше памяти кучи.
Конфигурация lookaside по умолчанию, размер малых блоков и способ распределения памяти кучи между малыми и большими блоками могут меняться от одного выпуска к другому.
3.4. Состояние памяти
По умолчанию SQLite ведёт статистику использования памяти. Эта статистика помогает определить, сколько памяти действительно требуется приложению. В системах с высокими требованиями к надёжности статистику также можно использовать, чтобы определить, приближается ли использование памяти к ограничениям доказательства Робсона или превышает их, и, следовательно, не грозит ли подсистеме выделения памяти сбой.
Большая часть статистики памяти является глобальной, поэтому её сбор необходимо синхронизировать с помощью мьютекса. По умолчанию сбор статистики включён, но его можно отключить. Отключив сбор статистики памяти, SQLite избегает захвата и освобождения мьютекса при каждом выделении и освобождении памяти. Это может дать заметный выигрыш в системах, где операции с мьютексами обходятся дорого. Чтобы отключить сбор статистики памяти, при запуске используется следующий интерфейс:
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);
Параметр "onoff" имеет значение true, чтобы включить сбор статистики памяти, и false, чтобы отключить его.
Если сбор статистики включён, для доступа к ней можно использовать следующую функцию:
sqlite3_status(verb, ¤t, &highwater, resetflag);
Аргумент "verb" определяет, к какой статистике выполняется доступ. Определено несколько значений. Ожидается, что их список будет расширяться по мере развития интерфейса sqlite3_status(). Текущее значение выбранного параметра записывается в целочисленную переменную "current", а наибольшее значение за всё время — в целочисленную переменную "highwater". Если resetflag имеет значение true, после возврата из вызова максимальное значение сбрасывается до текущего.
Для получения статистики, связанной с одним соединением с базой данных, используется другой интерфейс:
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 собрана с параметром компиляции SQLITE_ENABLE_MEMORY_MANAGEMENT.
Ограничение кучи называется «мягким», потому что, если SQLite не удаётся освободить достаточно вспомогательной памяти, чтобы не выйти за пределы ограничения, она всё равно выделяет дополнительную память и превышает его. Это основано на предположении, что использовать дополнительную память лучше, чем полностью прекратить работу.
Начиная с версии 3.6.1 SQLite (2008-08-06), мягкое ограничение кучи применяется только к универсальному распределителю памяти. Оно не учитывает распределитель памяти кэша страниц и распределитель памяти lookaside и не взаимодействует с ними. Вероятно, это ограничение будет устранено в одном из будущих выпусков.
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.
Введём следующие обозначения (они похожи, но не совпадают с обозначениями Робсона):
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 has checked out. 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 + (log2 n)/2) - n + 1
Если говорить проще, доказательство Робсона показывает, что для гарантии работы без сбоев любому распределителю памяти необходим пул размером N, который превышает максимальный объём когда-либо использовавшейся памяти M в число раз, зависящее от n — отношения размера самого большого выделения к размеру самого маленького. Иными словами, если все выделения памяти не имеют строго одинаковый размер (n=1), системе требуется доступ к большему объёму памяти, чем она когда-либо будет использовать одновременно. Кроме того, мы видим, что необходимый избыток памяти быстро растёт с увеличением отношения размеров самого большого и самого маленького выделений, поэтому настоятельно рекомендуется, чтобы размеры всех выделений были как можно ближе друг к другу.
Доказательство Робсона конструктивно. В нём приводится алгоритм вычисления последовательности операций выделения и освобождения памяти, которая приводит к сбою выделения из-за фрагментации, если доступной памяти хотя бы на один байт меньше N. Робсон также показывает, что распределитель памяти с выбором первого подходящего блока и степенями двойки (например, реализованный в memsys5) никогда не завершит выделение памяти ошибкой, если доступно N байт или больше.
Значения M и n являются характеристиками приложения. Если приложение устроено так, что значения M и n известны или, по крайней мере, известны их верхние границы, и если приложение использует распределитель памяти memsys5 и ему предоставлено N байт доступной памяти с помощью SQLITE_CONFIG_HEAP, то, как доказывает Робсон, ни один запрос на выделение памяти в приложении никогда не завершится ошибкой. Иными словами, разработчик приложения может выбрать значение N, которое гарантирует, что ни один вызов интерфейса SQLite никогда не вернёт SQLITE_NOMEM. Фрагментация пула памяти никогда не станет настолько сильной, чтобы помешать удовлетворить новый запрос на выделение памяти. Это важное свойство для приложений, в которых программная ошибка может привести к травме, физическому ущербу или утрате невосстановимых данных.
4.1. Вычисление и управление параметрами M и n
Доказательство Робсона применяется отдельно к каждому распределителю памяти, используемому SQLite:
- Универсальный распределитель памяти (memsys5).
- Распределитель памяти кэша страниц.
- Распределитель памяти lookaside.
В распределителях, отличных от memsys5, все выделения памяти имеют одинаковый размер. Следовательно, n=1 и, соответственно, N=M. Иными словами, пул памяти не должен быть больше наибольшего объёма памяти, используемого в любой момент времени.
В версии SQLite 3.6.1 использование памяти кэша страниц несколько сложнее контролировать, хотя для следующих выпусков запланированы механизмы, которые значительно упростят эту задачу. До появления этих новых механизмов единственный способ контролировать память кэша страниц — использовать прагму cache_size.
В приложениях, критичных к безопасности, обычно требуется изменить конфигурацию памяти lookaside по умолчанию, чтобы при выделении начального буфера памяти lookaside во время вызова sqlite3_open() выделяемый объём памяти не был настолько большим, чтобы параметр n стал слишком большим. Чтобы контролировать значение n, лучше всего не допускать, чтобы максимальный размер выделения памяти превышал 2 или 4 килобайта. Поэтому разумной конфигурацией распределителя памяти lookaside по умолчанию может быть один из следующих вариантов:
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 */
Другой подход — изначально отключить распределитель памяти lookaside:
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);
Затем приложение может поддерживать отдельный пул больших буферов lookaside и распределять их между соединениями с базой данных по мере их создания. Обычно приложение использует только одно соединение с базой данных, поэтому пул памяти lookaside может состоять из одного большого буфера.
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, aStatic, 256, 500);
Распределитель памяти lookaside предназначен прежде всего для повышения производительности, а не для гарантии выделения памяти без сбоев, поэтому полностью отключить его для операций, критичных к безопасности, вполне допустимо.
Управлять универсальным распределителем памяти сложнее всего, поскольку он поддерживает выделение блоков разных размеров. Поскольку n является множителем для M, мы хотим, чтобы значение n было как можно меньше. Поэтому минимальный размер выделения для memsys5 следует сделать как можно больше. В большинстве приложений небольшие выделения обрабатывает распределитель памяти lookaside. Поэтому разумно установить минимальный размер выделения для memsys5 равным 2, 4 или даже 8 максимальным размерам выделения lookaside. Разумное значение минимального размера выделения — 512.
Чтобы уменьшить значение n, также следует контролировать размер самых больших выделений памяти. Крупные запросы к универсальному распределителю памяти могут поступать из нескольких источников:
- Строки таблиц SQL, содержащие большие строки или BLOB-объекты.
- Сложные запросы SQL, при компиляции превращающиеся в большие подготовленные инструкции.
- Объекты синтаксического анализатора SQL, используемые внутри sqlite3_prepare_v2().
- Память для объектов соединения с базой данных.
- Выделения памяти для кэша страниц, которые переполняют его и переходят к универсальному распределителю памяти.
- Выделения памяти для буфера lookaside для новых соединений с базой данных.
Последние два вида выделений можно контролировать и/или устранить, правильно настроив распределитель памяти кэша страниц и распределитель памяти lookaside, как описано выше. Объём памяти, требуемый для объектов соединения с базой данных, в некоторой степени зависит от длины имени файла базы данных, но в 32-разрядных системах редко превышает 2 КБ. (В 64-разрядных системах требуется больше памяти из-за увеличенного размера указателей.) Каждый объект синтаксического анализатора использует около 1,6 КБ памяти. Таким образом, для элементов с 3-го по 6-й выше можно легко ограничить максимальный размер выделения памяти значением менее 2 КБ.
Если приложение предназначено для работы с небольшими порциями данных, база данных не должна содержать больших строк или BLOB-объектов, поэтому элемент 1 выше не будет иметь значения. Если база данных содержит большие строки или BLOB-объекты, их следует считывать с помощью инкрементального ввода-вывода BLOB-объектов, а строки, содержащие эти большие строки или BLOB-объекты, следует обновлять только с помощью инкрементального ввода-вывода BLOB-объектов. В противном случае функции sqlite3_step() в какой-то момент потребуется прочитать всю строку в непрерывный блок памяти, что потребует как минимум одного крупного выделения памяти.
Последний источник крупных выделений памяти — пространство для хранения подготовленных инструкций, создаваемых при компиляции сложных операций SQL. Разработчики SQLite продолжают работу над сокращением необходимого для этого объёма памяти. Тем не менее для больших и сложных запросов могут по-прежнему требоваться подготовленные инструкции размером в несколько килобайт. В настоящее время единственный обходной путь — разделить сложные операции SQL на две или более небольшие и простые операции, содержащиеся в отдельных подготовленных инструкциях.
В целом приложения обычно могут удерживать максимальный размер выделения памяти ниже 2 или 4 КБ. Это даёт значение log2(n), равное 2 или 3. Тогда N будет в 2–2,5 раза больше M.
Максимальный объём универсальной памяти, необходимый приложению, определяется такими факторами, как количество одновременно открытых объектов соединения с базой данных и подготовленных инструкций, используемых приложением, а также сложность подготовленных инструкций. Для каждого приложения эти факторы обычно постоянны, и их можно определить экспериментально с помощью SQLITE_STATUS_MEMORY_USED. Типичное приложение может использовать всего около 40 КБ универсальной памяти. В таком случае значение N составит примерно 100 КБ.
4.2. Отказ без резкого прекращения работы
Если подсистемы выделения памяти SQLite настроены на работу без сбоев, но фактическое использование памяти превышает ограничения, установленные доказательством Робсона, SQLite обычно продолжает работать в штатном режиме. Распределитель памяти кэша страниц и распределитель памяти lookaside автоматически переключаются на универсальный распределитель памяти memsys5. Обычно распределитель памяти memsys5 продолжает работать без фрагментации, даже если M и/или n превышают ограничения, установленные доказательством Робсона. Доказательство Робсона показывает, что в таких условиях возможен сбой при выделении памяти, но для этого требуется особенно неудачная последовательность выделений и освобождений — такую последовательность SQLite никогда не наблюдали. Поэтому на практике ограничения Робсона обычно можно значительно превысить без каких-либо негативных последствий.
Тем не менее разработчикам приложений рекомендуется отслеживать состояние подсистем выделения памяти и подавать сигналы тревоги, когда использование памяти приближается к ограничениям Робсона или превышает их. Так приложение заблаговременно предупредит операторов о возможном сбое. Интерфейсы SQLite для получения статистики памяти предоставляют приложению все необходимые средства для такого мониторинга.
5. Стабильность интерфейсов памяти
Обновление: Начиная с версии SQLite 3.7.0 (2010-07-21), все интерфейсы SQLite для выделения памяти считаются стабильными и будут поддерживаться в будущих выпусках.
Эта страница обновлена 2025-11-13 07:12:58Z
SQLite is in the Public Domain.
https://sqlite.org/malloc.html