vak: (Default)
Serge Vakulenko ([personal profile] vak) wrote2021-06-21 06:15 pm

48 байт на одну сопрограмму

Судя по комментам к предыдущему посту, существует устойчивое подозрение, что сопрограммы имеют стек, и вообще кушают ресурсы. Хорошо, давайте померяем, это нетрудно. Создадим миллион сопрограмм и глянем расход памяти. Вот такой тестик.
co_void_t print(int n)
{
for (;;) {
std::cout << n << std::endl;
co_await co_await_t{};
}
}

int main(int argc, char **argv)
{
std::coroutine_handle<> continuation[1000000];

std::cout << "Using " << mallinfo().uordblks << " bytes after start.\n";

for (int i = 0; i < 1000000; i++)
continuation[i] = print(i);

std::cout << "Using " << mallinfo().uordblks << " bytes after coroutine allocation.\n";

for (int i = 0; i < 1000000; i++)
continuation[i].resume();

std::cout << "Using " << mallinfo().uordblks << " bytes after coroutine run.\n";

for (int i = 0; i < 1000000; i++)
continuation[i].destroy();

std::cout << "Using " << mallinfo().uordblks << " bytes after coroutine destroyed.\n";
return 0;
}
Результат:
Using 77488 bytes after start.
Using 48077488 bytes after coroutine allocation.
0
...
999999
Using 48077488 bytes after coroutine run.
Using 77824 bytes after coroutine destroyed.
Делим разницу на количество сопрограмм: (48077488 - 77488) / 1000000 = 48 байтов на одну сопрограмму. Вполне недорогое удовольствие получается. Конечно, никакого собственного стека у сопрограмм не имеется, просто негде.
sab123: (Default)

[personal profile] sab123 2021-06-22 02:55 am (UTC)(link)
Стек сопрограмм выражается в виде контекстов, которые они аллокируют(как это по-русски?) из кучи. Это, кстати, и к вопросу об отладке: чтобы понять стек вызова, надо раскручивать цепочку этих контекстов.

В результате выгода в том, что пока сопрограмма активно не выполняется, ей не надо _пустое_место_ на стеке. И синхронные функции выполняются с той же эффективностью как обычно. А вот асинхронные функции (сопрограммы) имеют дополнительные накладные расходы по динамическому распределению памяти для стековых фреймов.
ircicq: (Default)

[personal profile] ircicq 2021-06-22 03:50 am (UTC)(link)
Стек - это прежде всего структура вида LIFO.
А как она реализована: сплошной массив у Threads, или список фреймов в Heap у coroutines - частности.

В любом случае где-то хранится цепь continuations вместо цепочки return addresses.


sab123: (Default)

[personal profile] sab123 2021-06-22 05:13 am (UTC)(link)
Не копируется. На стек (или скорее даже в регистр, как аргумент вызова) кладется указатель на контекст. Точно так же, как и в лямбде. На стеке резервируются только краткоживущие локальные переменные.
sab123: (Default)

[personal profile] sab123 2021-06-22 04:31 am (UTC)(link)
Для ограниченного определения слова "работает". Внутри, конечно, асинхронная функция делится на несколько синхронных, каждая из которых ответственна за кусок от одного ожидания до другого. И вот эти синхронные функции выполняются на стеке, там же выделяют краткоживущие колакльные переменные, и вызывают другие синхронные функции там же. Но аналог полноценного стекового фрейма - это контекст.
sab123: (Default)

[personal profile] sab123 2021-06-22 05:27 am (UTC)(link)
Это одно и то же. Сопрограммы, конечно, разные бывают (вот когда-то еще в детстве я читал про них на Форте, user-space threads - тоже они, и я как-то даже делал свою реализацию), но вот конкретно такие, которые имеются в виду в С++ и Расте - функции с интерфейсом async/await.

Еще один вариант их представить - это несколько лямбд с общим контекстом. Запускается первая лямбда, она работает-работает пока не доходит до слова await. На этом месте она присоединяет к ожидаемому фьючеру лямбду-продолжение и возвращается. Когда фьючер будет готов, шедулер вызовет лямбду-продолжение и все продолжится. Когда же лямбда встречает слово return, она не просто так возвращается, а сначала сигналит в привязанный к контексту промиз/фьючер и освобождает контекст (выдывая деструкторы для всех объектов в нем).

Тут есть интересный момент: что если мы вызываем другую функцию, которая может внутри себя сделать await? Тогда та функция вернется в нашу, когда ее значение еще не готово. Ответ такой, что так делать не разрешается. Нельзя напрямую вызвать другую функцию, которая будет делать await, потому что та функция в таком разе обязана быть асинхронной. То есть, ее вызов транслируется в создание контекста и промиза/фьючера вызываемой асинхронной функции, привязывание к этому фьючеру своей лямбды-продолжения, шедуленье первой лямбды от новой функции, и возвращение из текущей лямбды.