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

feat: optimize no-op bitfield operations #849

Merged
merged 4 commits into from
Sep 12, 2022
Merged

Conversation

Stebalien
Copy link
Member

We have quite a few bitfield operations where one bitfield is empty.
Optimize them.

@codecov-commenter
Copy link

codecov-commenter commented Sep 8, 2022

Codecov Report

Merging #849 (62c9c9c) into master (9b1f206) will decrease coverage by 0.26%.
The diff coverage is 32.17%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #849      +/-   ##
==========================================
- Coverage   51.29%   51.02%   -0.27%     
==========================================
  Files         123      124       +1     
  Lines       10060    10149      +89     
==========================================
+ Hits         5160     5179      +19     
- Misses       4900     4970      +70     
Impacted Files Coverage Δ
ipld/bitfield/src/ops.rs 27.77% <27.77%> (ø)
ipld/bitfield/src/lib.rs 78.07% <100.00%> (+2.22%) ⬆️
shared/src/version/mod.rs 3.70% <0.00%> (-0.15%) ⬇️

}
}

impl BitOrAssign<&BitField> for BitField {
#[inline]
fn bitor_assign(&mut self, rhs: &BitField) {
*self = &*self | rhs;
if !rhs.is_trivially_empty() {
Copy link
Member Author

Choose a reason for hiding this comment

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

saves a clone

#[inline]
fn bitor_assign(&mut self, rhs: BitField) {
// by-value to optimize the case where one is empty.
let s = std::mem::take(self);
Copy link
Member Author

Choose a reason for hiding this comment

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

also may save a clone

Choose a reason for hiding this comment

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

I don't quite follow. The | op on 430 uses the bitor's defined above. Are you saying impl BitOr<&BitField> for &BitField { is less efficient than impl BitOr<BitField> for &BitField { apart from the clone? All I can see different is the clone.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's the difference. But I'll see if I can simplify a bit.

@@ -367,7 +406,20 @@ impl BitAnd<&BitField> for &BitField {

#[inline]
fn bitand(self, rhs: &BitField) -> Self::Output {
BitField::from_ranges(self.ranges().intersection(rhs.ranges()))
if self.is_trivially_empty() || rhs.is_trivially_empty() {
BitField::new()
Copy link
Member Author

Choose a reason for hiding this comment

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

This is free.

}
}

impl BitAnd<BitField> for BitField {
Copy link
Member Author

Choose a reason for hiding this comment

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

Implementing by-value ops encourages users to perform by-value ops where possible (possibly leading to optimizations).

Choose a reason for hiding this comment

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

This uses impl BitAnd<&BitField> for &BitField { under the hood so there's no speedup to using this instead correct? But you're saying just letting them work with values directly can lead to user side optimizations?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, no. I just don't want the user to try a by-value operation, notice that it doesn't work, then assume that bitfields don't support by-value operations.

Copy link

Choose a reason for hiding this comment

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

Which is what I’ve been doing recently. 😂

We have quite a few bitfield operations where one bitfield is empty.
Optimize them.
@Stebalien Stebalien force-pushed the feat/optimize-bitfields branch from 5df7645 to d329f5d Compare September 8, 2022 05:08
@jennijuju
Copy link
Member

(are we supposed to backport this to release/v2? (I can totally do it just wanna confirm we should

@Stebalien
Copy link
Member Author

(are we supposed to backport this to release/v2? (I can totally do it just wanna confirm we should

Ah.... no. This create isn't actually a part of the FVM, we just stashed it in this repo because we're LAAAAAAAAAAAAAAZY.

We can safely release this from master.

@Stebalien
Copy link
Member Author

We've discussed breaking all the "ipld" creates into a new repo, but haven't because that's more work.

Copy link

@ZenGround0 ZenGround0 left a comment

Choose a reason for hiding this comment

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

I'm not so strong on rust's memory model so some of the tricks used here go over my head. However it looks correct.

@@ -297,6 +297,12 @@ impl BitField {
.all(|bit| self.unset.contains(&bit))
}

/// Returns `true` if the bit field is _trivially_ empty. This is significantly faster than
/// checking if it's actually empty, but can be used to short-circuite certain operations.

Choose a reason for hiding this comment

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

Making sure I understand: to check if its actually empty we'd need to apply unset bits across set bits and ranges?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep. Basically, if it returns true, it's definitely empty. But if it returns false, we won't be able to know without doing a bit of work.

#[inline]
fn bitor_assign(&mut self, rhs: BitField) {
// by-value to optimize the case where one is empty.
let s = std::mem::take(self);

Choose a reason for hiding this comment

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

I don't quite follow. The | op on 430 uses the bitor's defined above. Are you saying impl BitOr<&BitField> for &BitField { is less efficient than impl BitOr<BitField> for &BitField { apart from the clone? All I can see different is the clone.


#[inline]
fn bitor(self, rhs: &BitField) -> Self::Output {
if rhs.is_trivially_empty() {

Choose a reason for hiding this comment

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

nit: unless I'm missing some speedup gain it would be nice to check in a consistent order. Other bitors do self, then rhs

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm trying to avoid a clone if both are trivially empty. It probably won't matter in most case so I'll switch it back.

}
}

impl BitAnd<BitField> for BitField {

Choose a reason for hiding this comment

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

This uses impl BitAnd<&BitField> for &BitField { under the hood so there's no speedup to using this instead correct? But you're saying just letting them work with values directly can lead to user side optimizations?

And put them in a new file. This is verbose, but should be easy to
understand and reason about.
@Stebalien
Copy link
Member Author

@ZenGround0 I broke the ops into a separate file and implemented them on all ref/non-ref combinations. It's very verbose, but should be easier to reason about.

#[inline]
fn bitxor(self, rhs: &BitField) -> Self::Output {
// Nothing to optimize.
BitField::from_ranges(self.ranges().symmetric_difference(rhs.ranges()))
Copy link

Choose a reason for hiding this comment

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

Wouldn't XOR be a no-op if one of the sets is trivially empty?

Copy link

@Kubuxu Kubuxu left a comment

Choose a reason for hiding this comment

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

Other than the XOR question, LGTM.

@Stebalien Stebalien force-pushed the feat/optimize-bitfields branch from 5674d74 to 62d53e0 Compare September 12, 2022 22:00
@Stebalien Stebalien enabled auto-merge (squash) September 12, 2022 22:02
@Stebalien Stebalien force-pushed the feat/optimize-bitfields branch from 62d53e0 to f642707 Compare September 12, 2022 22:37
@Stebalien Stebalien disabled auto-merge September 12, 2022 22:37
@Stebalien Stebalien merged commit 28d79e6 into master Sep 12, 2022
@Stebalien Stebalien deleted the feat/optimize-bitfields branch September 12, 2022 22:38
Kubuxu pushed a commit that referenced this pull request Sep 20, 2022
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.

5 participants