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.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.