Динамическое выделение памяти в 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 правильно реагирует на сбой динамического выделения памяти.
Инфраструктура тестирования проверяет, что 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(), всегда выделяя 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
Если 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. 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 и базовым выделением памяти. Например, логика тестирования выделения памяти «out-of-memory» для 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 — максимальное количество доступных выделений.
Если 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 с большими слотами, отдавая предпочтение пулу 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 не может освободить достаточно вспомогательной памяти, чтобы остаться ниже лимита, то он продолжает выделять дополнительную память и превышает свой лимит. Это происходит в теории, что лучше использовать дополнительную память, чем полностью потерпеть неудачу.
Начиная с версии SQLite 3.6.1 (2008-08-06), мягкий лимит кучи применяется только к универсальному выделенному памяти аллокатору. Мягкий лимит кучи не знает о или взаимодействует с аллокатором кэш-памяти или аллокатором lookaside-памяти. Эта недостача, вероятно, будет устранена в будущей версии.
4. Математические гарантии против сбоев выделения памяти
Проблема динамического выделения памяти, а именно проблема сбоя аллокатора памяти, изучалась Дж. М. Робсоном, и результаты опубликованы в:
Дж. М. Робсон. «Границы для некоторых функций, относящихся к динамическому распределению памяти». Журнал Ассоциации вычислительной техники, том 21, номер 8, июль 1974 года, страницы 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 ever has checked out at any point in time. 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, хотя для последующих версий планируются механизмы, которые значительно упростят управление кэш-памятью. До появления этих новых механизмов единственный способ управлять кэш-памятью — использование pragma 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-памяти предназначен в первую очередь для повышения производительности, а не для обеспечения бесперебойного выделения памяти, поэтому для критически важных операций неразумно полностью отключать аллокатор lookaside-памяти.
Универсальный аллокатор памяти — это самый сложный для управления пул памяти, поскольку он поддерживает выделение памяти различного размера. Поскольку n — это множитель M, мы хотим сохранить n как можно меньше. Это говорит о том, чтобы сохранить минимальный размер выделения для memsys5 как можно больше. В большинстве приложений аллокатор lookaside-памяти способен обрабатывать небольшие выделения. Поэтому разумно установить минимальный размер выделения для memsys5 в 2, 4 или даже 8 раз больше максимального размера выделения lookaside. Минимальный размер выделения 512 — разумное значение.
Для того чтобы сохранить n маленьким, желательно контролировать размер наибольших выделений памяти. Крупные запросы к универсальному аллокатору памяти могут поступать из нескольких источников:
- Строки таблиц SQL, содержащие длинные строки или BLOB-данные.
- Сложные запросы SQL, компилируемые в большие предварительно подготовленные операторы.
- Объекты парсера SQL, используемые внутри sqlite3_prepare_v2().
- Пространство хранения для объектов подключений к базам данных.
- Выделения памяти кэш-страницы, которые переполняются в универсальный аллокатор памяти.
- Выделения буфера lookaside для новых подключений к базам данных.
Последние два выделения могут быть контролируемы и/или устранены путем соответствующей настройки аллокатора кэш-памяти и аллокатора lookaside-памяти, как описано выше. Объем памяти, необходимый для объектов подключений к базам данных, в некоторой степени зависит от длины имени файла базы данных, но редко превышает 2 КБ на 32-разрядных системах. (На 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 считаются стабильными и будут поддерживаться в будущих выпусках.
SQLite is in the Public Domain.
https://sqlite.org/malloc.html