Skip to content

Commit 5e87710

Browse files
committed
optimize {Path,PathBuf,Components}::{cmp,partial_cmp} for shared prefixes
1 parent 5dcfec3 commit 5e87710

File tree

2 files changed

+90
-6
lines changed

2 files changed

+90
-6
lines changed

library/std/src/path.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -962,16 +962,49 @@ impl cmp::Eq for Components<'_> {}
962962
impl<'a> cmp::PartialOrd for Components<'a> {
963963
#[inline]
964964
fn partial_cmp(&self, other: &Components<'a>) -> Option<cmp::Ordering> {
965-
Iterator::partial_cmp(self.clone(), other.clone())
965+
Some(compare_components(self.clone(), other.clone()))
966966
}
967967
}
968968

969969
#[stable(feature = "rust1", since = "1.0.0")]
970970
impl cmp::Ord for Components<'_> {
971971
#[inline]
972972
fn cmp(&self, other: &Self) -> cmp::Ordering {
973-
Iterator::cmp(self.clone(), other.clone())
973+
compare_components(self.clone(), other.clone())
974+
}
975+
}
976+
977+
fn compare_components(mut left: Components<'_>, mut right: Components<'_>) -> cmp::Ordering {
978+
// Fast path for long shared prefixes
979+
//
980+
// - compare raw bytes to find first mismatch
981+
// - backtrack to find separator before mismatch to avoid ambiguous parsings of '.' or '..' characters
982+
// - if found update state to only do a component-wise comparison on the remainder,
983+
// otherwise do it on the full path
984+
//
985+
// The fast path isn't taken for paths with a PrefixComponent to avoid backtracking into
986+
// the middle of one
987+
if left.prefix.is_none() && right.prefix.is_none() && left.front == right.front {
988+
// this might benefit from a [u8]::first_mismatch simd implementation, if it existed
989+
let first_difference =
990+
match left.path.iter().zip(right.path.iter()).position(|(&a, &b)| a != b) {
991+
None if left.path.len() == right.path.len() => return cmp::Ordering::Equal,
992+
None => left.path.len().min(right.path.len()),
993+
Some(diff) => diff,
994+
};
995+
996+
if let Some(previous_sep) =
997+
left.path[..first_difference].iter().rposition(|&b| left.is_sep_byte(b))
998+
{
999+
let mismatched_component_start = previous_sep + 1;
1000+
left.path = &left.path[mismatched_component_start..];
1001+
left.front = State::Body;
1002+
right.path = &right.path[mismatched_component_start..];
1003+
right.front = State::Body;
1004+
}
9741005
}
1006+
1007+
Iterator::cmp(left, right)
9751008
}
9761009

9771010
/// An iterator over [`Path`] and its ancestors.
@@ -1704,15 +1737,15 @@ impl cmp::Eq for PathBuf {}
17041737
impl cmp::PartialOrd for PathBuf {
17051738
#[inline]
17061739
fn partial_cmp(&self, other: &PathBuf) -> Option<cmp::Ordering> {
1707-
self.components().partial_cmp(other.components())
1740+
Some(compare_components(self.components(), other.components()))
17081741
}
17091742
}
17101743

17111744
#[stable(feature = "rust1", since = "1.0.0")]
17121745
impl cmp::Ord for PathBuf {
17131746
#[inline]
17141747
fn cmp(&self, other: &PathBuf) -> cmp::Ordering {
1715-
self.components().cmp(other.components())
1748+
compare_components(self.components(), other.components())
17161749
}
17171750
}
17181751

@@ -2706,15 +2739,15 @@ impl cmp::Eq for Path {}
27062739
impl cmp::PartialOrd for Path {
27072740
#[inline]
27082741
fn partial_cmp(&self, other: &Path) -> Option<cmp::Ordering> {
2709-
self.components().partial_cmp(other.components())
2742+
Some(compare_components(self.components(), other.components()))
27102743
}
27112744
}
27122745

27132746
#[stable(feature = "rust1", since = "1.0.0")]
27142747
impl cmp::Ord for Path {
27152748
#[inline]
27162749
fn cmp(&self, other: &Path) -> cmp::Ordering {
2717-
self.components().cmp(other.components())
2750+
compare_components(self.components(), other.components())
27182751
}
27192752
}
27202753

library/std/src/path/tests.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use super::*;
22

3+
use crate::collections::BTreeSet;
34
use crate::rc::Rc;
45
use crate::sync::Arc;
6+
use core::hint::black_box;
57

68
macro_rules! t(
79
($path:expr, iter: $iter:expr) => (
@@ -1392,3 +1394,52 @@ fn into_rc() {
13921394
assert_eq!(&*rc2, path);
13931395
assert_eq!(&*arc2, path);
13941396
}
1397+
1398+
#[bench]
1399+
fn bench_path_cmp_fast_path_buf_sort(b: &mut test::Bencher) {
1400+
let prefix = "my/home";
1401+
let mut paths: Vec<_> =
1402+
(0..1000).map(|num| PathBuf::from(prefix).join(format!("file {}.rs", num))).collect();
1403+
1404+
paths.sort();
1405+
1406+
b.iter(|| {
1407+
black_box(paths.as_mut_slice()).sort_unstable();
1408+
});
1409+
}
1410+
1411+
#[bench]
1412+
fn bench_path_cmp_fast_path_long(b: &mut test::Bencher) {
1413+
let prefix = "/my/home/is/my/castle/and/my/castle/has/a/rusty/workbench/";
1414+
let paths: Vec<_> =
1415+
(0..1000).map(|num| PathBuf::from(prefix).join(format!("file {}.rs", num))).collect();
1416+
1417+
let mut set = BTreeSet::new();
1418+
1419+
paths.iter().for_each(|p| {
1420+
set.insert(p.as_path());
1421+
});
1422+
1423+
b.iter(|| {
1424+
set.remove(paths[500].as_path());
1425+
set.insert(paths[500].as_path());
1426+
});
1427+
}
1428+
1429+
#[bench]
1430+
fn bench_path_cmp_fast_path_short(b: &mut test::Bencher) {
1431+
let prefix = "my/home";
1432+
let paths: Vec<_> =
1433+
(0..1000).map(|num| PathBuf::from(prefix).join(format!("file {}.rs", num))).collect();
1434+
1435+
let mut set = BTreeSet::new();
1436+
1437+
paths.iter().for_each(|p| {
1438+
set.insert(p.as_path());
1439+
});
1440+
1441+
b.iter(|| {
1442+
set.remove(paths[500].as_path());
1443+
set.insert(paths[500].as_path());
1444+
});
1445+
}

0 commit comments

Comments
 (0)