Skip to content

Commit 65c31b2

Browse files
committed
rustc_metadata: Preprocess search paths for better performance
Over in Zed we've noticed that loading crates for a large-ish workspace can take almost 200ms. We've pinned it down to how rustc searches for paths, as it performs a linear search over the list of candidate paths. In our case the candidate list had about 20k entries which we had to iterate over for each dependency being loaded. This commit introduces a simple FilesIndex that's just a BTreeMap under the hood. Since crates are looked up by both prefix and suffix, we perform a range search on said BTree (which constraints the search space based on prefix) and follow up with a linear scan of entries with matching suffixes. Overall, this commit brings down build time for us in dev scenarios by about 6%. 100ms might not seem like much, but this is a constant cost that each of our workspace crates has to pay, even when said crate is miniscule.
1 parent fbcdd72 commit 65c31b2

File tree

2 files changed

+80
-47
lines changed

2 files changed

+80
-47
lines changed

compiler/rustc_metadata/src/locator.rs

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub(crate) struct CrateLocator<'a> {
247247
cfg_version: &'static str,
248248

249249
// Immutable per-search configuration.
250-
crate_name: Symbol,
250+
pub crate_name: Symbol,
251251
exact_paths: Vec<CanonicalizedPath>,
252252
pub hash: Option<Svh>,
253253
extra_filename: Option<&'a str>,
@@ -409,45 +409,40 @@ impl<'a> CrateLocator<'a> {
409409
// handled safely in `extract_one`.
410410
for search_path in self.filesearch.search_paths() {
411411
debug!("searching {}", search_path.dir.display());
412-
for spf in search_path.files.iter() {
413-
debug!("testing {}", spf.path.display());
414-
415-
let f = &spf.file_name_str;
416-
let (hash, kind) = if let Some(f) = f.strip_prefix(rlib_prefix)
417-
&& let Some(f) = f.strip_suffix(rlib_suffix)
418-
{
419-
(f, CrateFlavor::Rlib)
420-
} else if let Some(f) = f.strip_prefix(rmeta_prefix)
421-
&& let Some(f) = f.strip_suffix(rmeta_suffix)
422-
{
423-
(f, CrateFlavor::Rmeta)
424-
} else if let Some(f) = f.strip_prefix(dylib_prefix)
425-
&& let Some(f) = f.strip_suffix(dylib_suffix.as_ref())
426-
{
427-
(f, CrateFlavor::Dylib)
428-
} else {
429-
if f.starts_with(staticlib_prefix) && f.ends_with(staticlib_suffix.as_ref()) {
430-
self.crate_rejections.via_kind.push(CrateMismatch {
431-
path: spf.path.clone(),
432-
got: "static".to_string(),
433-
});
434-
}
435-
continue;
436-
};
412+
let spf = &search_path.files;
437413

438-
info!("lib candidate: {}", spf.path.display());
414+
for (prefix, suffix, kind) in [
415+
(rlib_prefix.as_str(), rlib_suffix, CrateFlavor::Rlib),
416+
(rmeta_prefix.as_str(), rmeta_suffix, CrateFlavor::Rmeta),
417+
(dylib_prefix, dylib_suffix, CrateFlavor::Dylib),
418+
] {
419+
if let Some(matches) = spf.query(prefix, suffix) {
420+
for (hash, spf) in matches {
421+
info!("lib candidate: {}", spf.path.display());
439422

440-
let (rlibs, rmetas, dylibs) = candidates.entry(hash.to_string()).or_default();
441-
let path = try_canonicalize(&spf.path).unwrap_or_else(|_| spf.path.clone());
442-
if seen_paths.contains(&path) {
443-
continue;
444-
};
445-
seen_paths.insert(path.clone());
446-
match kind {
447-
CrateFlavor::Rlib => rlibs.insert(path, search_path.kind),
448-
CrateFlavor::Rmeta => rmetas.insert(path, search_path.kind),
449-
CrateFlavor::Dylib => dylibs.insert(path, search_path.kind),
450-
};
423+
let (rlibs, rmetas, dylibs) =
424+
candidates.entry(hash.to_string()).or_default();
425+
let path =
426+
try_canonicalize(&spf.path).unwrap_or_else(|_| spf.path.to_path_buf());
427+
if seen_paths.contains(&path) {
428+
continue;
429+
};
430+
seen_paths.insert(path.clone());
431+
match kind {
432+
CrateFlavor::Rlib => rlibs.insert(path, search_path.kind),
433+
CrateFlavor::Rmeta => rmetas.insert(path, search_path.kind),
434+
CrateFlavor::Dylib => dylibs.insert(path, search_path.kind),
435+
};
436+
}
437+
}
438+
}
439+
if let Some(static_matches) = spf.query(staticlib_prefix, staticlib_suffix) {
440+
for (_, spf) in static_matches {
441+
self.crate_rejections.via_kind.push(CrateMismatch {
442+
path: spf.path.to_path_buf(),
443+
got: "static".to_string(),
444+
});
445+
}
451446
}
452447
}
453448

compiler/rustc_session/src/search_paths.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use std::collections::BTreeMap;
2+
use std::ops::Bound;
13
use std::path::{Path, PathBuf};
4+
use std::sync::Arc;
25

36
use rustc_macros::{Decodable, Encodable, HashStable_Generic};
47
use rustc_target::spec::TargetTuple;
@@ -10,9 +13,40 @@ use crate::filesearch::make_target_lib_path;
1013
pub struct SearchPath {
1114
pub kind: PathKind,
1215
pub dir: PathBuf,
13-
pub files: Vec<SearchPathFile>,
16+
pub files: FilesIndex,
1417
}
1518

19+
/// [FilesIndex] contains paths that can be efficiently looked up with (prefix, suffix) pairs.
20+
#[derive(Clone, Debug)]
21+
pub struct FilesIndex(BTreeMap<Arc<str>, Arc<SearchPathFile>>);
22+
23+
impl FilesIndex {
24+
/// Look up [SearchPathFile] by (prefix, suffix) pair.
25+
pub fn query<'this, 'prefix, 'suffix>(
26+
&'this self,
27+
prefix: &'prefix str,
28+
suffix: &'suffix str,
29+
) -> Option<impl Iterator<Item = (String, Arc<SearchPathFile>)> + use<'this, 'prefix, 'suffix>>
30+
{
31+
let start = prefix;
32+
let end = format!("{prefix}\u{FFFF}");
33+
let mut ret = self
34+
.0
35+
.range::<str, _>((Bound::Included(start), Bound::Excluded(end.as_str())))
36+
.filter_map(move |(k, v)| {
37+
k.ends_with(suffix).then(|| {
38+
(
39+
String::from(
40+
&v.file_name_str[prefix.len()..v.file_name_str.len() - suffix.len()],
41+
),
42+
v.clone(),
43+
)
44+
})
45+
})
46+
.peekable();
47+
ret.peek().is_some().then(|| ret)
48+
}
49+
}
1650
/// The obvious implementation of `SearchPath::files` is a `Vec<PathBuf>`. But
1751
/// it is searched repeatedly by `find_library_crate`, and the searches involve
1852
/// checking the prefix and suffix of the filename of each `PathBuf`. This is
@@ -26,8 +60,8 @@ pub struct SearchPath {
2660
/// UTF-8, and so a non-UTF-8 filename couldn't be one we're looking for.)
2761
#[derive(Clone, Debug)]
2862
pub struct SearchPathFile {
29-
pub path: PathBuf,
30-
pub file_name_str: String,
63+
pub path: Arc<Path>,
64+
pub file_name_str: Arc<str>,
3165
}
3266

3367
#[derive(PartialEq, Clone, Copy, Debug, Hash, Eq, Encodable, Decodable, HashStable_Generic)]
@@ -102,16 +136,20 @@ impl SearchPath {
102136
Ok(files) => files
103137
.filter_map(|e| {
104138
e.ok().and_then(|e| {
105-
e.file_name().to_str().map(|s| SearchPathFile {
106-
path: e.path(),
107-
file_name_str: s.to_string(),
139+
e.file_name().to_str().map(|s| {
140+
let file_name_str: Arc<str> = s.into();
141+
(
142+
file_name_str.clone(),
143+
Arc::new(SearchPathFile { path: e.path().into(), file_name_str }),
144+
)
108145
})
109146
})
110147
})
111-
.collect::<Vec<_>>(),
112-
Err(..) => vec![],
113-
};
148+
.collect::<BTreeMap<_, _>>(),
114149

150+
Err(..) => Default::default(),
151+
};
152+
let files = FilesIndex(files);
115153
SearchPath { kind, dir, files }
116154
}
117155
}

0 commit comments

Comments
 (0)