-
Notifications
You must be signed in to change notification settings - Fork 472
.*: Box all Futures that are larger than 8KiB #23283
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
Conversation
2ed1871
to
2b32857
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surfaces changes LGTM. I personally don't understand the difference between boxed()
and boxed_local()
, it might be useful to add that to the PR description.
I kicked off a scalability benchmark: https://buildkite.com/materialize/nightlies/builds/5283, I thought the results might be interesting.
# Futures get compiled into state machines, which can end up being very large (10's of KB). | ||
# It's inefficient to moves these around on the stack, so require that they get boxed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be useful to include a link to rust-lang/rust#99504 here.
@@ -529,36 +529,40 @@ pub struct AuthedClient { | |||
} | |||
|
|||
impl AuthedClient { | |||
async fn new( | |||
adapter_client: &Client, | |||
/// Note: the returned Future is intentionally boxed because it is very large. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is subjective, so it's ultimately up to you, but I think these comments are a little redundant. As long as the lint has a sufficient explanation somewhere around it, I think we can remove these comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1! I also find these comments more noisy than helpful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree! I assume people that wonder about this would try to remove the boxed
call and find out the reason for it through the lint.
2b32857
to
c6f9ac9
Compare
@@ -689,17 +690,30 @@ where | |||
/// The `Since` error indicates that the requested `as_of` cannot be served | |||
/// (the caller has out of date information) and includes the smallest | |||
/// `as_of` that would have been accepted. | |||
#[instrument(level = "debug", skip_all, fields(shard = %self.machine.shard_id()))] | |||
pub async fn listen(self, as_of: Antichain<T>) -> Result<Listen<K, V, T, D>, Since<T>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit sad that we're changing signatures here... in particular it seems brittle if this method is refactored.
IIUC it would be ~nearly as good to insert a .boxed()
before the .await
and leave the signature alone... is that right, and if so how would you feel about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We definitely can revert the change to the signature!
The only reason I originally made the changes instead of using .boxed()
was because certain functions were called numerous times, and it seemed easier to change the signature rather than all of the callsites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah +1 on this too, I dislike baking this into signatures. As long as the lint catches it if the boxed call is missed at a callsite, then I have a decently strong preference to do that everywhere in the persist bits of this change.
Moving this to the draft stage while I work out if it actually impacts performance or not, given the size of the PR! |
Thanks for kicking off the scalability benchmark! If anything were to show an improvement I think that would be the one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat find!
@@ -529,36 +529,40 @@ pub struct AuthedClient { | |||
} | |||
|
|||
impl AuthedClient { | |||
async fn new( | |||
adapter_client: &Client, | |||
/// Note: the returned Future is intentionally boxed because it is very large. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1! I also find these comments more noisy than helpful
@@ -689,17 +690,30 @@ where | |||
/// The `Since` error indicates that the requested `as_of` cannot be served | |||
/// (the caller has out of date information) and includes the smallest | |||
/// `as_of` that would have been accepted. | |||
#[instrument(level = "debug", skip_all, fields(shard = %self.machine.shard_id()))] | |||
pub async fn listen(self, as_of: Antichain<T>) -> Result<Listen<K, V, T, D>, Since<T>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah +1 on this too, I dislike baking this into signatures. As long as the lint catches it if the boxed call is missed at a callsite, then I have a decently strong preference to do that everywhere in the persist bits of this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compute changes LGTM.
@@ -73,6 +73,7 @@ | |||
#![warn(clippy::disallowed_macros)] | |||
#![warn(clippy::disallowed_types)] | |||
#![warn(clippy::from_over_into)] | |||
#![warn(clippy::large_futures)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very exited about moving these to the workspace Cargo.toml
and getting out of the business of having to touch every crate when we adjust a lint :)
@@ -529,36 +529,40 @@ pub struct AuthedClient { | |||
} | |||
|
|||
impl AuthedClient { | |||
async fn new( | |||
adapter_client: &Client, | |||
/// Note: the returned Future is intentionally boxed because it is very large. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree! I assume people that wonder about this would try to remove the boxed
call and find out the reason for it through the lint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
storage stuff looks good!
@jkosh44 boxed_local
just doesn't require the boxed future is Send
!
Some(RehydrationCommand::Send(command)) => { | ||
self.absorb_command(&command); | ||
/// Note: the returned Future is intentionally boxed because it is very large. | ||
fn step_await_address<'a>(&'a mut self) -> BoxFuture<'a, RehydrationTaskState<T>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
im surprised this one is so big!
I tried to interpret the scalability benchmark results and this didn't seem to move the needle much. I'm starting to lean against merging this lint as it is applying a premature optimization to many places that don't really benefit by it. An analogy I thought is that we also don't always bother to reuse a vector or use So all in all I think we should punt on this and box futures if we discover that we spend a lot of time copying memory in some critical part of the code. |
Closing this because we're currently unsure of the actual impact |
This PR boxes (aka moves to the heap) all Rust Futures that are larger than 8KiB, and enables the large_futures clippy lint.
Motivation
Calling Rust Futures currently can result in inefficient code with excessive memcpy's, as documented by this issue: rust-lang/rust#99504. There are a number of Futures in our codebase which are quite large >8KiB, and thus could see poor performance. By Box-ing the Futures we'd end up memcpy-ing only a fat pointer, which is 16 bytes, which should be a good performance improvement.
Discussed this in Slack
Tips for reviewer
This PR is split into two commits that can be reviewed separately:
large_futures
lint.When boxing a Future there are two possible approaches, either changing the
async fn
tofn -> BoxFuture
or using.boxed()
at the callsite. I generally used.boxed()
and only when a function was called multiple times, didn't use generics, and it didn't complicate the return type too much, would I update fromasync fn
tofn -> BoxFuture
.Checklist
$T ⇔ Proto$T
mapping (possibly in a backwards-incompatible way), then it is tagged with aT-proto
label.