Максимально возможная производительность для функции C в PostgreSQL? - PullRequest
0 голосов
/ 21 января 2019

В настоящее время я пытаюсь реализовать функцию в Postgres, которая будет вычислять косинусное расстояние между 2 реальными массивами ( real [] ).Массивы / векторы имеют длину 2000 элементов .

Я буду использовать эту функцию для поиска от 1 до n по 500.000 векторов (пока).

Я пытаюсь добиться наилучшей производительности, не задумываясь о том, чтобы выбросить оборудование / процессоры на сервер.

У меня уже есть успешное решение за пределами Postgres.Я кеширую данные в память и там я могу выполнить поиск косинуса под 1 с (используя ядро ​​dotnet).Но чтобы подготовить это производство, требуется много времени на разработку.Прежде чем углубиться в это, я хочу убедиться, что я исчерпал все опции Postgres (Postgres уже используется во многих наших микро сервисах).

Ниже приведены варианты, которые я тестировал, и мои результаты:

1) функция plpgsql (Postgres 10.3)

Это был большой сбой - потребовалось 5 минут для поиска 500 000 строк - с распараллеливанием (2 рабочих).

2) c функцией с Postgres 10,3

Огромное улучшение - заняло 10 секунд , включая 2 рабочих распараллеливания

Источник

#include "postgres.h"
#include "fmgr.h"
#include "math.h"
#include <utils/array.h>

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(cosine_distance_vector);

Datum cosine_distance_vector(PG_FUNCTION_ARGS)
{
    ArrayType *input1ValuesArray, *input2ValuesArray;
    float4 *input1Values, *input2Values;
    float4 result;
    float4 dot = 0.0;
    float4 denom_a = 0.0;
    float4 denom_b = 0.0;

    input1ValuesArray = PG_GETARG_ARRAYTYPE_P(0);
    input2ValuesArray = PG_GETARG_ARRAYTYPE_P(1);

    input1Values = (float4 *) ARR_DATA_PTR(input1ValuesArray);
    input2Values = (float4 *) ARR_DATA_PTR(input2ValuesArray);

    for(unsigned int i = 0u; i < sizeof(input1Values); ++i) {
        dot += input1Values[i] * input2Values[i] ;
        denom_a += input1Values[i] * input1Values[i] ;
        denom_b += input2Values[i] * input2Values[i] ;
    }

    result = dot / (sqrt(denom_a) * sqrt(denom_b));
    PG_RETURN_FLOAT4(result);
}

3) c функцией с Postgres 11.1

Еще одно улучшение - заняло 9 секунд, включая распараллеливание 2 рабочих

Мои наблюдения оФункция C

Насколько я вижу, % 90 времени уходит на вызовы PG_GETARG_ARRAYTYPE_P ;

Я проверил это, сравнив 2 реализации

Реализация 1 заняла 9 секунд , чтобы завершить поиск=>

Datum cosine_distance_vector(PG_FUNCTION_ARGS)
{
    ArrayType *input1ValuesArray, *input2ValuesArray;

    float4 result = 0.0;

    input1ValuesArray = PG_GETARG_ARRAYTYPE_P(0);
    input2ValuesArray = PG_GETARG_ARRAYTYPE_P(1);

    PG_RETURN_FLOAT4(result);
}

Внедрение 2 заняло 1,5 секунды для выполнения

Datum cosine_distance_vector(PG_FUNCTION_ARGS)
{
    float4 result = 0.0;

    PG_RETURN_FLOAT4(result);
}

Существует ли более быстрый или более конкретный способ ввода массивов / указателей Float4 вфункция вместо того, чтобы использовать универсальную функцию PG_GETARG_ARRAYTYPE_P?

Я также пытался реализовать эту функцию, используя Соглашение о вызовах версии 0 в Postgres 9.6 (10 и 11 не поддерживают их), так как это кажется болееэффективный (низкий уровень).Но я не смог успешно реализовать эту функцию.Даже примеры в документации Postgres вызывали ошибки сегментации.

Я буду использовать отдельную установку Dockerized Postgres для этой функции поиска, так что я открыт для любой версии postgres и любого типа трюка конфигурации.

Некоторая дополнительная информация на основе комментариев @ LaurenzAlbe.

Это SQL-запрос, который я использую, чтобы найти лучший результат:

SELECT 
    * 
FROM 
    documents t 
ORDER BY cosine_distance_vector(
    t.vector, 
    ARRAY [1,1,1,....]::real[]) DESC
LIMIT 1

Массив огромен, поэтому я не вставил его полностью.

Вот результат EXPLAIN (ANALYZE, BUFFERS, VERBOSE):

Explain Screen Shot

2019-01-23 Прогресс

Я немного углубился в исходный код Postgres и сосредоточился на том, почему функция косинуса работала медленнее, когда функция PG_GETARG_ARRAYTYPE_P былавызывается.

Итак, я сталкивался с вызовом этой функции в какой-то момент в fmgr.c:

struct varlena *
pg_detoast_datum(struct varlena *datum)
{
    if (VARATT_IS_EXTENDED(datum))
        return heap_tuple_untoast_attr(datum);
    else
        return datum;
}

Если тип хранения вашего столбца расширенный , который добавляетзначительное превышение.

Размер строки таблицы векторов в общей сложности составил более 8192 байта, что по умолчанию является размером блока Postgres.Вот почему тип хранения векторного столбца был автоматически выбран как EXTENDED .Я попытался преобразовать его в PLAIN и безо всякой ошибки это сработало.Я попытался выполнить запрос и 500ms !

Но, поскольку мой размер строки теперь превышал 8192 (хотя тип хранения был успешно преобразован PLAIN), я не смог добавить новые строки в таблицубольше на INSERT стали жаловаться, что размер строки слишком большой.

Следующим шагом я скомпилировал postgres с 16KB blockize (у меня ушло некоторое время).В конце я смог создать идеальную таблицу с векторным хранилищем PLAIN с работающими INSERT.

Я проверил запрос с шагом 100К строк. Первые строки 100K, для запуска потребовалось 50 мс . На 200К строк это заняло 4 секунды ! - Теперь, я думаю, из-за размера блока 16K мне нужно найти баланс префекта .conf file settings . Я больше не могу оптимизировать функцию.

1 Ответ

0 голосов
/ 01 февраля 2019

Я наконец-то пришел к выводу и смог достичь скорости менее секунды. Но это было невозможно без игры с настройками памяти.

Это сделало возможным:

1. Размер блока => Наличие одной строки таблицы, помещаемой в один блок, является огромным преимуществом перед выполнением. В моем случае это означало создание postgres из источника с размером блока 16 КБ.

2. C Function => C Функция по меньшей мере в 50 раз быстрее, чем функции SQL, по понятным причинам. Но комбинация функции C с правильными размерами блоков делает еще большую разницу.

3. Исправьте postgresql.config params => Я использовал этот инструмент для настройки своих начальных параметров https://pgtune.leopard.in.ua/#/ - мне очень помог (не принадлежит мне или чему-то еще, нашел его в другом вопросе SO, который я не могу найти больше)

4. Особое внимание к параметру work_mem => Очевидно, что параметр work_mem очень важен для некоторых агрегатов, таких как MIN (который я использую), поэтому я значительно увеличил его по сравнению с предложениями на веб-сайте на шаге 3.

5. Особая забота о параметреffective_cache_size => Это реальная сделка, которая оказала наибольшее влияние. Опять же, по понятным причинам, размещение всех данных в памяти - это максимальный выигрыш в скорости. Итак, я выбрал это число: 500.000 * 16K (размер блока) => 8 ГБ + некоторый буфер. После выполнения запроса в третий раз я получил скорость 500 мс.

6. ОЗУ сервера => Да, часть, где мне пришлось обманывать. В моем случае идеальный объем оперативной памяти был, естественно, счетчик строк * размер блока и резервная копия с правильно установленным параметром ffective_cache_size (шаг 5).

Итак, это была комбинация вещей, которые я должен был сделать, чтобы поразить мою цель.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...