Exponential type size of async function

I'm making an async function for a library. It's not very big, but yet I'm meeting the error:


error: reached the type-length limit while instantiating `std::pin::Pin::<&mut std::future...}[1]::poll[0]::{{closure}}[0])]>`
    |
    = note: consider adding a `#![type_length_limit="9479903"]` attribute to your crate

error: aborting due to previous error

Commit: https://github.com/Ploppz/s3-upload/tree/e3168d83b73ad373c928a89b1bdca1d01da4299e
Async function in question: https://github.com/Ploppz/s3-upload/blob/e3168d83b73ad373c928a89b1bdca1d01da4299e/src/lib.rs#L75

I have asked around and it seems nobody knows how to overcome this and only suggest that I do as the error suggests. But that makes for a long compile time; and I suppose it also requires the type_length_limit in any crate that uses my library.

I really want to get to the bottom of what part of my code is the 'bottleneck'.
In the linked-to commit I have adjusted the type length limits to be just enough to compile the tests (cargo test). It required a limit of 10 million. How can it possibly get this high? I know it's the return type that becomes complex due to combinators and what-not that are applied on the futures in the function but I only have some ten-fold of combinators/wrappers, so there must be some exponential growth at play here?

Now hold on to your hats because what I did next was to try to use #[tokio::test] async fn ... in tests. Diff: https://github.com/Ploppz/s3-upload/commit/71d2a668a4e2fb104428046f77a93a0ce0a19e9f (ignore the example).

Now I get:

error: reached the type-length limit while instantiating `std::pin::Pin::<&mut std::future...}[1]::poll[0]::{{closure}}[0])]>`
    |
    = note: consider adding a `#![type_length_limit="18960398"]` attribute to your crate

It almost doubled from 10 million to 19 million just because I now call the problematic async function from inside an async function. How is that possible? Is it a bug?

1 Like

The proposed fix is found here. Basically the issue is that when functions have a signature like this:

fn then<Fut, F>(self, f: F) -> Then<Self, Fut, F> where
    F: FnOnce(Self::Output) -> Fut,
    Fut: Future,

The return type Then includes the type of Fut in both F and Fut, thus the exponential growth. You can fight this by hiding types with Box<dyn Future>.

I think that writing your code with async await instead of combinators is also likely to decrease the type size.

5 Likes

That makes sense, thanks a ton!

2 Likes

Expanding on @alice with an example, I recently hit this type length limit issue working with async/await and streams. I specifically was doing this:

let stream = stream::iter(vec![1,2,3]);
let stream_senders_fut =  stream.for_each_concurrent(3, |num| async move {
    do_async_stuff(num).await;
    // I didn't need to return anything here in this example
});

...and got "error: reached the type-length limit while instantiating XXX"
To solve the problem in the simplest way and drastically speed up compile time, I Pin::box'ed the future like this:

let stream = stream::iter(vec![1,2,3]);
// I specify the return type on stream_fut as Pin<Box<...>> while specifying the future Output as () because that is what the future resolves to
let stream_fut: Pin<Box<dyn Future<Output=()>>> = Box::pin(stream.for_each_concurrent(3, |num| async move { 
    do_async_stuff(num).await;
}));

I could then satisfyingly remove the #![type_length_limit="6954178"] as well as recompile in milliseconds vs minutes.

I hope this helps someone else struggling with this!

Thanks for sharing your experience.
Why not just .boxed() on the future? (what is the difference?)
I solved my problem by putting .boxed() behind a future. Only in one place actually.

Here is why:

the trait `std::marker::Unpin` is not implemented for `dyn core::future::future::Future<Output = ()>`

And the best description I could back up why this error is important is:

The main point made is that, without the pinning constraint, you could safely move the future out of a Box into a new Box and poll it from multiple memory locations, which would be unsound.

I cannot give a real underlying reason as to why your's is not throwing this error when compiling, but maybe a more seasoned pinning/future expert can jump in and explain. My main goal was to give another option to solve this problem and provide a high-level reason as to why it works. Either way, I hope it helps if you end up in the same situation!

I assume @Ploppz is referring to FutureExt::boxed, which indeed returns a Pin<Box<...>>. It should be equivalent to what you're doing.

Thanks @alice! It was hard for me to tell what type was being returned in his code without seeing all the code.