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 unnecessary allocation in delete-miss #28

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mlodato517
Copy link
Owner

This Commit

Uses bangsafe's DeleteResult construct to avoid unnecessary Rc::newing when trying to delete a node that isn't there.

Why?

Previously, Child::Delete would call Rc::new on the result of n.delete(k) regardless of what happened. This means that, if n.delete(k) did nothing, we'd allocate a new Rc instead of cloning an existing one. See this comment for more details.

Benchmarks

Ignoring find, insert, and find-miss since those codepaths weren't touched, we see:

delete/immutable/6          time:   [58.097 ns 58.167 ns 58.250 ns]
                            change: [+12.429% +12.732% +13.025%]
                            Performance has regressed.
delete/immutable/126        time:   [161.59 ns 161.74 ns 161.91 ns]
                            change: [+11.545% +11.783% +12.023%]
                            Performance has regressed.
delete/immutable/2046       time:   [306.21 ns 306.81 ns 307.88 ns]
                            change: [+1.7977% +2.2199% +2.5886%]
                            Performance has regressed.
delete/immutable/32766      time:   [494.58 ns 495.30 ns 496.57 ns]
                            change: [+6.8019% +7.0829% +7.3743%]
                            Performance has regressed.
delete-miss/immutable/6     time:   [34.370 ns 34.401 ns 34.438 ns]
                            change: [-51.402% -51.225% -51.080%]
                            Performance has improved.
delete-miss/immutable/126   time:   [45.142 ns 45.171 ns 45.200 ns]
                            change: [-73.200% -73.095% -73.020%]
                            Performance has improved.
delete-miss/immutable/2046  time:   [57.241 ns 57.282 ns 57.327 ns]
                            change: [-82.457% -82.430% -82.402%]
                            Performance has improved.
delete-miss/immutable/32766 time:   [67.454 ns 67.489 ns 67.527 ns]
                            change: [-86.728% -86.697% -86.672%]
                            Performance has improved.

which is expected. delete is now doing more work because it's tracking whether or not the value was found. So when the value is found, we're doing extra work for "nothing"*. However, in delete-miss, we now avoid unnecessary allocations of new Rcs so there are huge improvements.

* I say this is for "nothing" but I don't really mean it. Realistically, Tree::delete should return the value of the deleted node and DeleteResult is a reasonable place to store that because the caller needs both the new tree and the Option<Rc<V>>. Because of this, I'm okay with the hit to delete and a future PR should add the deleted value to the return value of delete.

@mlodato517 mlodato517 self-assigned this Mar 16, 2023
@mlodato517 mlodato517 force-pushed the avoid-allocation-in-delete-miss branch from 0ab9ade to 286c94b Compare March 16, 2023 16:18
let new_left = self.left.delete(k);
Some(self.clone_with_children(new_left, self.right.clone()))
}
cmp::Ordering::Equal => match (&self.left.0, &self.right.0) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Best viewed hiding whitespace

@@ -28,5 +28,7 @@
pub mod bangsafe;
pub mod immutable;

pub(crate) mod util;
Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah the obligatory util module - now it's a real crate

## This Commit

Uses `bangsafe`'s `DeleteResult` construct to avoid unnecessary
`Rc::new`ing when trying to delete a node that isn't there.

## Why?

Previously, `Child::Delete` would call `Rc::new` on the result of
`n.delete(k)` regardless of what happened. This means that, if
`n.delete(k)` did nothing, we'd allocate a new `Rc` instead of cloning
an existing one. See [this comment][0] for more details.

## Benchmarks

Ignoring `find`, `insert`, and `find-miss` since those codepaths weren't
touched, we see:

```
delete/immutable/6          time:   [58.097 ns 58.167 ns 58.250 ns]
                            change: [+12.429% +12.732% +13.025%]
                            Performance has regressed.
delete/immutable/126        time:   [161.59 ns 161.74 ns 161.91 ns]
                            change: [+11.545% +11.783% +12.023%]
                            Performance has regressed.
delete/immutable/2046       time:   [306.21 ns 306.81 ns 307.88 ns]
                            change: [+1.7977% +2.2199% +2.5886%]
                            Performance has regressed.
delete/immutable/32766      time:   [494.58 ns 495.30 ns 496.57 ns]
                            change: [+6.8019% +7.0829% +7.3743%]
                            Performance has regressed.
delete-miss/immutable/6     time:   [34.370 ns 34.401 ns 34.438 ns]
                            change: [-51.402% -51.225% -51.080%]
                            Performance has improved.
delete-miss/immutable/126   time:   [45.142 ns 45.171 ns 45.200 ns]
                            change: [-73.200% -73.095% -73.020%]
                            Performance has improved.
delete-miss/immutable/2046  time:   [57.241 ns 57.282 ns 57.327 ns]
                            change: [-82.457% -82.430% -82.402%]
                            Performance has improved.
delete-miss/immutable/32766 time:   [67.454 ns 67.489 ns 67.527 ns]
                            change: [-86.728% -86.697% -86.672%]
                            Performance has improved.
```

which is expected. `delete` is now doing more work because it's tracking
whether or not the value was found. So when the value _is_ found, we're
doing extra work for "nothing"*. However, in `delete-miss`, we now avoid
unnecessary allocations of new `Rc`s so there are huge improvements.

\* I say this is for "nothing" but I don't really mean it. Realistically,
`Tree::delete` should return the value of the deleted node and
`DeleteResult` is a reasonable place to store that because the caller
needs both the new tree and the `Option<Rc<V>>`. Because of this, I'm
okay with the hit to `delete` and a future PR should add the deleted
value to the return value of `delete`.

[0]: #17 (comment)
@mlodato517 mlodato517 force-pushed the avoid-allocation-in-delete-miss branch from 286c94b to 3447a55 Compare March 16, 2023 16:18
@mlodato517 mlodato517 marked this pull request as ready for review March 16, 2023 16:26
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.

1 participant