Skip to content

Commit 047dc4e

Browse files
committed
Auto merge of #153754 - Zalathar:would-change, r=<try>
Avoid the would-change check for bitset chunks that are unique
2 parents d1ee5e5 + 66d118a commit 047dc4e

4 files changed

Lines changed: 242 additions & 50 deletions

File tree

compiler/rustc_index/src/bit_set.rs

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,39 @@ impl<T: Idx> ChunkedBitSet<T> {
743743
bit_relations_inherent_impls! {}
744744
}
745745

746+
/// Behaves like [`Rc::make_mut`], except that if an inner clone would be
747+
/// required but `would_change` returns false, then no inner clone occurs and
748+
/// None is returned instead.
749+
///
750+
/// This function assumes that `would_change` is cheaper than an inner clone,
751+
/// but expensive enough to be worth avoiding if an inner clone isn't required.
752+
///
753+
/// In other words, this function returns:
754+
/// - Some (without cloning) if [`Rc::get_mut`] succeeds
755+
/// - None (without cloning) if `would_change` returns false
756+
/// - Some (after cloning) otherwise
757+
fn make_chunk_words_mut_for_change<'a>(
758+
self_words: &'a mut Rc<[Word; CHUNK_WORDS]>,
759+
would_change: impl Fn(&[Word; CHUNK_WORDS]) -> bool,
760+
) -> Option<&'a mut [Word; CHUNK_WORDS]> {
761+
// If the Rc is already unique, we can skip the would-change check entirely.
762+
if let Some(words) = Rc::get_mut(self_words) {
763+
// Round-trip through a raw pointer to work around a well-known limitation of the
764+
// non-Polonius borrow checker <https://github.com/rust-lang/rust/issues/54663>.
765+
// SAFETY: Equivalent to `Some(words)`, which is sound but only accepted by Polonius;
766+
// see `tests/ui/nll/polonius/nll-problem-case-3-issue-54663.rs`.
767+
return Some(unsafe { &mut *(words as *mut _) });
768+
};
769+
770+
// At this point we would need to clone the chunk if we wanted to return `&mut`,
771+
// so first check if the intended operation would actually modify the words.
772+
if !would_change(self_words) {
773+
return None;
774+
}
775+
776+
Some(Rc::make_mut(self_words))
777+
}
778+
746779
impl<T: Idx> BitRelations<ChunkedBitSet<T>> for ChunkedBitSet<T> {
747780
fn union(&mut self, other: &ChunkedBitSet<T>) -> bool {
748781
assert_eq!(self.domain_size, other.domain_size);
@@ -780,38 +813,36 @@ impl<T: Idx> BitRelations<ChunkedBitSet<T>> for ChunkedBitSet<T> {
780813
// performance win. Also, we only need to operate on the
781814
// in-use words, hence the slicing.
782815
let num_words = num_words(chunk_domain_size as usize);
783-
784-
// If both sides are the same, nothing will change. This
785-
// case is very common and it's a pretty fast check, so
786-
// it's a performance win to do it.
787-
if self_chunk_words[0..num_words] == other_chunk_words[0..num_words] {
788-
continue;
789-
}
790-
791-
// Do a more precise "will anything change?" test. Also a
792-
// performance win.
793816
let op = |a, b| a | b;
794-
if !bitwise_changes(
795-
&self_chunk_words[0..num_words],
796-
&other_chunk_words[0..num_words],
797-
op,
798-
) {
817+
818+
// Try to get a mutable reference to our words, but only if doing so is
819+
// cheap, or if we're definitely going to modify the words.
820+
let Some(self_chunk_words) =
821+
make_chunk_words_mut_for_change(self_chunk_words, |self_chunk_words| {
822+
let lhs = &self_chunk_words[0..num_words];
823+
let rhs = &other_chunk_words[0..num_words];
824+
// If both sides are the same, nothing will change. That case is very
825+
// common, so it's a performance win to do an extremely fast equality
826+
// check before the full would-change check.
827+
(lhs != rhs) && bitwise_changes(lhs, rhs, op)
828+
})
829+
else {
799830
continue;
800-
}
831+
};
801832

802-
// If we reach here, `self_chunk_words` is definitely changing.
803-
let self_chunk_words = Rc::make_mut(self_chunk_words);
804833
let has_changed = bitwise(
805834
&mut self_chunk_words[0..num_words],
806835
&other_chunk_words[0..num_words],
807836
op,
808837
);
809-
debug_assert!(has_changed);
810-
*self_chunk_count = count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
811-
if *self_chunk_count == chunk_domain_size {
812-
*self_chunk = Ones;
838+
if has_changed {
839+
*self_chunk_count =
840+
count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
841+
if *self_chunk_count == chunk_domain_size {
842+
*self_chunk = Ones;
843+
}
844+
changed = true;
813845
}
814-
changed = true;
815846
}
816847
}
817848
}
@@ -868,26 +899,32 @@ impl<T: Idx> BitRelations<ChunkedBitSet<T>> for ChunkedBitSet<T> {
868899
// See `ChunkedBitSet::union` for details on what is happening here.
869900
let num_words = num_words(chunk_domain_size as usize);
870901
let op = |a: Word, b: Word| a & !b;
871-
if !bitwise_changes(
872-
&self_chunk_words[0..num_words],
873-
&other_chunk_words[0..num_words],
874-
op,
875-
) {
902+
903+
let Some(self_chunk_words) =
904+
make_chunk_words_mut_for_change(self_chunk_words, |self_chunk_words| {
905+
bitwise_changes(
906+
&self_chunk_words[0..num_words],
907+
&other_chunk_words[0..num_words],
908+
op,
909+
)
910+
})
911+
else {
876912
continue;
877-
}
913+
};
878914

879-
let self_chunk_words = Rc::make_mut(self_chunk_words);
880915
let has_changed = bitwise(
881916
&mut self_chunk_words[0..num_words],
882917
&other_chunk_words[0..num_words],
883918
op,
884919
);
885-
debug_assert!(has_changed);
886-
*self_chunk_count = count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
887-
if *self_chunk_count == 0 {
888-
*self_chunk = Zeros;
920+
if has_changed {
921+
*self_chunk_count =
922+
count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
923+
if *self_chunk_count == 0 {
924+
*self_chunk = Zeros;
925+
}
926+
changed = true;
889927
}
890-
changed = true;
891928
}
892929
}
893930
}
@@ -926,26 +963,32 @@ impl<T: Idx> BitRelations<ChunkedBitSet<T>> for ChunkedBitSet<T> {
926963
// See `ChunkedBitSet::union` for details on what is happening here.
927964
let num_words = num_words(chunk_domain_size as usize);
928965
let op = |a, b| a & b;
929-
if !bitwise_changes(
930-
&self_chunk_words[0..num_words],
931-
&other_chunk_words[0..num_words],
932-
op,
933-
) {
966+
967+
let Some(self_chunk_words) =
968+
make_chunk_words_mut_for_change(self_chunk_words, |self_chunk_words| {
969+
bitwise_changes(
970+
&self_chunk_words[0..num_words],
971+
&other_chunk_words[0..num_words],
972+
op,
973+
)
974+
})
975+
else {
934976
continue;
935-
}
977+
};
936978

937-
let self_chunk_words = Rc::make_mut(self_chunk_words);
938979
let has_changed = bitwise(
939980
&mut self_chunk_words[0..num_words],
940981
&other_chunk_words[0..num_words],
941982
op,
942983
);
943-
debug_assert!(has_changed);
944-
*self_chunk_count = count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
945-
if *self_chunk_count == 0 {
946-
*self_chunk = Zeros;
984+
if has_changed {
985+
*self_chunk_count =
986+
count_ones(&self_chunk_words[0..num_words]) as ChunkSize;
987+
if *self_chunk_count == 0 {
988+
*self_chunk = Zeros;
989+
}
990+
changed = true;
947991
}
948-
changed = true;
949992
}
950993
}
951994
}

compiler/rustc_index/src/bit_set/tests.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,108 @@ fn chunked_bitset() {
298298
b10000b.assert_valid();
299299
}
300300

301+
/// Creates a new `ChunkedBitSet` containing all `i` for which `fill_fn(i)` is true.
302+
fn chunked_fill_with(domain_size: usize, fill_fn: impl Fn(usize) -> bool) -> ChunkedBitSet<usize> {
303+
let mut bitset = ChunkedBitSet::new_empty(domain_size);
304+
for i in 0..domain_size {
305+
if fill_fn(i) {
306+
bitset.insert(i);
307+
}
308+
}
309+
bitset
310+
}
311+
312+
/// Asserts that for each `i` in `0..bitset.domain_size()`, `bitset.contains(i) == expected_fn(i)`.
313+
#[track_caller]
314+
fn assert_chunked_contains(bitset: &ChunkedBitSet<usize>, expected_fn: impl Fn(usize) -> bool) {
315+
for i in 0..bitset.domain_size() {
316+
let expected = expected_fn(i);
317+
assert_eq!(bitset.contains(i), expected, "i = {i}");
318+
}
319+
}
320+
321+
#[test]
322+
fn chunked_bulk_ops() {
323+
struct ChunkedBulkOp {
324+
name: &'static str,
325+
op_fn: fn(&mut ChunkedBitSet<usize>, &ChunkedBitSet<usize>) -> bool,
326+
spec_fn: fn(fn(usize) -> bool, fn(usize) -> bool, usize) -> bool,
327+
}
328+
let ops = &[
329+
ChunkedBulkOp {
330+
name: "union",
331+
op_fn: ChunkedBitSet::union,
332+
spec_fn: |fizz, buzz, i| fizz(i) || buzz(i),
333+
},
334+
ChunkedBulkOp {
335+
name: "subtract",
336+
op_fn: ChunkedBitSet::subtract,
337+
spec_fn: |fizz, buzz, i| fizz(i) && !buzz(i),
338+
},
339+
ChunkedBulkOp {
340+
name: "intersect",
341+
op_fn: ChunkedBitSet::intersect,
342+
spec_fn: |fizz, buzz, i| fizz(i) && buzz(i),
343+
},
344+
];
345+
346+
let domain_sizes = [
347+
CHUNK_BITS / 7, // Smaller than a full chunk.
348+
CHUNK_BITS,
349+
(CHUNK_BITS + CHUNK_BITS / 7), // Larger than a full chunk.
350+
];
351+
352+
for ChunkedBulkOp { name, op_fn, spec_fn } in ops {
353+
for domain_size in domain_sizes {
354+
// If true, use identical values for LHS and RHS, to test "fizz op fizz".
355+
// If false, use different values, to test "fizz op buzz".
356+
for identical in [false, true] {
357+
// If true, make a clone of LHS before doing the op.
358+
// This covers optimizations that depend on whether chunk words are shared or not.
359+
for unique in [false, true] {
360+
// Print the current test case, so that we can see which one failed.
361+
println!(
362+
"Testing op={name}, domain_size={domain_size}, identical={identical}, unique={unique} ..."
363+
);
364+
365+
let fizz_fn = |i| i % 3 == 0;
366+
let buzz_fn = if identical { fizz_fn } else { |i| i % 5 == 0 };
367+
368+
// Check that `fizz op buzz` gives the expected results.
369+
chunked_test_inner(domain_size, unique, fizz_fn, buzz_fn, op_fn, |i| {
370+
spec_fn(fizz_fn, buzz_fn, i)
371+
});
372+
}
373+
}
374+
}
375+
}
376+
}
377+
378+
fn chunked_test_inner(
379+
domain_size: usize,
380+
unique: bool,
381+
fizz_fn: impl Fn(usize) -> bool + Copy,
382+
buzz_fn: impl Fn(usize) -> bool + Copy,
383+
op_fn: impl Fn(&mut ChunkedBitSet<usize>, &ChunkedBitSet<usize>) -> bool,
384+
expected_fn: impl Fn(usize) -> bool + Copy,
385+
) {
386+
// Create two bitsets, "fizz" (LHS) and "buzz" (RHS).
387+
let mut fizz = chunked_fill_with(domain_size, fizz_fn);
388+
let buzz = chunked_fill_with(domain_size, buzz_fn);
389+
390+
// If requested, clone `fizz` so that its word Rcs are not uniquely-owned.
391+
let _cloned = (!unique).then(|| fizz.clone());
392+
393+
// Perform the op (e.g. union/subtract/intersect), and verify that the
394+
// mutated LHS contains exactly the expected values.
395+
let changed = op_fn(&mut fizz, &buzz);
396+
assert_chunked_contains(&fizz, expected_fn);
397+
398+
// Verify that the "changed" return value is correct.
399+
let should_change = (0..domain_size).any(|i| fizz_fn(i) != expected_fn(i));
400+
assert_eq!(changed, should_change);
401+
}
402+
301403
fn with_elements_chunked(elements: &[usize], domain_size: usize) -> ChunkedBitSet<usize> {
302404
let mut s = ChunkedBitSet::new_empty(domain_size);
303405
for &e in elements {

tests/ui/nll/polonius/nll-problem-case-3-issue-54663.nll.stderr

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error[E0499]: cannot borrow `*x` as mutable more than once at a time
2-
--> $DIR/nll-problem-case-3-issue-54663.rs:20:9
2+
--> $DIR/nll-problem-case-3-issue-54663.rs:22:9
33
|
44
LL | fn foo(x: &mut u8) -> Option<&u8> {
55
| - let's call the lifetime of this reference `'1`
@@ -11,6 +11,35 @@ LL | }
1111
LL | bar(x)
1212
| ^ second mutable borrow occurs here
1313

14-
error: aborting due to 1 previous error
14+
error[E0502]: cannot borrow `*self_words` as immutable because it is also borrowed as mutable
15+
--> $DIR/nll-problem-case-3-issue-54663.rs:38:22
16+
|
17+
LL | fn make_chunk_words_mut_for_change<'a>(
18+
| -- lifetime `'a` defined here
19+
...
20+
LL | if let Some(words) = Rc::get_mut(self_words) {
21+
| ---------- mutable borrow occurs here
22+
LL | return Some(words);
23+
| ----------- returning this value requires that `*self_words` is borrowed for `'a`
24+
...
25+
LL | if !would_change(self_words) {
26+
| ^^^^^^^^^^ immutable borrow occurs here
27+
28+
error[E0499]: cannot borrow `*self_words` as mutable more than once at a time
29+
--> $DIR/nll-problem-case-3-issue-54663.rs:42:23
30+
|
31+
LL | fn make_chunk_words_mut_for_change<'a>(
32+
| -- lifetime `'a` defined here
33+
...
34+
LL | if let Some(words) = Rc::get_mut(self_words) {
35+
| ---------- first mutable borrow occurs here
36+
LL | return Some(words);
37+
| ----------- returning this value requires that `*self_words` is borrowed for `'a`
38+
...
39+
LL | Some(Rc::make_mut(self_words))
40+
| ^^^^^^^^^^ second mutable borrow occurs here
41+
42+
error: aborting due to 3 previous errors
1543

16-
For more information about this error, try `rustc --explain E0499`.
44+
Some errors have detailed explanations: E0499, E0502.
45+
For more information about an error, try `rustc --explain E0499`.

tests/ui/nll/polonius/nll-problem-case-3-issue-54663.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
//@ [legacy] check-pass
1414
//@ [legacy] compile-flags: -Z polonius=legacy
1515

16+
use std::rc::Rc;
17+
1618
fn foo(x: &mut u8) -> Option<&u8> {
1719
if let Some(y) = bar(x) {
1820
return Some(y);
@@ -23,3 +25,19 @@ fn foo(x: &mut u8) -> Option<&u8> {
2325
fn bar(x: &mut u8) -> Option<&u8> {
2426
Some(x)
2527
}
28+
29+
// Adapted from the compiler's `make_chunk_words_mut_for_change` in `bit_set.rs`.
30+
fn make_chunk_words_mut_for_change<'a>(
31+
self_words: &'a mut Rc<[u64; 32]>,
32+
would_change: impl Fn(&[u64; 32]) -> bool,
33+
) -> Option<&'a mut [u64; 32]> {
34+
if let Some(words) = Rc::get_mut(self_words) {
35+
return Some(words);
36+
};
37+
38+
if !would_change(self_words) {
39+
return None;
40+
}
41+
42+
Some(Rc::make_mut(self_words))
43+
}

0 commit comments

Comments
 (0)