Расширения PostgreSQL: управление памятью

11 апреля 2022

Как мы недавно выяснили, в PostgreSQL есть исключения. Но использовать исключения в языке С, где нет ни автоматического управления памятью, ни умных указателей, не кажется хорошей идеей. Так вот, оказывается, что вместо умных указателей PostgreSQL предлагает совершенно другой механизм — контексты памяти (memory contexts). Давайте же разберемся, что это такое, и чем помогает в работе с исключениями.

Контекст памяти — это объект, отвечающий за выделение и освобождение памяти. По сути, аллокатор. Но в отличие от простого аллокатора, у контекста памяти есть родительский контекст, а также дочерние контексты. Другими словами, контекстов много, и они объединяются в древовидную структуру. Корень дерева хранится в глобальной переменной TopMemoryContext. Кроме того, в любой момент времени существует текущий контекст, доступный через глобальную переменную CurrentMemoryContext.

Для выделения памяти в текущем контексте PostgreSQL предлагает функции palloc(), repalloc(), pfree(), а также некоторые другие. Они очень похожи на стандартные функции, такие как malloc(), realloc() и free():

Datum
experiment_palloc(PG_FUNCTION_ARGS)
{
    char *buffcopy, *fmtstr;
    char *mybuff = (char*)palloc(128); /* or palloc0() */
    snprintf(mybuff, 128, "test data");
    elog(NOTICE, "mybuff after palloc() = %s", mybuff);

    mybuff = repalloc(mybuff, 256);
    elog(NOTICE, "mybuff after repalloc() = %s", mybuff);

    /* similar to strdup() */
    buffcopy = pstrdup(mybuff);
    pfree(mybuff);

    elog(NOTICE, "byffcopy = %s", buffcopy);

    /* similar to sprintf() */
    fmtstr = psprintf("This is %s example", "psprintf()");
    elog(NOTICE, "fmtstr = %s", fmtstr);

    PG_RETURN_VOID();
}

Все доступные функции перечислены в src/include/utils/palloc.h. Можно заметить, что память, выделенная под buffcopy и fmtstr, не была освобождена. Подобный код часто встречается как в PostgreSQL, так и в его расширениях. Как будет объяснено далее по тексту, это не ошибка.

Функции из семейства palloc() имеют ряд важных отличий от функций семейства malloc():

  • palloc() никогда не возвращает NULL. В случае ошибки будет брошено исключение. За счет этого упрощается код, ведь в нем не нужно делать лишние проверки;
  • Выделение памяти размером ноль байт — это совершенно законная операция. При этом возвращается указатель, отличный от NULL;
  • Не допускается передавать NULL в pfree();

Характерно, что repalloc() и pfree() можно вызывать для памяти, выделенной в контексте, отличном от текущего. Действия с памятью всегда выполняются в контексте, в котором она была выделена изначально.

Рассмотрим еще один пример, на этот раз выводящий имя текущего контекста, а также его предков вплоть до TopMemoryContext:

Datum
experiment_ctxnames(PG_FUNCTION_ARGS)
{
    MemoryContext ctx = CurrentMemoryContext;

    while(ctx)
    {
        /* see src/include/nodes/memnodes.h, MemoryContextData */
        elog(NOTICE, "ctx->name = %s", ctx->name);
        ctx = ctx->parent;
    }

    PG_RETURN_VOID();
}

Код выводит:

NOTICE:  ctx->name = ExprContext
NOTICE:  ctx->name = ExecutorState
NOTICE:  ctx->name = MessageContext
NOTICE:  ctx->name = TopMemoryContext

При желании в исходном коде PostgreSQL можно найти точные места, где был создан каждый из этих контекстов.

Как же контексты памяти помогают при работе с исключениями? А очень просто. Если будет брошено исключение, PostgreSQL позаботится о том, чтобы освободить весь контекст памяти целиком. Более того, контексты памяти часто имеют короткое время жизни. Например, здесь мы видим, что хранимая процедура выполняется в контексте, выделенном для вычисления одного выражения запроса. Независимо от того, бросит ли процедура исключение или нет, вся выделенная ею память освободится еще до завершения транзакции.

Как результат, в коде расширений для PostgreSQL pfree() используется редко. Зачем вручную освобождать память маленькими кусками, если скоро будет удален весь контекст целиком? Освобождать память вручную имеет смысл только тогда, когда ее выделяется много, а контекст будет освобожден не скоро. Например, если память выделяется в цикле, и используется в рамках одной итерации.

Выше мы рассмотрели функции для выделения и освобождения памяти. Функции для работы с контекстами памяти следует искать в src/include/utils/memutils.h.

Рассмотрим их использование на таком примере:

static void
reset_callback(void* arg)
{
    elog(NOTICE, "reset_callback() called with arg = %s", (char*)arg);
}

Datum
experiment_memctx(PG_FUNCTION_ARGS)
{
    MemoryContextCallback* cb;
    Size buffsize, totalsize;

    MemoryContext myctx = AllocSetContextCreate(
        CurrentMemoryContext, "MemCtx", ALLOCSET_DEFAULT_SIZES);
    MemoryContext oldctx = MemoryContextSwitchTo(myctx);

    cb = (MemoryContextCallback*)palloc(sizeof(MemoryContextCallback));
    cb->func = reset_callback;
    cb->arg = pstrdup("memctx");
    MemoryContextRegisterResetCallback(myctx, cb);

    buffsize = GetMemoryChunkSpace(cb);
    totalsize = MemoryContextMemAllocated(myctx, true /* recursive */);
    elog(NOTICE, "Memory allocated for cb: %lld, sizeof(*cb) = %lld",
        (long long)buffsize, (long long)sizeof(*cb));
    elog(NOTICE, "Total memory allocated: %lld", (long long)totalsize);

    MemoryContextSwitchTo(oldctx);

    elog(NOTICE, "Calling MemoryContextDelete()...");
    MemoryContextDelete(myctx);
    elog(NOTICE, "Returning from experiment_memctx() ...");
    PG_RETURN_VOID();
}

Здесь мы создаем новый контекст с именем "MemCtx". Текущий контекст автоматически становится его предком в момент создания. PostgreSQL поддерживает разные типы контекстов памяти, оптимизированные под разные задачи. По-умолчанию рекомендуется использовать AllocSet, что мы здесь и делаем. Данный контекст выделяет память блоками по 8 Кбайт и удваивает количество блоков, когда памяти перестает хватать. Пока контекст не будет удален, или ему явно не будет сделан reset, он не вернет память операционной системе.

Далее мы делаем этот контекст текущим при помощи MemoryContextSwitchTo(). С помощью функции MemoryContextRegisterResetCallback() мы также вешаем колбэк на reset и/или удаление контекста. В колбэке можно освобождать дополнительные ресурсы, ассоциированные с объектами, память под которые выделяется заданным контекстом. Затем мы узнаем, сколько памяти было выделено под структуру cb с учетом накладных расходов на выравнивание, служебную информацию, и так далее, а также сколько памяти myctx и его дети (которых нет) забрали у операционной системы. В боевом коде это используется редко, но может быть полезно при отладке. Наконец, мы переключаем контекст на тот, что использовался раньше, и уничтожаем "MemCtx". При этом происходит вызов нашего колбэка.

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

Представленной информации должно быть достаточно в где-то в 90% случаев. Но если вы хотите знать абсолютно все о контекстах памяти, рекомендую прочитать src/backend/utils/mmgr/README, а также находящиеся в одном каталоге с ним .c файлы с реализацией контекстов памяти. Полную версию исходников к посту, как обычно, вы найдете на GitHub.

Метки: , , .