C ++ самый легкий способ отложить вызов функции - PullRequest
0 голосов
/ 07 апреля 2020

Я делаю облегченные кооперативные потоки для виртуальной машины C ++, и хотя потоки работают просто отлично, стоит отложить вызов std :: function:

template <typename T, typename... Args>
inline Thread* create(const T& func, Args&&... args)
{
...
    auto* thread = new (xxx) Thread(
        [func, args...] () {
            self()->exit( func(args...) );
        });

Приведенная выше функция будет хранить std :: function с аргументами в другой std :: function, которая позволяет отложить вызов до завершения системного вызова клона, и мы запускаем (среди прочего) новый стек.

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

API подобен обычным потокам C ++: вы передаете функцию, а затем аргументы. Есть ли способ сделать отсроченный звонок дешевле? Какие варианты есть? Я использую все последние компиляторы (Clang-11, G CC -9.1) с включенным C ++ 17.

Код доступен здесь: https://github.com/fwsGonzo/libriscv/blob/master/binaries/barebones/libc/microthread.hpp#L95

Я думаю, что должна быть полная копия аргументов. Когда вы создаете новый поток и немедленно вызываете его, параметры все еще доступны, потому что родительский поток все еще там ожидает. Тем не менее, можно go вернуться к основному потоку и оставить функцию, которая делает параметры go вне области действия, в то время как новый поток получен. Не уверен, как решить эту проблему, кроме как скопировать / переместить аргументы.

Давайте сравним микропотоки с Lua сопрограммами. Без аргументов:

libriscv: micro threads => median 529ns  lowest: 518ns  highest: 586ns
lua5.3: coroutines => median 687ns  lowest: 667ns  highest: 727ns

С аргументами:

libriscv: micro thread args => median 2261ns  lowest: 2181ns  highest: 2725ns
lua5.3: coroutine args => median 762ns  lowest: 735ns  highest: 1051ns

Захват по ссылке:

libriscv: micro thread args => median 643ns  lowest: 612ns  highest: 1319ns
lua5.3: coroutine args => median 748ns  lowest: 718ns  highest: 842ns

Передача аргументов в микропоток приводит к выделению кучи и полностью разрушает производительность против Lua сопрограмм. Теперь распределения в libriscv не такие медленные, но их нельзя превзойти, просто вставив в стек.

1 Ответ

0 голосов
/ 10 апреля 2020

Я решил эту проблему, сохранив аргументы в кортеже в стеке новых потоков, например:

// store arguments on stack
char* args_addr = stack_bot + sizeof(Thread);
auto* tuple = new (args_addr) std::tuple<Args&&...>{std::move(args)...};

// store the thread at the beginning of the stack
Thread* thread = new (stack_bot) Thread(
[func, tuple] ()
{
    if constexpr (std::is_same_v<void, decltype(func(args...))>)
    {
        std::apply(func, std::move(*tuple));
        self()->exit(0);
    } else {
        self()->exit( std::apply(func, std::move(*tuple)) );
    }
});

C ++ действительно иногда имеет инструмент для всего. Надеюсь, это поможет будущему кому-то. Это значительно улучшило производительность микропотоков, и я выиграл у Lua сопрограмм, как с аргументами, так и без:

libriscv: micro threads => median 564ns  lowest: 516ns  highest: 807ns
lua5.3: coroutines => median 737ns  lowest: 696ns  highest: 857ns
libriscv: micro thread args => median 796ns  lowest: 758ns  highest: 874ns
lua5.3: coroutine args => median 803ns  lowest: 770ns  highest: 866ns

Приветствия.

...