How Rust's async/future model avoids intermediate allocations

I feel like this might be already asked as it is directly from Async book. Apologies if it is, but I am not able to find the answer.
The async book mentions:

This model of Futures allows for composing together multiple asynchronous operations without needing intermediate allocations. 

I am not sure I understand what intermediate allocations this refers to. Can someone please shed some light on this, and if possible share an example alternative model for comparison to show how Rust's model avoid allocations.

In a model like JavaScript Promises, every Promise can be used independently, so every Promise exists as a separate object. async/await syntax is only a sugar on top of Promise, so it behaves the same.

some_promise() // allocates a Promise object
  .then(() => {}) // allocates *another* Promise object to return
  .catch(() => {}) // allocates a *third* Promise object to return

Each Promise has to exists on its own, because a user could do something like:

let tmp = some_promise();
tmp.then(one);
tmp.then(two);
tmp.then(three);
return tmp;

So each Promise not only needs to exist as an object, but also needs to keep track for an array of possible callbacks listening on it.

In Rust however, Future has a single owner, so this won't compile:

let tmp = some_future();
tmp.then(one); // this is fine so far, but
tmp.then(two); // ERROR, tmp used after a move

When each Future has only a single owner, it doesn't need to support an arbitrary number of callback listeners.

And then because it has a single owner, it can be merged with its owner. So as you build .then().then().then() chain, instead of creating additional Future objects at each step, you're just wrapping one Future object in more steps it handles. All of this is known statically, so it doesn't require dynamic allocation (at least not until you use recursion).

2 Likes

This blog post explains some of the design choices that were made to allow async without a separate heap allocation for each future:

https://aturon.github.io/blog/2016/09/07/futures-design/

1 Like

Thanks for sharing. This makes sense, and I can see how a single owner is helpful in rust. Though the possibility of calling multiple then on a future didn't occur to me so I didn't realize that this is also a difference here. I think that the snippet in the question was referring to the poll based approach:

 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;

So I was mostly confused about how the poll is reducing allocations. We could have had a callback-based mechanism, while still enforcing a single owner similar to what you mentioned. I found the explanation in the blog shared by @mbrubeck:

The issue with callback was that in practice it ended up needing an allocation for each closure during future composition. The blog post goes into more detail.

I also found the RustLatam talk by @withoutboats on Zero-Cost Async IO helpful.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.