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 11 commits into
rust-lang:mainfrom
asder8215:components_rewrite
Open

Split Components iterator to prefixed and non-prefixed versions and optimize Components for non-prefix path based platforms#156496
asder8215 wants to merge 11 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 entirely changes how Components<'_> is implemented. Currently, the Components<'_> iterator 'consumes' components through mutating its path field to a subslice that presents the left over unconsumed path components (this consumed path component is what's returned in Components::next or Components::next_back). However, this PR keeps the path field alive/unmodified and uses front and back indexing strategy to extract consumed/unconsumed components. This Components<'_> implementation still uses a subslicing approach similar to the original implementation, but it's highly optimized for non-Windows platform, given that there's no Prefix<'_> components to check with.

Ideally, at least from a few benchmarking test I've done locally, this Components<'_> implementation should perform similarly with the original one when it comes to Components::next/Components ordering comparisons (the original one was already highly optimized as is). Where this non-Windows implementation shine though is improving Components::as_path (as a result certain things benefit from this like Ancestors iterator, std::fs::create_dir_all, etc.) and Components::next_back (and as a result Path equality).

There are a lot of different things I've done to make sure that Components equality is improved dramatically. For example, just like what Components comparison does with checking if we see a mismatched byte in the forward direction and setting our Components iterator to the nearest separator, we do the same thing with Components equality in the opposite direction (this actually is one of the significant reason why this implementation of Components equality is really, really faster now). Additionally, there are other optimizations I do here, like doing less boolean checks or usize comparisons within Components::next_back and normalizing any separator/curr directory bytes upfront (the original implementation had a lot of conditional checks that could be way more simplified; the prefix checking also didn't benefit non-Windows platform). With the way I have State enum implemented in this Components iterator, I can do other small optimizations like checking for empty paths, so that I can return true or false immediately.

Because the implementation I'm going for is specific to non-Windows/non-prefix-supporting platforms, I chose to split the Components iterator implementation to two different version that is re-exported accordingly to individual platforms (this leads to HAS_PREFIXES constant being redundant and unnecessary). I'm open to discussion on how to organize the code here better, naming stuff, etc. Some prefix-specific things have been refactored to be exclusively on platforms that utilize prefix components.

So far, I've been benchmarking it locally. I formerly did it with Criterion, but now I've just been benchmarking manually through hyperfine. What I've been testing was how fast can this implementation ofComponents::next/Components::next_back run paths like "a1..aN/a1..aN" vs "b/a1..aN/a1..aN/" (where N is some number of a's I put for each component, and you can assume that there are some X path components in the path); I also played around with the mismatching component placed at the end of the path versus beginning, and benchmarked running the same path equality/comparisons 10000 times to see the result cumulatively. You can try hyperfining the code in this repo; I'll get around to consolidating those benchmarks in Criterion a bit later and then posting results in the comments below.

@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
@asder8215

asder8215 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Pass something in that amounts to Into<Components> + AsRef<OsStr> and then only run the fastpath once. For a Components the into would be a noop, for the other cases it would defer the construction of components until after the fastpath.

I think I'm having a hard time seeing how this would work with the PartialEq trait impl for Path/PathBuf. I was actually thinking of doing something a bit more direct after realizing a redundant clone:

impl PartialEq for Path {
    #[inline]
    fn eq(&self, other: &Path) -> bool {
        // Identical byte sequences always produce identical component sequences,
        // so skip the more expensive component-wise comparison in that case.
        self.as_os_str() == other.as_os_str() || Iterator::eq(self.components().rev(), other.components().rev())
    }
}

(same thing for PathBuf)

Because PartialEq for Components takes in a &Components<'a> (for self and other), in order for us to do a component base comparison using Iterator::eq we need to clone Components for it to be able to mutate Components with Components::next_back. But Path::components already gives us an owned Components, so... we don't want that redundant clone that the PartialEq trait impl from Components struct does when it falls down from the fast path (i.e. when they're unequal, or one path requires normalization to determine equality).

That kinda also means that the current Path equality from the rust repo has been doing an unnecessary 64 byte clone when the fast path fails...

…yte equality and Iterator::eq directly.

Co-authored-by: =?UTF-8?q?=E7=A6=BE=E5=8F=AF?= <chengkelfan@qq.com>
@asder8215
asder8215 force-pushed the components_rewrite branch from c10aaf6 to 56a6c63 Compare July 8, 2026 19:56
@rustbot

rustbot commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@asder8215

Copy link
Copy Markdown
Contributor Author

@clarfonthey, there's something else I realized with this PR Components struct is that I could simplify the struct with using a *const u8 pointer for path_bytes field instead of a &'a [u8] slice. I don't really need the len portion of the slice since that's accounted through front/back indexing.

This would reduce the Components struct by 8 bytes, but it does require a lot of unsafe usage of slice::from_raw_parts and dereferencing a *const u8 pointer. It would also mean that we would have to do a manual while loop instead of iterator methods for things like normalizing the path or finding the next separator, but these while loops should still be able to be auto-vectorized.

What do you think about this?

@rust-log-analyzer

This comment has been minimized.

…path check to Components/Path equality by finding first mismatched byte, separated Components iterator into different versions and re-exported them accordingly based on target os. This also ends up removing HAS_PREFIXES because it becomes unused.
@asder8215
asder8215 force-pushed the components_rewrite branch from 2c07391 to 1153b10 Compare July 16, 2026 13:32
@asder8215
asder8215 requested a review from clarfonthey July 16, 2026 14:57
@asder8215

asder8215 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@clarfonthey You might like the results of this Components iterator now. Since I introduced a fast path for Components/Path equality where it checks for mismatched byte from right to left (opposite of what Components ordering does), it significantly improves the performance for equality.

And with bringing this back to doing the same subslicing approach, but written in a more simplified way and normalizing bytes upfront, as far as I can tell, we pretty much have the same performance for Components::next (maybe slightly better for certain cases?)

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 17, 2026
Redesigned `Components` iterator to use front and back indexing instead mutating and subslicing path field
@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=dist-*

Just in case any other errors pop up on other platforms.

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 17, 2026
Redesigned `Components` iterator to use front and back indexing instead mutating and subslicing path field


try-job: dist-*
@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 Jul 17, 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 ureq v3.0.8
   Compiling citool v0.1.0 (/home/runner/work/rust/rust/src/ci/citool)
    Finished `dev` profile [unoptimized] target(s) in 38.84s
     Running `target/debug/citool calculate-job-matrix`
Run type: TryJob { job_patterns: Some(["dist-*"]) }
Error: Failed to calculate job matrix

Caused by:
    It is only possible to schedule up to 20 custom jobs, received 42 custom jobs expanded from 1 pattern(s)
##[error]Process completed with exit code 1.
Post job cleanup.

@rust-bors

rust-bors Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

💔 Test for 8b548fb failed: CI. Failed job:

@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=dist-*

I'll try running it again, I didn't see the bors message on phone when I ran the try command without the jobs parameter, so I thought I ran the incorrect command without specifying a jobs parameter.

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 17, 2026
Redesigned `Components` iterator to use front and back indexing instead mutating and subslicing path field


try-job: dist-*
@rust-bors

rust-bors Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

💔 Test for 9f2eefd failed: CI. Failed job:

@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 askama v0.16.0
   Compiling citool v0.1.0 (/home/runner/work/rust/rust/src/ci/citool)
    Finished `dev` profile [unoptimized] target(s) in 20.74s
     Running `target/debug/citool calculate-job-matrix`
Run type: TryJob { job_patterns: Some(["dist-*"]) }
Error: Failed to calculate job matrix

Caused by:
    It is only possible to schedule up to 20 custom jobs, received 42 custom jobs expanded from 1 pattern(s)
##[error]Process completed with exit code 1.
Post job cleanup.

@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try

Let's try this.

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 17, 2026
Redesigned `Components` iterator to use front and back indexing instead mutating and subslicing path field
@asder8215 asder8215 changed the title Redesigned Components iterator to use front and back indexing instead mutating and subslicing path field Split Components iterator to prefixed and non-prefixed versions and optimize Components for non-prefix path based platforms Jul 17, 2026
@rust-bors

rust-bors Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: b57b790 (b57b79012db02458947fa2fc8470de2fd212c176)
Base parent: 3d50c25 (3d50c25bc66853bf0ad205529d0f305a1d841b5e)

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

Labels

S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. 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.

8 participants