Почему использование асинхронного замыкания внутри другого асинхронного замыкания приводит к увеличению потребления памяти в два раза?

Я разрабатываю свой асинхронный движок и стремлюсь создать более удобный интерфейс для пользователей. Для этого мне необходимо расширить его функциональность, добавив поддержку Future. В частности, я реализую следующий подход: <br/> <pre><code>let mut res = MaybeUninit::uninit();
let res_ptr: *mut T = res.as_mut_ptr();
self.exec_future(async move {
     unsafe { res_ptr.write(future.await) };
});
unsafe { res.assume_init() }</code></pre> <br/> <br/> После реализации я заметил, что размер создаваемого Future увеличивается вдвое, что меня озадачило. Я провел несколько экспериментов, но не смог найти причину этого явления. <br/> <br/> Ниже представлен код для иллюстрации проблемы (его выполнение не обязательно, так как все важные моменты прокомментированы): <br/> <pre><code>let closure = async {
            let a = [0u8;8192];
            yield_now().await;
            black_box(println!("{}", a.len()));
            0
        };

        #[inline(never)]
        async fn a() -> usize {
            let a = [0u8;8192];
            yield_now().await;
            black_box(println!("{}", a.len()));
            0
        }

        let size_of_future_from_closure = mem::size_of_val(&closure);
        println!("size_of_future_from_closure: {}", size_of_future_from_closure); // 8194
        let size_of_future_from_fn = mem::size_of_val(&a());
        println!("size_of_future_from_fn: {}", size_of_future_from_fn); // 8194

        let closure_from_future_from_closure = async {
            closure.await
        };
        let size_of_closure_from_future_from_closure = mem::size_of_val(&closure_from_future_from_closure);
        println!("size_of_closure_from_future_from_closure: {}", size_of_closure_from_future_from_closure); // 16389

        let</code>
  • 28 августа 2024 г. 19:44
Ответы на вопрос 2
В вашем коде вы создаете асинхронные замыкания и функции, которые имеют одинаковый размер, но когда вы помещаете одно асинхронное замыкание внутрь другого, размер результата увеличивается. Это связано с тем, как компилятор Rust обрабатывает асинхронные функции и замыкания.

### Причины увеличения размера

1. **Избыточные поля данных**: Каждое асинхронное замыкание или функция генерирует структуру с состоянием (state machine), состоящую из всех переменных, которые оно захватывает. Если у вас есть одно асинхронное замыкание, оно включает массив `a` из 8192 байт. Когда вы вызываете это замыкание в другом асинхронном контексте, поскольку это замыкание имеет свое собственное состояние, то создается новая структура с новым состоянием, которая включает оригинальный массив.

2. **Обертки и дополнительная информация**: Каждое асинхронное замыкание оборачивается в структуру, которая также содержит информацию о состоянии выполнения, такие как `Poll`, а также может включать дополнительные поля для управления жизненным циклом будущего (future). Это лишние накладные расходы, которые добавляются каждый раз, когда вы создаете новое асинхронное контекст.

3. **Комплексные состояния**: Когда вы создаете асинхронное замыкание, которое содержит вызов другого асинхронного замыкания, суммарное состояние (из обоих замыканий) должно помещаться в единой структуре. Это приводит к тому, что памяти требуется больше, потому что каждая из этих структур должна хранить собственные данные, потенциально дублируя содержимое, если они не могут быть оптимизированы или переиспользованы.

### Как уменьшить потребление памяти

- **Избегайте вложенности**: Если возможно, избегайте создания асинхронных замыканий внутри других. Вместо этого попробуйте разделить ваш код таким образом, чтобы у вас было прямое выполнение функций без создания новых замыканий.

- **Используйте `async fn`**: Использование `async fn` вместо замыканий может помочь в упрощении конструкций и уменьшении количества генерируемых структур, так как каждая асинхронная функция передает свои данные обратно в вызывающий контекст, не создавая дополнительных оберток.

- **Учитывайте использование `Box`**: Если состояние вашего будущего достаточно велико и сложно, возможно, стоит рассмотреть возможность упаковки его в `Box`, чтобы использовать динамическое выделение памяти и избежать ненужного увеличения размера структуры.

Надеюсь, это объясняет причины увеличения размера будущего и даст некоторые идеи о том, как с этим справиться.
Я поднял этот вопрос на официальном форуме. Мне ответили, что пока что в async Rust есть несколько мест, в которых память используется неэффективно. Это будет исправлено в скором времени.
Похожие вопросы