这个问题源于 rust 的一个 issue: Async fn doubles argument size

考虑下面的代码,可能会产生一下几个问题:

  • async fn 生成的对象内存如何布局?

  • 为什么 fut1 和 fut2 的大小不一样?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async fn wait() {}

async fn foo(arg: [u8; 10]) {
    wait().await;
    drop(arg);
}

fn main() {
    let fut1 = async {
        let arg = [0u8; 10];
        wait().await;
        drop(arg);
    };
    let fut2 = foo([0u8; 10]);
    println!("{}, {}", std::mem::size_of_val(&fut1), std::mem::size_of_val(&fut2));
}
12, 22

async fn 对象内存布局

每个 async 函数被编译器实现为一个 generator,每个 await 对应于一个 yield ,每个 yield 点需要保存当前必要的上下文用于后续恢复执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let xs = vec![1, 2, 3];
let mut gen = || {
let mut sum = 0;
    for x in xs.iter() {  // iter0
        sum += x;
        yield sum;  // Suspend0
    }
    for x in xs.iter().rev() {  // iter1
        sum -= x;
        yield sum;  // Suspend1
    }
};

//  maybe?
enum SumGenerator {
    Unresumed { xs: Vec<i32> },
    Suspend0 { xs: Vec<i32>, iter0: Iter<'self, i32>, sum: i32 },
    Suspend1 { xs: Vec<i32>, iter1: Iter<'self, i32>, sum: i32 },
    Returned
}

rust 的 enum 为 tag enum,理论上内存占用为 :tag + 最大内存占用 variant + 对齐。

对 async 函数,执行到一个新的 await 点时,即转变为另一个 variant,由于 enum 复用一块内存,内存被重新布局,可能发生 move。

但 future 对象可能包含自引用,move 后,内存安全将不被保证。所以 async 函数的每一个 variant,都有自己的内存,也就导致了最开始的问题。

实际上,生成器的对象布局是编译器内部行为,一般只是以 Enum 的方式来表述状态机,在实现上,并不一定是 Enum 的方式。

fut1 和 fut2 的区别

async 函数生成的代码,将参数作为了 Unresumed 的一个状态值保存了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
enum Fut1Generator {
    Unresumed,
    Suspend0 { arg: [u8; 10] },
    Returned
}

enum Fut2Generator {
    Unresumed { arg: [u8; 10] }, // future delay 执行者, arg 最为函数参数被捕获
    Suspend0 { arg: [u8; 10] },
    Returned
}

reference

https://tmandry.gitlab.io/blog/posts/optimizing-await-2/

介绍了 rust 的一些生成器内存布局的优化,但只介绍了 local 变量的优化