Разрешение функции Dynami c во время выполнения - PullRequest
3 голосов
/ 01 февраля 2020

Мой проект должен загружать много модулей во время выполнения, и каждый из них содержит много функций в форме, подобной приведенному ниже псевдокоду:

void someFunction(Context &ctx) {
    bool result;
    result = ctx.call("someFunction2")(ctx.arg["arg1"], ctx.arg["arg2"])
             && ctx.call("someFunction3")(ctx.arg["arg1"], ctx.arg["arg3"]);
    ctx.result(result);
} 

, где ctx.arg["arg1"], ctx.arg["arg2"], ctx.arg["arg3"] аргументы, переданные someFunction во время выполнения. someFunction2 и someFunction3 не могут быть статически разрешены во время компиляции, но будут известны (были ли они определены в других модулях) во время выполнения, когда все модули загружены.

Теперь наивная реализация будет использовать карту ha sh для хранения дескриптора функции для всех этих функций, но хеширование будет медленным, так как обычно для поиска требуется 10 000 функций, и каждая функция будет вызываться много раз в других функциях (например: перечисляются аргументы для поиска правильной комбинации, которая даст желаемый результат).

Поэтому я ищу какое-то решение, которое выполнит однократную замену этих «ctx.call», когда все модули загружены, а не выполнит «ha sh -and-probe». " каждый раз. В настоящее время основной проблемой является действие «замена». Я выдвинул некоторые идеи, но они не идеальны:


1-е решение: создайте внутреннюю функцию inner_func(func_handle1, func_handle2, arg1, arg2, arg3) и используйте std::bind для создания внешней оболочки outer_wrapper().

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


2-е решение: использовать метапрограммирование + constexpr + макросы для автоматического подсчета ссылок на имена функций и аргументов, затем создайте справочную таблицу, а затем пусть контекст заполнит каждую таблицу во время выполнения.

проблема: я не могу разобраться, нужна помощь. Я читал документы библиотеки Fatal из facebook, mpl и hana из boost, но, похоже, нет чистого способа сделать это.


3-е решение: использовать JIT-компилятор

проблема: выбор JIT-компилятора c ++ ограничен. NativeJIT не достаточно мощный, простой :: JIT, кажется, не настраивается и его нелегко распространять. asmjit не может использоваться.


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

(and (at ?p ?c1)
(aircraft ?a)
(at ?a ?c3)
(different ?c1 ?c3))

PPS: более конкретно, я думаю о что-то похожее на это:

Пользователь определит функцию, похожую на эту:

void module_init() {
    FUNC ("someFunction")("p", "a", "c1", "c3") (
        bool result;
        result = CALL("at")("p", "c1") 
                 && CALL("aircraft")("a")
                 && CALL("at")("a", "c3")
                 && CALL("different")("c1", "c3")

        /// Users should also be able to access arguments as a "Variable" 
        /// class using ARG["p"]
        return result;
    )
}

Затем, каким-то образом, FUNC() будет преобразован в функтор, подобный:

struct func_someFunction {
    some_vector<std::function<bool()>> functions;
    some_vector<Variable*> args;
    some_vector<std::string> func_name;
    some_vector<std::string> arg_name;

    bool operator()() {
       /// above representation of Func(), but function and args are pointers in "functions" and "args"
    }
}

Затем, когда все модули загружены, система будет читать func_name и arg_name и заполнять соответствующие функциональные указатели и переменные указатели в functions и args соответственно.

Статус: сначала используя hashmap, я буду публиковать обновления после завершения.

Статус: сам нашел решение, также протестировал Ха sh реализация, опубликовано ниже.

Любая идея будет оценена. Спасибо!

Ответы [ 4 ]

5 голосов
/ 01 февраля 2020

Теперь наивная реализация будет использовать карту ha sh для хранения дескриптора функции для всех этих функций, но хеширование будет медленным, так как обычно для поиска [...] используются 10k функций.

Ха sh таблицы O (1) стоимость для поиска. Вы пробовали это широко используемое решение этой проблемы и провели анализ производительности? Вы пытались использовать разные алгоритмы хеширования, чтобы уменьшить время хеширования и коллизии?

0 голосов
/ 02 февраля 2020

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

Демонстрация с использованием таблицы ha sh и указателя соответственно

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

  1. Для метода отображения ha sh можно использовать интернирование строки , чтобы уменьшить накладные расходы на конструкцию строки, как предложено Konrad Rudolph и cmaster - восстанавливает monica , это приведет к снижению производительности в среднем (примерно на 50% по сравнению с указателями), но устраняет накладные расходы на динамическое создание c и уменьшает потребление памяти. boost::flyweight - хороший вариант.

  2. Для метода карты ha sh я только что реализовал демонстрацию, используя std::unordered_map, но существуют лучшие заменители, включая google::dense_hash_map, tsl::hop_scotch_map и такие, их стоит попробовать, но согласно эталону Тесиля , я сомневаюсь в их O (s) (где s - средняя длина строки) временная сложность для каждого отдельного поиска может быть быстрее, чем доступ к указателю O (1).

  3. В моем сценарии все функции могут быть однако, найденный после этапа загрузки модуля, вы можете захотеть охватить такой сценарий, как поиск символов в python, тогда hashmap будет лучше, если вы не введете больше ограничений для вашего senario или не обновите разрешенные указатели периодически. Tr ie структура данных может быть хорошим вариантом, если вы вставляете и удаляете объекты в большом масштабе.

Достаточно болтовни, вот результаты и решения :


Производительность

Тест: 1,28e8 возможных комбинаций для смешанных логических и числовых значений c SAT

Платформа: i7 6700HQ, однопоточная

cmake-build-debug/test_ptr  0.70s user 0.00s system 99% cpu 0.697 total
cmake-build-debug/test_hash  4.24s user 0.00s system 99% cpu 4.241 total

Точка доступа и время выполнения функции от perf:

test_ptr:

  53.17%  test_ptr  test_ptr       [.] main
  35.38%  test_ptr  test_ptr       [.] module_1_init(Domain&)::__internal_func_some_circuit::operator()
   8.02%  test_ptr  test_ptr       [.] module_2_init(Domain&)::__internal_func_and_circuit::operator()
   1.90%  test_ptr  test_ptr       [.] module_2_init(Domain&)::__internal_func_or_circuit::operator()
   0.18%  test_ptr  libc-2.23.so   [.] _int_malloc
   0.15%  test_ptr  ld-2.23.so     [.] do_lookup_x
   0.15%  test_ptr  test_ptr       [.] module_2_init(Domain&)::__internal_func_xor_circuit::operator()

test_ha sh:

  33.11%  test_hash  test_hash            [.] Domain::call<char const (&) [11], Domain::Variable&, Domain::Variable&>
  25.37%  test_hash  test_hash            [.] main
  21.46%  test_hash  libstdc++.so.6.0.26  [.] std::_Hash_bytes
   5.10%  test_hash  libc-2.23.so         [.] __memcmp_sse4_1
   4.64%  test_hash  test_hash            [.] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char const*>
   3.41%  test_hash  test_hash            [.] module_1_init(Domain&)::__internal_func_some_circuit::operator()
   1.86%  test_hash  libc-2.23.so         [.] strlen
   1.44%  test_hash  test_hash            [.] module_2_init(Domain&)::__internal_func_and_circuit::operator()
   1.39%  test_hash  libc-2.23.so         [.] __memcpy_avx_unaligned
   0.55%  test_hash  test_hash            [.] std::_Hash_bytes@plt

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


Решение

Макросы интенсивно используются для упрощения определения функций (предикатов) для пользователей:

in test_ptr:

void module_1_init(Domain &d) {
    FUNC(some_circuit, d,
         DEP(and_circuit, or_circuit, xor_circuit, not_circuit),
         ARG(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10),
         BODY(
             return CALL(and_circuit, a1, a2)
                 && CALL(or_circuit, a3, a4)
                 && CALL(xor_circuit, a5, a6)
                 && CALL(not_circuit, a7)
                 && a8.value >= R1 && a9.value >= R2 && a10.value >= R3;
         )
    );
}
in test_hash:

void module_1_init(Domain &d) {
    FUNC(some_circuit, d,\
         ARG(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), \
         BODY(
             return CALL(and_circuit, a1, a2)
                 && CALL(or_circuit, a3, a4)
                 && CALL(xor_circuit, a5, a6)
                 && CALL(not_circuit, a7)
                 && a8.value >= R1 && a9.value >= R2 && a10.value >= R3;
         )
    );
}

Основным отличием является макрос DEP() в решении указателя, DEP() будет явно указывать зависимые функции, и будет построена таблица указателей локальной функции.

Вот фактический полученный код после расширения макроса:

in test_ptr:

void module_1_init(Domain &d) {
    class __internal_func_some_circuit : public Domain::Function { 
    public: 
        enum func_dep_idx { 
            and_circuit, 
            or_circuit, 
            xor_circuit, 
            not_circuit, 
            __func_dep_idx_end }; 
    Domain::Variable a1; 
    Domain::Variable a2;
    ...
    Domain::Variable a10; 
    explicit __internal_func_some_circuit(Domain &d) : 
    a1(), a2(), a3(), a4(), a5(), a6(), a7(), a8(), a9(), a10(),
    Domain::Function(d) { 
        arg_map = {{"a1", &a1}, {"a2", &a2}, {"a3", &a3} ..., {"a10", &a10}}; 
        arg_pack = { &a1, &a2, &a3, &a4, &a5, &a6, &a7, &a8, &a9, &a10}; 
        func_dep_map = {{"and_circuit", func_dep_idx::and_circuit}, 
                        {"or_circuit", func_dep_idx::or_circuit},
                        {"xor_circuit", func_dep_idx::xor_circuit} , 
                        {"not_circuit", func_dep_idx::not_circuit}}; 
        func_dep.resize(__func_dep_idx_end); 
    } 

    bool operator()() override { 
        return func_dep[func_dep_idx::and_circuit]->call(a1, a2) && 
               func_dep[func_dep_idx::or_circuit]->call(a3, a4) && 
               func_dep[func_dep_idx::xor_circuit]->call(a5, a6) && 
               func_dep[func_dep_idx::not_circuit]->call(a7) && 
               a8.value >= 100 && a9.value >= 100 && a10.value >= 100; 
    } 
}; 
d.registerFunction("some_circuit", new __internal_func_some_circuit(d))
in test_hash:

class __internal_func_some_circuit : public Domain::Function { 
public: 
    Domain::Variable a1; 
    Domain::Variable a2; 
    ...
    Domain::Variable a10; 
    explicit __internal_func_some_circuit(Domain &d) : 
    a1() , a2(), a3(), a4(), a5(), a6(), a7(), a8(), a9(), a10(), 
    Domain::Function(d) { 
        arg_map = {{"a1", &a1}, {"a2", &a2} ..., {"a10", &a10}}; 
        arg_pack = {&a1, &a2, &a3, &a4, &a5, &a6, &a7, &a8, &a9, &a10}; 
    } 

    bool operator()() override { 
        return domain.call("and_circuit", a1, a2) && 
               domain.call("or_circuit", a3, a4) && 
               domain.call("xor_circuit", a5, a6) && 
               domain.call("not_circuit", a7) && 
               a8.value >= 100 && a9.value >= 100 && a10.value >= 100; } 
}; 
d.registerFunction("some_circuit", new __internal_func_some_circuit(d))

По сути, решение с указателем создает таблицу поиска функции func_dep_map, которая позже будет использоваться классом Domain для поиска других функций, зависящих от этой функции, и вектор указателя на функцию func_dep, который будет заполнен их указателями.

enum используется для предоставления элегантного и компактного способа поиска индексов, вместо использования классов карт, предоставляемых библиотеками метапрограммирования, такими как Fatal и boost :: mpl, они не удобны для использования в этом случае .

Эта реализация сильно зависит от boost :: preprocessor, чтобы узнать больше, обратитесь к моему репозиторию github.

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

Это может быть излишним, но может быть полезной идеей:

  1. Используйте интернирование строк, чтобы гарантировать, что каждый MyString("aircraft") приводит к одному и тому же объекту. Конечно, это означает, что ваши строки должны быть неизменяемыми.

  2. Свяжите каждый созданный строковый объект с высококачественным случайным числом (uint64_t) и используйте его как "ha sh" этой строки .

Поскольку "ha sh" хранится вместе со строкой, это простая загрузка памяти для ее "вычисления" , И поскольку вы используете хороший PRNG для генерации этого "ha sh", он прекрасно работает как ключ в таблице ha sh.

Вам все еще нужно вычислить классическое ha sh, чтобы найти MyString объект в таблице существующих строковых объектов всякий раз, когда std::string преобразуется в ваш интернированный строковый объект, но это одноразовое усилие, которое может быть выполнено, когда ваши файлы конфигурации обрабатываются лексером или когда ваши модули загружены. Фактическое соответствие строк их соответствующим реализациям функций и т. Д. c. будет отделен от расчета классических хешей.

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

Если вам нужно постоянно находить правильную функцию для запуска на основе строковых ключей времени выполнения в течение всего жизненного цикла программы, то нет никакого способа использовать карту ha sh. (Ответ Павла)

Но если вы инициализируете список функций во время выполнения, который не изменяется в течение продолжительности программы (т. Е. Вам не нужно выполнять какую-либо операцию «найти» после начального этапа), то вы могли бы поместите эти функции в непрерывный контейнер (например, std::vector) для улучшения времени доступа и использования кэша:

// getFuncNames is where you are deciding on the list of functions to run
// funcs is a vector of function handles
// funcMap is a hash map of function names to function handles
for (auto& funcName : getFuncNames())
{
    funcs.push_back(funcMap.at(funcName));
}
...