Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid some extra clones/allocations #2865

Closed
wants to merge 3 commits into from
Closed

Conversation

novacrazy
Copy link
Contributor

@novacrazy novacrazy commented Aug 7, 2024

Motivation

I noticed when writing a Layer implementation that it requires Clone, and after running a small hello world noticed it cloned a lot. After looking at the code I saw that some of the clones were pointless, just to get a mutable reference that then cloned again internally.

Solution

This adds a couple specialized routines that help avoid a few clones, and cleans up some code elsewhere to avoid a couple more allocations.

The most contentious change might be to MethodRouter::call_with_state, but a match might be more efficient, and the new macro should more easily help catch issues the comment about destructuring was after. Edit: Partially reverted, overlooked something simple.

I'm still looking for places where clones/allocations can be avoided.

@pickfire
Copy link

pickfire commented Aug 9, 2024

@novacrazy I am interested how did you get to figure out where allocations occur. Did you overwrite the global allocator to make it tracks allocation?

@novacrazy
Copy link
Contributor Author

@pickfire I had a hunch it was inefficient somewhere, so I put a println! in my layer/service Clone implementation to confirm. Then I just read through Axum’s code, starting at the Router’s own Service call until I saw all the cloning going on. Nothing special.

When I have some more time I’d like to figure out how to remove even more clones. 1-2 should be possible.

Copy link
Member

@jplatte jplatte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the PR! I'm not intimately familiar with the code pieces you're changing here. Would you mind splitting this PR into smaller ones? It seems like there's a bunch of independent changes here that would be easier to discuss separately.

@novacrazy
Copy link
Contributor Author

novacrazy commented Aug 9, 2024

Honestly, I don't think it's necessary to split up. It's only 45 lines changed.

The first change was to get rid of the req.uri().path().to_owned(), which was a pointless allocation. By splitting the request into its parts and body, we can borrow from parts.uri while also having mutable access to the parts.extensions, then just recombine the request when it needs to be sent downstream. Zero cloning.

Second, notice the method oneshot_inner, which calls self.0.get_mut() to acquire mutable access to a mutex's inner value, only to immediately clone it and call oneshot. Instead of get_mut(), we can use into_inner() to simply move the inner value in cases where we have ownership of it, which is what oneshot_inner_owned and call_owned do. Then I replaced some code that cloned and called oneshot_inner/Route::call with those, respectively, since the clone already occurred and we have ownership in those places.

It's also worth noting this old code:

let route = handler.clone().into_route(state);
return RouteFuture::from_future(route.clone().oneshot_inner($req))

which clones the handler, applies state to it, then clones it again for no reason. I fixed that as well.

Lastly, I avoided cloning the Method for use within the call! macro in MethodRouter::call_with_state. Most of the time cloning that is simple, but Method is not Copy, because of custom methods, so may as well just avoid the cloning.

Copy link

@Bmwx12013 Bmwx12013 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chancha

Copy link
Collaborator

@yanns yanns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good to me, but I'm no expert here.

@novacrazy novacrazy requested a review from jplatte August 31, 2024 19:13
@yanns
Copy link
Collaborator

yanns commented Oct 7, 2024

@novacrazy could you check the conflicts with main?

yanns added a commit that referenced this pull request Oct 9, 2024
@yanns yanns mentioned this pull request Oct 9, 2024
@yanns
Copy link
Collaborator

yanns commented Oct 9, 2024

@novacrazy I've created #2968 that would merge your changes with main. I hope this can help you updating to main.

match endpoint {
Endpoint::MethodRouter(method_router) => {
Ok(method_router.call_with_state(req, state))
}
Endpoint::Route(route) => Ok(route.clone().call(req)),
Endpoint::Route(route) => Ok(route.clone().call_owned(req)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not reflected in any tests. Do you think we could test this?

/// Variant of [`Route::call`] that takes ownership of the route to avoid cloning.
pub(crate) fn call_owned(self, req: Request<Body>) -> RouteFuture<E> {
let req = req.map(Body::new);
RouteFuture::from_future(self.oneshot_inner_owned(req))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When comparing with main, it would now be self.oneshot_inner_owned(req).not_top_level().

I'm not sure about the .not_top_level(). And there are no tests checking if this is necessary or not.
Do you think we could test this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a note for me.
The current .not_top_level() is covered by axum/src/routing/tests/mod.rs:1086

Comment on lines -659 to +666
fn call_with_state(&mut self, req: Request, state: S) -> RouteFuture<E> {
fn call_with_state(self, req: Request, state: S) -> RouteFuture<E> {
match self {
Fallback::Default(route) | Fallback::Service(route) => {
RouteFuture::from_future(route.oneshot_inner(req))
RouteFuture::from_future(route.oneshot_inner_owned(req))
}
Fallback::BoxedHandler(handler) => {
let mut route = handler.clone().into_route(state);
RouteFuture::from_future(route.oneshot_inner(req))
let route = handler.into_route(state);
RouteFuture::from_future(route.oneshot_inner_owned(req))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests covering this change. Do you think we could test this?

@jplatte
Copy link
Member

jplatte commented Oct 9, 2024

Given latest comments, I want to emphasize once again that this would be much easier to get merged as several smaller PRs, however small the diff may already be.

@novacrazy
Copy link
Contributor Author

If @yanns wants to take over with #2968 that's fine with me. I ended up just writing my own zero-cost web framework to replace Axum in my stack.

@jplatte
Copy link
Member

jplatte commented Oct 9, 2024

Okay, I'll close this in favor of the new PR then.

@jplatte jplatte closed this Oct 9, 2024
yanns added a commit that referenced this pull request Oct 9, 2024
This is an extraction of a part of #2865
@yanns yanns mentioned this pull request Oct 9, 2024
@yanns
Copy link
Collaborator

yanns commented Oct 9, 2024

I've extracted one part that is covered by tests in #2969.
I'll tackle the other parts in other PRs, trying to find way to add test coverage.

@yanns yanns mentioned this pull request Oct 9, 2024
yanns pushed a commit that referenced this pull request Oct 9, 2024
This is an extraction of a part of #2865
yanns pushed a commit that referenced this pull request Oct 9, 2024
This is an extraction of a part of #2865
This was referenced Oct 10, 2024
jplatte pushed a commit that referenced this pull request Nov 14, 2024
This is an extraction of a part of #2865
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants