Skip to content

Split Components iterator to prefixed and non-prefixed versions and optimize Components for non-prefix path based platforms - #156496

Open
asder8215 wants to merge 15 commits into
rust-lang:mainfrom
asder8215:components_rewrite
Open

asder8215 wants to merge 15 commits into
rust-lang:mainfrom
asder8215:components_rewrite

Conversation

@asder8215

@asder8215 asder8215 commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

View all comments

This PR fixes #154521 specifically for Unix/non-prefixed platforms. By non-prefixed filepath platforms this includes:

  • all(target_vendor = "fortanix", target_env = "sgx")
  • not(any(target_os = "cygwin", target_os = "uefi", target_os = "solid_asp3", target_os = "windows") (the default/fallback _ path in sys/path/mod.rs, which notably includes Unix platforms)

For every other prefixed filepath platforms (notably Windows), the Components iterator implementation remains largely unchanged (just some minor refactoring).

The point of the non-prefixed Components implementation is to:

  1. Gravitate away from accounting for the Prefix components that Windows/other prefixed-based filepath platform use as Unix-based platforms do not have those prefix components in their filepaths;
  2. Optimize the whole Components iterator traversal algorithm as well as improve Path equality and Path comparison

From benchmarking, I noticed that all the Prefix checking/returning code specific for Windows affects how Components perform on Unix, though to be fair, part of the reason that is the case is because the Prefix enum takes up 40 bytes to represent (which we shouldn't want Unix to take the consequence of handling Prefix). There are other factors in the current Components::next/Components::next_back implementation that could've been optimized better or written in a way that would enable the compiler to optimize that code better (e.g. autovectorization optimizations). It's certainly possible that the prefixed/current Components implementation could be enhanced to make performance better on Windows, but I will leave that to someone who can verifiably test and benchmark those changes on Windows.

In terms of the optimizations I made for the non-prefixed Components, a smaller one I could talk about first is that the size of Components has been reduced. In the current/original Components we have this:

#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
enum State {
    Prefix = 0,   // c:
    StartDir = 1, // / or . or nothing
    Body = 2,     // foo/bar/baz
    Done = 3,
}

#[derive(Clone)]
pub struct Components<'a> {
    // The path left to parse components from
    path: &'a [u8],

    // The prefix as it was originally parsed, if any
    prefix: Option<Prefix<'a>>,

    // true if path *physically* has a root separator; for most Windows
    // prefixes, it may have a "logical" root separator for the purposes of
    // normalization, e.g., \\server\share == \\server\share\.
    has_physical_root: bool,

    // The iterator is double-ended, and these two states keep track of what has
    // been produced from either end
    front: State,
    back: State,
}

This overall takes up 64 bytes to represents Components

Now, without the need to account for Prefix, we can simplify the Components struct to:

#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
enum State {
    Absolute = 1, // A root component (i.e. '/')
    Relative = 2, // A relative component ('foo')
    Done = 3,     // Iterator is fully consumed
}

#[derive(Clone)]
pub struct Components<'a> {
    // The path left to parse components from
    path: &'a [u8],
    // The current state of the iterator
    state: State,
}

This prefix-less Components takes up 24 bytes to represent.

Implementation-wise, Components::next and Components::next_back takes a similar approach to the original Components iterator by subslicing the path field to present the left over unconsumed path. However, one of the key differences with this Components::next and Components::next_back is that we normalize away in between and trailing separator + cur dir bytes after parsing the component (e.g. "foo///bar", when we parse "foo" via Components::next or "bar" via Components::next_back", the in between separator bytes will be subsliced from the path field in preparation for the next Components::next/Components::next_back). Normalizing away these bytes in preparation for the next Components::next/Components::next_back were not done in the original implementation, which meant that while !self.is_finished loop in the original ran through multiple iterations to actually normalize away those separator bytes and find the next component to parse. Moreover, I relied on iterator methods to make that normalization process really fast, as it appears to be autovectorized and optimized really well by the compiler. Also noteworthy is that the optimization made to normalizing away in between/trailing separator/cur dir bytes using iterator methods enhances the performance of Components::as_path significantly.

As for Path equality and Path comparison, which largely rely on Components underneath the hood, there are many optimizations we introduce there to increase performance.

Starting with Path equality, we check for the following:

  • Introduced by @Lfan-ke, we check directly if the two Paths when converted as an OsStrs are equal. If so, we can just return true immediately. Otherwise, we have to hinge on Components (e.g. of "/foo" is equivalent to "/foo/").
  • We have a fast path in checking if either the left path or right path an empty path? We can easily use Components state field to determine if we're looking at an empty path (reading 1 byte via the State::Done enum rather than directly looking at each path's length)
  • We use iterator methods to check for the first mismatching byte between the left and right path. So long as the mismatching byte is not a separator or cur dir byte on either end, we can directly return false.
  • Otherwise, we find the nearest separator byte from the right (also using iterator methods) for both to readjust their Components path field and just rely on Components::next_back to handle the rest on determining whether the two paths are equivalent or not.

As for Path comparison, we do the following:

  • Check if the two paths are equal. If so, we can return Ordering::Equal immediately.
  • We then use iterator methods to check for the first mismatching byte between the left and right path. So long as the mismatching byte is not a separator or cur dir byte on either end, we can directly compare the byte and return Ordering::Greater or Ordering::Less as appropriate).
  • Otherwise, we find the nearest separator byte from the left (also using iterator methods) for both to readjust their Components path field and just rely on Components::next to handle the rest on determining whether to return Ordering::Equal, Ordering::Less, or Ordering::Greater.
    • Note: this was already done from the original Components implementation, so this code/idea was just taken from there.

All of these enhances the performance of Path equality and comparison significantly, which you can see from benchmarking results.

Regarding benchmarking, I did all benchmarking and experimentation on this Components implementation within a separate repository. You can take a look here to see the raw and markdown formatted results and the methodology I chose to go about benchmarking this Components iterator implementation.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels May 12, 2026
@rustbot

rustbot commented May 12, 2026

Copy link
Copy Markdown
Collaborator

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ChrisDenton, libs
  • @ChrisDenton, libs expanded to 8 candidates

@rustbot

This comment has been minimized.

@asder8215
asder8215 force-pushed the components_rewrite branch from 1627e2f to 33e69e1 Compare May 12, 2026 09:09
@rustbot

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the components_rewrite branch from 33e69e1 to ed9d33d Compare May 12, 2026 17:05
@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the components_rewrite branch from ed9d33d to 0b0f84c Compare May 12, 2026 17:19
@rust-log-analyzer

This comment has been minimized.

@asder8215
asder8215 force-pushed the components_rewrite branch from 0b0f84c to 8ed33ea Compare May 12, 2026 22:05
@asder8215

This comment was marked as outdated.

@asder8215
asder8215 force-pushed the components_rewrite branch from 2151b8f to 83cdbed Compare May 13, 2026 22:21
@asder8215

This comment was marked as outdated.

@asder8215
asder8215 force-pushed the components_rewrite branch from 83cdbed to 3921fff Compare May 15, 2026 00:30
@asder8215
asder8215 marked this pull request as draft May 16, 2026 12:22
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 16, 2026
@asder8215
asder8215 force-pushed the components_rewrite branch from 0a25dda to 92e0132 Compare May 17, 2026 16:09
@asder8215

asder8215 commented May 17, 2026 •

Copy link
Copy Markdown
Contributor Author

New benchmarking results. You can see what the benchmark code looks like here and run it yourself to see if there are any difference in measurements on your end:

This is the measurement of the current implementation of Components<'_> (without black box):

Std Components (No BB)  time:   [21.546 µs 21.800 µs 22.096 µs]
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) high mild
  1 (1.00%) high severe

Std Components Next (No BB)
                        time:   [20.434 µs 20.482 µs 20.538 µs]
Found 7 outliers among 100 measurements (7.00%)
  5 (5.00%) high mild
  2 (2.00%) high severe

Std Components Next Back (No BB)
                        time:   [38.367 µs 38.757 µs 39.199 µs]
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe

Std Path Iter (No BB)   time:   [21.547 µs 21.730 µs 21.921 µs]

Std As Path Iter (No BB)
                        time:   [87.680 µs 88.439 µs 89.231 µs]
Found 6 outliers among 100 measurements (6.00%)
  2 (2.00%) high mild
  4 (4.00%) high severe

Std Eq Comps (No BB)    time:   [591.21 ns 593.35 ns 595.82 ns]
Found 16 outliers among 100 measurements (16.00%)
  1 (1.00%) low severe
  3 (3.00%) low mild
  7 (7.00%) high mild
  5 (5.00%) high severe

Std Uneq Comps (No BB)  time:   [60.953 ns 61.419 ns 61.911 ns]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

Std Uneq 2 Comps (No BB)
                        time:   [75.454 µs 75.734 µs 76.027 µs]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

Std Compare Comps (No BB)
                        time:   [46.182 µs 46.621 µs 47.192 µs]
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe

Std Compare Uneq Comps (No BB)
                        time:   [46.679 µs 46.980 µs 47.291 µs]
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe

Std Compare Uneq 2 Comps (No BB)
                        time:   [41.480 ns 41.827 ns 42.160 ns]
Found 3 outliers among 100 measurements (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe

This is the measurement of the new implementation of Components<'_> I'm working on (without black box):

Components Rewrite (No BB)
                        time:   [24.982 µs 25.267 µs 25.570 µs]

Components Next Rewrite (No BB)
                        time:   [24.388 µs 24.655 µs 24.937 µs]
Found 6 outliers among 100 measurements (6.00%)
  6 (6.00%) high mild

Components Next Back Rewrite (No BB)
                        time:   [18.184 µs 18.567 µs 19.034 µs]
Found 16 outliers among 100 measurements (16.00%)
  1 (1.00%) high mild
  15 (15.00%) high severe

Path Iter Rewrite (No BB)
                        time:   [23.485 µs 23.659 µs 23.829 µs]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

As Path Iter Rewrite (No BB)
                        time:   [22.936 µs 23.066 µs 23.208 µs]
Found 7 outliers among 100 measurements (7.00%)
  3 (3.00%) high mild
  4 (4.00%) high severe

Eq Comps Rewrite (No BB)
                        time:   [605.12 ns 608.83 ns 612.98 ns]
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) low mild
  7 (7.00%) high mild
  3 (3.00%) high severe

Uneq Comps Rewrite (No BB)
                        time:   [31.799 ns 32.108 ns 32.433 ns]
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild

Uneq Comps 2 Rewrite (No BB)
                        time:   [47.091 µs 48.186 µs 49.085 µs]

Compare Comps Rewrite (No BB)
                        time:   [50.234 µs 50.725 µs 51.254 µs]
Found 10 outliers among 100 measurements (10.00%)
  9 (9.00%) high mild
  1 (1.00%) high severe

Compare Uneq Comps Rewrite (No BB)
                        time:   [49.262 µs 49.631 µs 50.067 µs]
Found 16 outliers among 100 measurements (16.00%)
  4 (4.00%) high mild
  12 (12.00%) high severe

Compare Uneq Comps 2 Rewrite (No BB)
                        time:   [43.397 ns 43.767 ns 44.171 ns]
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild

Edit: Updated Components::as_path to match on Option<FirstComponent>/self.first_comp instead of using if let Some(_) = self.first_comp and matching on that, benchmarking for this PR Components<'_> has been updated as a result. Everything else is unaffected by this change.

@asder8215

asder8215 commented May 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Here are the benchmark results with black box:

From current Components<'_> implementation:

Std Components          time:   [20.947 µs 21.010 µs 21.084 µs]
Found 8 outliers among 100 measurements (8.00%)
  4 (4.00%) high mild
  4 (4.00%) high severe

Std Components Next     time:   [20.967 µs 20.993 µs 21.021 µs]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

Std Components Next Back
                        time:   [35.715 µs 35.802 µs 35.925 µs]
Found 20 outliers among 100 measurements (20.00%)
  6 (6.00%) high mild
  14 (14.00%) high severe

Std Path Iter           time:   [20.883 µs 20.992 µs 21.152 µs]
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe

Std As Path Iter        time:   [80.673 µs 80.935 µs 81.261 µs]
Found 9 outliers among 100 measurements (9.00%)
  6 (6.00%) high mild
  3 (3.00%) high severe

Std Eq Comps            time:   [589.43 ns 593.36 ns 597.88 ns]
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) low severe
  2 (2.00%) low mild
  1 (1.00%) high severe

Std Uneq Comps          time:   [63.919 ns 64.262 ns 64.765 ns]
Found 10 outliers among 100 measurements (10.00%)
  6 (6.00%) high mild
  4 (4.00%) high severe

Std Uneq 2 Comps        time:   [75.284 µs 75.939 µs 76.599 µs]
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high severe

From this Components<'_> implementation PR:

Components Rewrite      time:   [24.190 µs 24.425 µs 24.687 µs]
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) high mild
  1 (1.00%) high severe

Components Next Rewrite time:   [24.230 µs 24.550 µs 24.889 µs]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

Components Next Back Rewrite
                        time:   [17.339 µs 17.488 µs 17.655 µs]
Found 5 outliers among 100 measurements (5.00%)
  3 (3.00%) high mild
  2 (2.00%) high severe

Path Iter Rewrite       time:   [23.845 µs 23.996 µs 24.154 µs]
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

As Path Iter Rewrite    time:   [22.431 µs 22.676 µs 23.010 µs]
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) high mild
  2 (2.00%) high severe

Eq Comps Rewrite        time:   [586.16 ns 588.10 ns 590.14 ns]

Found 5 outliers among 100 measurements (5.00%)
  2 (2.00%) low mild
  2 (2.00%) high mild
  1 (1.00%) high severe

Uneq Comps Rewrite      time:   [31.733 ns 32.023 ns 32.378 ns]

Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) high mild
  6 (6.00%) high severe

Uneq 2 Comps Rewrite    time:   [36.318 µs 36.574 µs 36.913 µs]
Found 23 outliers among 100 measurements (23.00%)
  23 (23.00%) high severe

Edit: Updated Components::as_path to match on Option<FirstComponent>/self.first_comp instead of using if let Some(_) = self.first_comp and matching on that, benchmarking for this PR Components<'_> has been updated as a result. Everything else is unaffected by this change.

Edit 2: Took off Path ordering benchmark here since it was incorrect see below to see corrected path ordering benchmarks.

@asder8215
asder8215 force-pushed the components_rewrite branch from 92e0132 to 574d7f2 Compare May 17, 2026 18:41
@asder8215
asder8215 marked this pull request as ready for review May 17, 2026 18:59
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels May 17, 2026
@asder8215

asder8215 commented May 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm confident this code works (passed CI in previous run, the current amended commit change I made doesn't change logic, but makes the code written in a more idiomatic way).

In my opinion, the logic in this code should look more readable than how Component<'_> is currently written implemented as. From benchmarking, we can see that Components::next_back, Components::as_path, and in the cases where Components equality falls down to using Components::next_back (when they are unequal or equality can't be determine unless one/both Components<'_> are normalized), this PR implementation of Components<'_> is faster than how it's currently implemented as. The trade off is that this PR implementation of Components<'_> has a slight reduction in performance for Components::next and as a result Components<'_> comparison, but I would take this slight reduction in performance to make path equality faster.

@asder8215

asder8215 commented May 19, 2026 •

Copy link
Copy Markdown
Contributor Author

@rustbot label +I-libs-nominated

Since Components<'_> is pretty well-used in many other methods, I think this may need discussion from the libs team on whether the re-implementation of Components<'_> here is okay/valid to take over the current implementation (and the trade-off between faster Components::next_back with a slight reduction in performance in Components::next). I wasn't sure if this should be labeled as I-libs-api-nominated or not since it pertains to an existing stable feature than a new feature.

@rustbot rustbot added the I-libs-nominated Nominated for discussion during a libs team meeting. label May 19, 2026
// and will be handled below.
parse_prefix(other_path.as_os_str()).is_some()
} else {
// On Unix: prefix is always None.

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Irrelevant here, since this is Windows code.

View changes since the review

self_path.inner = res;
return;

// `path` has a root but no prefix, e.g., `\windows` (Windows only)

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again, (Windows only) is irrelevant here, I think?

View changes since the review

Comment thread library/std/src/path.rs Outdated
Comment on lines +326 to +320
/// Says whether the first byte after the prefix is a separator.
fn has_physical_root(s: &[u8], prefix: Option<Prefix<'_>>) -> bool {
let path = if let Some(p) = prefix { &s[p.len()..] } else { s };
!path.is_empty() && is_sep_byte(path[0])
}
// /// Says whether the first byte after the prefix is a separator.
// fn has_physical_root(s: &[u8], prefix: Option<Prefix<'_>>) -> bool {
// let path = if let Some(p) = prefix { &s[p.len()..] } else { s };
// !path.is_empty() && is_sep_byte(path[0])
// }

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can just be deleted?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My bad, will delete this soon.

Comment on lines +62 to +74
match self.path[front..].iter().position(|b| {
if !is_sep_byte(*b) {
if *b == b'.' && !cur_dir_present {
cur_dir_present = true;
false
} else {
true
}
} else {
cur_dir_present = false;
false
}
}) {

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gonna be honest, I kind of hate this logic, mostly because having stateful position code is just very confusing to reason about.

I kind of would rather you just use a for loop here to figure this out.

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(Note: it is certainly clever, but looking at this code makes my brain hurt, and I'm not sure it actually has any performance justifications.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will have to check if rewriting it as a for loop here will affect performance. Unsure if LLVM will optimize it as well as the iter version.

I think I might have start off with understanding how position iterates underneath the hood. The thing I'm worried about implementing this in a for loop is ensuring that I'm implementing this in a way that it gets autovectorized really well; I believe any incrementation with front here has to be done in the end so you prevent subsequent iterations from having a dependency on the previous iteration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Honestly, I'm kind of surprised this autovectorises very well. If there are compelling performance benefits I'm fine keeping it, just, it feels like there might be a nicer way of doing this.

@asder8215 asder8215 Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Honestly, I'm kind of surprised this autovectorises very well.

I took a look at what position does underneath the hood:

fn position<P>(&mut self, mut predicate: P) -> Option<usize> where
Self: Sized,
P: FnMut(Self::Item) -> bool,
{
let n = len!(self);
let mut i = 0;
while let Some(x) = self.next() {
if predicate(x) {
// SAFETY: we are guaranteed to be in bounds by the loop invariant:
// when `i >= n`, `self.next()` returns `None` and the loop breaks.
unsafe { assert_unchecked(i < n) };
return Some(i);
}
i += 1;
}
None
}

Okay, so I think I get what it's autovectorizing. This part of the code:

match self.path[front..].iter().position(|b| {
            if !is_sep_byte(*b) {
                if *b == b'.' && !cur_dir_present {
                    cur_dir_present = true;
                    false
                } else {
                    true
                }
            } else {
                cur_dir_present = false;
                false
            }
        })

This is based on my own assumption on what could happen underneath the hood (haven't actually looked at machine instructions), but what I can see here that could be done in parallel by the CPU/compiler optimizations is: fetching the next byte in the self.path via .next() method, checking if that byte is a separator from !is_sept_byte(*b), and as well as checking if the byte is a cur dir byte from *b == b'.'; none of those portions of the code have a dependency on previous iterations. Unfortunately, the !cur_dir_present condition is dependent on previous iterations, as we set the cur_dir_present variable to true if it's currently false and we see that the byte we're looking at is a cur dir byte, so some sort of synchronization has to occur over there. That's unavoidable since we want to normalize away non-starting cur dir components, but not normalize things like "..".

However, there is maybe one more thing that the CPU could do in parallel that we can optimize. In Iter::position, it's incrementing a variable i by 1 in each loop. However, technically, we don't need a local variable that's incremented by 1 in each loop as that forces subsequent iterations to depend on previous iterations for that i to be incremented appropriately. Since we know the bounds of our indices from self.path[front..], it should be pretty clear to the compiler that if we enumerated both the byte and index, we could do this in parallel fine. This is how I'm thinking about the for loop version right now:

fn normalize_front(&mut self, front: usize) -> usize {
        let mut cur_dir_present = false;

        for (index, &byte) in self.path[front..].iter().enumerate() { // could be done in parallel by CPU
            if !is_sep_byte(byte) { // could be done in parallel by CPU
                if byte == b'.' && !cur_dir_present { // first condition parallel, second not parallel
                    cur_dir_present = true;
                } else {
                    if cur_dir_present {
                        // Minus 1 to reorient the path back to the starting the "." in e.g. ".a" or "
                        return front + index - 1;
                    } else {
                        return front + index;
                    }
                }
            } else {
                cur_dir_present = false;
            }
        }
       
        // Consumed the iterator, so there's nothing left to normalize
        self.state = State::Done;
        self.path.len()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If there are compelling performance benefits I'm fine keeping it, just, it feels like there might be a nicer way of doing this.

So the for loop way I showed earlier does not run as fast as the iter version. I haven't run all the benchmark cases for all different component byte sizes, but it's very telling to see that performance was being dropped noticeably across all the cases for 1 and 3 byte components (e.g. 150 ns original to 200 ns using this for loop on running the components next iter case with a 1 byte component). I even tried doing this:

fn normalize_front(&mut self, mut front: usize) -> usize {
        for &byte in self.path[front..].iter() {
            if !is_sep_byte(byte) {
                if byte == b'.' && !cur_dir_present {
                    cur_dir_present = true;
                } else {
                    if cur_dir_present {
                        return front - 1;
                    } else {
                        return front;
                    }
                }
            } else {
                cur_dir_present = false;
            }
            front += 1;
        }
        self.state = State::Done;
        self.path.len()
}

Which should be the exact equivalent to what the current iter version I have here is doing, and it still showed some degradation (e.g. 150 ns original to 190 ns using this for loop on running the components next iter case with a 1 byte component). It's still better than the current standard library implementation of Components, but I'm dissatisfied with losing 25-33% performance writing it from this iterator style to imperative style.

I think it's possible there are other compiler optimizations done on the iterator/functional pattern version of the code that makes it faster than the imperative way I was going about things. I know you said you said you would be fine with this if there are compelling performance benefits, but I wanted to just make sure and ask again: are you okay with me keeping this as is? I'm unsure how to go about writing this in an imperative style that achieves the same performance as the iter/functional style. I can also show you the performance results across all the component size if that is something you are curious about and you want to know how big the effects are.

Comment on lines +96 to +107
if !is_sep_byte(*b) {
if *b == b'.' && !cur_dir_present {
cur_dir_present = true;
false
} else {
true
}
} else {
cur_dir_present = false;
false
}
}) {

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto here on just using a for loop.

View changes since the review

Comment on lines +134 to +146
let (done, back) = match self.path.iter().rposition(|b| {
if !is_sep_byte(*b) {
if *b == b'.' && !cur_dir_present {
cur_dir_present = true;
false
} else {
true
}
} else {
cur_dir_present = false;
false
}
}) {

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread library/std/src/sys/path/nonprefixed_paths.rs Outdated
Comment thread library/std/src/sys/path/nonprefixed_paths.rs Outdated
}

/// Prefix-less `Components` equality
pub fn eq_components(mut left: Components<'_>, mut right: Components<'_>) -> bool {

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm just going to leave a comment here indicating I haven't fully wrapped my head around this yet, so that I make sure to review before merging.

View changes since the review

@asder8215 asder8215 Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All good, there's a lot that goes on in here. This may be glossed over on a quick glance, but do note that we're checking the mismatched byte from right to left since it's more common to notice the difference between paths from last components (e.g. "project_name/src/some_file.rs == project_name/src/other_file.rs").

I think the most confusing part that took me some time to debug to realize the issue was the last part checking if the mismatched byte from the left component or right component is a separator byte.

For example, take this case: "foobar/foo/bar" == "barfoo/bar"

The mismatched byte here is the "/" on left and "r" on right. It's important that we capture the full component to ensure that Iter::eq(left.rev(), right.rev()) does comparisons on the right mismatching components (i.e. the "foo" component on left and "barfoo" component on right). This means finding the next separator byte after the mismatched byte.

In "foobar/foo/bar" == "barfoo/bar", our next component position is done relative to where the next separator byte is from the mismatched byte in "barfoo/bar" not "foobar/foo/bar" as finding the next separator byte for "foobar/foo" will just lead us to the same spot it was at. Since we know that the next separator byte for the "r" in "barfoo/bar" is 4 bytes away, we can increment the mismatched byte index for "foobar/foo/bar" by the same amount as next separator byte in "barfoo/bar", slice both Components' path by that index and have Iter::eq(left.rev(), right.rev()) take it away from there.

Comment thread library/std/src/sys/path/nonprefixed_paths.rs Outdated
Comment thread library/std/src/sys/path/nonprefixed_paths.rs Outdated
}
};

if let Some(previous_sep) = left.path[..first_difference].iter().rposition(|&b| is_sep_byte(b))

@clarfonthey clarfonthey Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gonna wrap my head around this later. Mostly just leaving an extra comment.

View changes since the review

@clarfonthey

Copy link
Copy Markdown
Contributor

Okay, this time I've finally (mostly) reviewed this and am cautiously optimistic about the result, especially since you seem to have done a fair bit of benchmarking on this. Funny enough, the unprefixed path logic is the one that caught me the most off guard, and I'm not sure if that's just because you haven't put as much effort into that as the prefixed logic (which is harder to optimize) or if I just reviewed it second and was a bit tired from the first part of the review.

Anyway, I am going to try and get back to reviewing this and properly closing out concerns from earlier in the review and taking a closer look to the benchmarks you've done. Thank you again for putting the work into this, and sorry it took so long to get to!

@rust-log-analyzer

This comment has been minimized.

@asder8215

asder8215 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Pending this try job, if it passes, I think the refactoring and minor updates to the prefixed version of Components should be good to go. I'll also try to update the PR description sometime this week, so that it gives a clean and clear rundown on the two different Components version + optimizations that are made in the nonprefixed version.

@bors try jobs=aarch64-pc-windows-msvc,x86_64-pc-windows-gnu,x86_64-pc-windows-msvc

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Sep 29, 2026
Split `Components` iterator to prefixed and non-prefixed versions and optimize `Components` for non-prefix path based platforms


try-job: aarch64-pc-windows-msvc
try-job: x86_64-pc-windows-gnu
try-job: x86_64-pc-windows-msvc
@rust-bors rust-bors Bot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 29, 2026
@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

A job failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
   Compiling diff v0.1.13
   Compiling citool v0.1.0 (/home/runner/work/rust/rust/src/ci/citool)
    Finished `dev` profile [unoptimized] target(s) in 21.46s
     Running `target/debug/citool calculate-job-matrix`
Run type: TryJob { job_patterns: Some(["aarch64-pc-windows-msvc", "x86_64-pc-windows-gnu", "x86_64-pc-windows-msvc"]), nolimit: false }
Error: Failed to calculate job matrix

Caused by:
    Patterns `aarch64-pc-windows-msvc, x86_64-pc-windows-gnu, x86_64-pc-windows-msvc` did not match any auto jobs
##[error]Process completed with exit code 1.
Post job cleanup.

@rust-bors

rust-bors Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

💔 Test for 86c1566 failed: CI. Failed job:

@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=test-x86_64-msvc-,test-i686-msvc,test-aarch64-msvc-

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Sep 29, 2026
Split `Components` iterator to prefixed and non-prefixed versions and optimize `Components` for non-prefix path based platforms


try-job: test-x86_64-msvc-*
try-job: test-i686-msvc
try-job: test-aarch64-msvc-*
@rust-bors

rust-bors Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: 0b71db5 (0b71db5cb857bee9d456cdbf2f0a028f4d1d5c04)
Base parent: 5d89371 (5d8937173e7c40406c167f4e94737da8925333ad)

@asder8215

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Path comparison is obscenely slow

9 participants