Skip to content

Commit c609b2e

Browse files
committed
Auto merge of #78317 - est31:linear_in_impl_count, r=matthewjasper
Turn quadratic time on number of impl blocks into linear time Previously, if you had a lot of inherent impl blocks on a type like: ```Rust struct Foo; impl Foo { fn foo_1() {} } // ... impl Foo { fn foo_100_000() {} } ``` The compiler would be very slow at processing it, because an internal algorithm would run in O(n^2), where n is the number of impl blocks. Now, we add a new algorithm that allocates but is faster asymptotically. Comparing rustc nightly with a local build of rustc as of this PR (results in seconds): | N | real time before | real time after | | - | - | - | | 4_000 | 0.57 | 0.46 | | 8_000 | 1.31 | 0.84 | | 16_000 | 3.56 | 1.69 | | 32_000 | 10.60 | 3.73 | I've tuned up the numbers to make the effect larger than the startup noise of rustc, but the asymptotic difference should hold for smaller n as well. Note: current state of the PR omits error messages if there are other errors present already. For now, I'm mainly interested in a perf run to study whether this issue is present at all. Please queue one for this PR. Thanks!
2 parents b0e5c7d + 73a7d93 commit c609b2e

File tree

5 files changed

+365
-16
lines changed

5 files changed

+365
-16
lines changed

compiler/rustc_typeck/src/coherence/inherent_impls_overlap.rs

+159-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
12
use rustc_errors::struct_span_err;
23
use rustc_hir as hir;
34
use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE};
45
use rustc_hir::itemlikevisit::ItemLikeVisitor;
56
use rustc_middle::ty::{self, TyCtxt};
7+
use rustc_span::Symbol;
68
use rustc_trait_selection::traits::{self, SkipLeakCheck};
79
use smallvec::SmallVec;
10+
use std::collections::hash_map::Entry;
811

912
pub fn crate_inherent_impls_overlap_check(tcx: TyCtxt<'_>, crate_num: CrateNum) {
1013
assert_eq!(crate_num, LOCAL_CRATE);
@@ -33,12 +36,9 @@ impl InherentOverlapChecker<'tcx> {
3336
}
3437

3538
for item1 in impl_items1.in_definition_order() {
36-
let collision = impl_items2.filter_by_name_unhygienic(item1.ident.name).any(|item2| {
37-
// Symbols and namespace match, compare hygienically.
38-
item1.kind.namespace() == item2.kind.namespace()
39-
&& item1.ident.normalize_to_macros_2_0()
40-
== item2.ident.normalize_to_macros_2_0()
41-
});
39+
let collision = impl_items2
40+
.filter_by_name_unhygienic(item1.ident.name)
41+
.any(|item2| self.compare_hygienically(item1, item2));
4242

4343
if collision {
4444
return true;
@@ -48,6 +48,12 @@ impl InherentOverlapChecker<'tcx> {
4848
false
4949
}
5050

51+
fn compare_hygienically(&self, item1: &ty::AssocItem, item2: &ty::AssocItem) -> bool {
52+
// Symbols and namespace match, compare hygienically.
53+
item1.kind.namespace() == item2.kind.namespace()
54+
&& item1.ident.normalize_to_macros_2_0() == item2.ident.normalize_to_macros_2_0()
55+
}
56+
5157
fn check_for_common_items_in_impls(
5258
&self,
5359
impl1: DefId,
@@ -58,12 +64,9 @@ impl InherentOverlapChecker<'tcx> {
5864
let impl_items2 = self.tcx.associated_items(impl2);
5965

6066
for item1 in impl_items1.in_definition_order() {
61-
let collision = impl_items2.filter_by_name_unhygienic(item1.ident.name).find(|item2| {
62-
// Symbols and namespace match, compare hygienically.
63-
item1.kind.namespace() == item2.kind.namespace()
64-
&& item1.ident.normalize_to_macros_2_0()
65-
== item2.ident.normalize_to_macros_2_0()
66-
});
67+
let collision = impl_items2
68+
.filter_by_name_unhygienic(item1.ident.name)
69+
.find(|item2| self.compare_hygienically(item1, item2));
6770

6871
if let Some(item2) = collision {
6972
let name = item1.ident.normalize_to_macros_2_0();
@@ -134,10 +137,150 @@ impl ItemLikeVisitor<'v> for InherentOverlapChecker<'tcx> {
134137
.map(|impl_def_id| (impl_def_id, self.tcx.associated_items(*impl_def_id)))
135138
.collect::<SmallVec<[_; 8]>>();
136139

137-
for (i, &(&impl1_def_id, impl_items1)) in impls_items.iter().enumerate() {
138-
for &(&impl2_def_id, impl_items2) in &impls_items[(i + 1)..] {
139-
if self.impls_have_common_items(impl_items1, impl_items2) {
140-
self.check_for_overlapping_inherent_impls(impl1_def_id, impl2_def_id);
140+
// Perform a O(n^2) algorithm for small n,
141+
// otherwise switch to an allocating algorithm with
142+
// faster asymptotic runtime.
143+
const ALLOCATING_ALGO_THRESHOLD: usize = 500;
144+
if impls.len() < ALLOCATING_ALGO_THRESHOLD {
145+
for (i, &(&impl1_def_id, impl_items1)) in impls_items.iter().enumerate() {
146+
for &(&impl2_def_id, impl_items2) in &impls_items[(i + 1)..] {
147+
if self.impls_have_common_items(impl_items1, impl_items2) {
148+
self.check_for_overlapping_inherent_impls(
149+
impl1_def_id,
150+
impl2_def_id,
151+
);
152+
}
153+
}
154+
}
155+
} else {
156+
// Build a set of connected regions of impl blocks.
157+
// Two impl blocks are regarded as connected if they share
158+
// an item with the same unhygienic identifier.
159+
// After we have assembled the connected regions,
160+
// run the O(n^2) algorithm on each connected region.
161+
// This is advantageous to running the algorithm over the
162+
// entire graph when there are many connected regions.
163+
164+
struct ConnectedRegion {
165+
idents: SmallVec<[Symbol; 8]>,
166+
impl_blocks: FxHashSet<usize>,
167+
}
168+
// Highest connected region id
169+
let mut highest_region_id = 0;
170+
let mut connected_region_ids = FxHashMap::default();
171+
let mut connected_regions = FxHashMap::default();
172+
173+
for (i, &(&_impl_def_id, impl_items)) in impls_items.iter().enumerate() {
174+
if impl_items.len() == 0 {
175+
continue;
176+
}
177+
// First obtain a list of existing connected region ids
178+
let mut idents_to_add = SmallVec::<[Symbol; 8]>::new();
179+
let ids = impl_items
180+
.in_definition_order()
181+
.filter_map(|item| {
182+
let entry = connected_region_ids.entry(item.ident.name);
183+
if let Entry::Occupied(e) = &entry {
184+
Some(*e.get())
185+
} else {
186+
idents_to_add.push(item.ident.name);
187+
None
188+
}
189+
})
190+
.collect::<FxHashSet<usize>>();
191+
match ids.len() {
192+
0 | 1 => {
193+
let id_to_set = if ids.len() == 0 {
194+
// Create a new connected region
195+
let region = ConnectedRegion {
196+
idents: idents_to_add,
197+
impl_blocks: std::iter::once(i).collect(),
198+
};
199+
connected_regions.insert(highest_region_id, region);
200+
(highest_region_id, highest_region_id += 1).0
201+
} else {
202+
// Take the only id inside the list
203+
let id_to_set = *ids.iter().next().unwrap();
204+
let region = connected_regions.get_mut(&id_to_set).unwrap();
205+
region.impl_blocks.insert(i);
206+
region.idents.extend_from_slice(&idents_to_add);
207+
id_to_set
208+
};
209+
let (_id, region) = connected_regions.iter().next().unwrap();
210+
// Update the connected region ids
211+
for ident in region.idents.iter() {
212+
connected_region_ids.insert(*ident, id_to_set);
213+
}
214+
}
215+
_ => {
216+
// We have multiple connected regions to merge.
217+
// In the worst case this might add impl blocks
218+
// one by one and can thus be O(n^2) in the size
219+
// of the resulting final connected region, but
220+
// this is no issue as the final step to check
221+
// for overlaps runs in O(n^2) as well.
222+
223+
// Take the smallest id from the list
224+
let id_to_set = *ids.iter().min().unwrap();
225+
226+
// Sort the id list so that the algorithm is deterministic
227+
let mut ids = ids.into_iter().collect::<SmallVec<[_; 8]>>();
228+
ids.sort();
229+
230+
let mut region = connected_regions.remove(&id_to_set).unwrap();
231+
region.idents.extend_from_slice(&idents_to_add);
232+
region.impl_blocks.insert(i);
233+
234+
for &id in ids.iter() {
235+
if id == id_to_set {
236+
continue;
237+
}
238+
let r = connected_regions.remove(&id).unwrap();
239+
// Update the connected region ids
240+
for ident in r.idents.iter() {
241+
connected_region_ids.insert(*ident, id_to_set);
242+
}
243+
region.idents.extend_from_slice(&r.idents);
244+
region.impl_blocks.extend(r.impl_blocks);
245+
}
246+
connected_regions.insert(id_to_set, region);
247+
}
248+
}
249+
}
250+
251+
debug!(
252+
"churning through {} components (sum={}, avg={}, var={}, max={})",
253+
connected_regions.len(),
254+
impls.len(),
255+
impls.len() / connected_regions.len(),
256+
{
257+
let avg = impls.len() / connected_regions.len();
258+
let s = connected_regions
259+
.iter()
260+
.map(|r| r.1.impl_blocks.len() as isize - avg as isize)
261+
.map(|v| v.abs() as usize)
262+
.sum::<usize>();
263+
s / connected_regions.len()
264+
},
265+
connected_regions.iter().map(|r| r.1.impl_blocks.len()).max().unwrap()
266+
);
267+
// List of connected regions is built. Now, run the overlap check
268+
// for each pair of impl blocks in the same connected region.
269+
for (_id, region) in connected_regions.into_iter() {
270+
let mut impl_blocks =
271+
region.impl_blocks.into_iter().collect::<SmallVec<[_; 8]>>();
272+
impl_blocks.sort();
273+
for (i, &impl1_items_idx) in impl_blocks.iter().enumerate() {
274+
let &(&impl1_def_id, impl_items1) = &impls_items[impl1_items_idx];
275+
for &impl2_items_idx in impl_blocks[(i + 1)..].iter() {
276+
let &(&impl2_def_id, impl_items2) = &impls_items[impl2_items_idx];
277+
if self.impls_have_common_items(impl_items1, impl_items2) {
278+
self.check_for_overlapping_inherent_impls(
279+
impl1_def_id,
280+
impl2_def_id,
281+
);
282+
}
283+
}
141284
}
142285
}
143286
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// force-host
2+
// no-prefer-dynamic
3+
4+
#![crate_type = "proc-macro"]
5+
6+
extern crate proc_macro;
7+
use proc_macro::{Ident, Group, TokenStream, TokenTree as Tt};
8+
9+
// This constant has to be above the ALLOCATING_ALGO_THRESHOLD
10+
// constant in inherent_impls_overlap.rs
11+
const REPEAT_COUNT: u32 = 501;
12+
13+
#[proc_macro]
14+
/// Repeats the input many times, while replacing idents
15+
/// named "IDENT" with "id_$v", where v is a counter.
16+
pub fn repeat_with_idents(input: TokenStream) -> TokenStream {
17+
let mut res = Vec::new();
18+
fn visit_stream(res: &mut Vec<Tt>, stream :TokenStream, v: u32) {
19+
let mut stream_iter = stream.into_iter();
20+
while let Some(tt) = stream_iter.next() {
21+
match tt {
22+
Tt::Group(group) => {
23+
let tt = Tt::Group(visit_group(group, v));
24+
res.push(tt);
25+
},
26+
Tt::Ident(id) => {
27+
let id = if &id.to_string() == "IDENT" {
28+
Ident::new(&format!("id_{}", v), id.span())
29+
} else {
30+
id
31+
};
32+
res.push(Tt::Ident(id));
33+
},
34+
Tt::Punct(p) => {
35+
res.push(Tt::Punct(p));
36+
},
37+
Tt::Literal(lit) => {
38+
res.push(Tt::Literal(lit));
39+
},
40+
}
41+
}
42+
}
43+
fn visit_group(group :Group, v: u32) -> Group {
44+
let mut res = Vec::new();
45+
visit_stream(&mut res, group.stream(), v);
46+
let stream = res.into_iter().collect();
47+
let delim = group.delimiter();
48+
Group::new(delim, stream)
49+
}
50+
for v in 0 .. REPEAT_COUNT {
51+
visit_stream(&mut res, input.clone(), v)
52+
}
53+
res.into_iter().collect()
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// run-pass
2+
// aux-build:repeat.rs
3+
4+
// This tests the allocating algo branch of the
5+
// inherent impls overlap checker.
6+
// This branch was added by PR:
7+
// https://github.com/rust-lang/rust/pull/78317
8+
// In this test, we repeat many impl blocks
9+
// to trigger the allocating branch.
10+
11+
#![allow(unused)]
12+
13+
extern crate repeat;
14+
15+
// Simple case where each impl block is distinct
16+
17+
struct Foo {}
18+
19+
repeat::repeat_with_idents!(impl Foo { fn IDENT() {} });
20+
21+
// There are overlapping impl blocks but due to generics,
22+
// they may overlap.
23+
24+
struct Bar<T>(T);
25+
26+
struct A;
27+
struct B;
28+
29+
repeat::repeat_with_idents!(impl Bar<A> { fn IDENT() {} });
30+
31+
impl Bar<A> { fn foo() {} }
32+
impl Bar<B> { fn foo() {} }
33+
34+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// aux-build:repeat.rs
2+
3+
#![allow(unused)]
4+
5+
// This tests the allocating algo branch of the
6+
// inherent impls overlap checker.
7+
// This branch was added by PR:
8+
// https://github.com/rust-lang/rust/pull/78317
9+
// In this test, we repeat many impl blocks
10+
// to trigger the allocating branch.
11+
12+
// Simple overlap
13+
14+
extern crate repeat;
15+
16+
struct Foo {}
17+
18+
repeat::repeat_with_idents!(impl Foo { fn IDENT() {} });
19+
20+
impl Foo { fn hello() {} } //~ERROR duplicate definitions with name `hello`
21+
impl Foo { fn hello() {} }
22+
23+
// Transitive overlap
24+
25+
struct Foo2 {}
26+
27+
repeat::repeat_with_idents!(impl Foo2 { fn IDENT() {} });
28+
29+
impl Foo2 {
30+
fn bar() {}
31+
fn hello2() {} //~ERROR duplicate definitions with name `hello2`
32+
}
33+
34+
impl Foo2 {
35+
fn baz() {}
36+
fn hello2() {}
37+
}
38+
39+
// Slightly stronger transitive overlap
40+
41+
struct Foo3 {}
42+
43+
repeat::repeat_with_idents!(impl Foo3 { fn IDENT() {} });
44+
45+
impl Foo3 {
46+
fn bar() {} //~ERROR duplicate definitions with name `bar`
47+
fn hello3() {} //~ERROR duplicate definitions with name `hello3`
48+
}
49+
50+
impl Foo3 {
51+
fn bar() {}
52+
fn hello3() {}
53+
}
54+
55+
// Generic overlap
56+
57+
struct Bar<T>(T);
58+
59+
struct A;
60+
struct B;
61+
62+
repeat::repeat_with_idents!(impl Bar<A> { fn IDENT() {} });
63+
64+
impl Bar<A> { fn foo() {} fn bar2() {} }
65+
impl Bar<B> {
66+
fn foo() {}
67+
fn bar2() {} //~ERROR duplicate definitions with name `bar2`
68+
}
69+
impl Bar<B> { fn bar2() {} }
70+
71+
fn main() {}

0 commit comments

Comments
 (0)