Skip to content

perf: reuse owned buffers on dirty normalize and relative fallbacks#46

Draft
hyfdev wants to merge 15 commits into
mainfrom
opt/general-path-sweep
Draft

perf: reuse owned buffers on dirty normalize and relative fallbacks#46
hyfdev wants to merge 15 commits into
mainfrom
opt/general-path-sweep

Conversation

@hyfdev

@hyfdev hyfdev commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Status: Draft — waiting to validate whether the architecture of this change is reasonable before merging.

Summary

Sweep of remaining general-path allocation headroom after #44:

  1. Owned dirty normalize reuse — bounded dirty paths rebuild into the original PathBuf allocation through a stack component arena. On non-Unix targets, complete native-encoding fragments are held by a branded append-only arena and written through safe OsString operations; Unix rebuilds use safe OsStringExt APIs.
  2. Relative absolute fallback — retain Cow<Path> instead of forcing a clean base into an owned buffer.
  3. Allocation boundary protection — rely on PathBuf::push for absolutize_with growth and cover exact-capacity, 24/25/33-component, bounded replay, and long Windows UNC boundaries.
  4. Borrowed normalize stability — in an equivalent CodSpeed build, keep the Unix borrowed normalizer's opcode sequence and out-of-line call boundary the same as main while compiling the consuming owned path independently; address-dependent layout may differ.
  5. Native encoding safety boundary — deny unsafe code by default, narrow the sole path-encoding exception to the exact reconstruction statement, and exercise invalid Unix bytes plus Windows surrogate boundaries through native CI.
  6. Bounded non-Unix fallback work — keep 32 live components inline. On the first overflow, profile the input once; use allocation-free replay only within 64 parsed components and 512 encoded bytes, otherwise reserve the component stack once for the profiled maximum live depth so normalization remains linear.

Hard-gate allocation moves

Row before after
owned dirty / collapsing into_normalized 1 0
dirty join owned → PathBuf / String 2 1
absolutize_with relative + borrowed cwd 2 1
absolutize_with relative + owned cwd 1 0
invalid-encoding absolute relative 3 2

Clean-path zero-allocation gates remain unchanged. Unequal relative inputs remain at one allocation from #44.

Boundaries

  • Owned dirty normalization reuses the existing buffer for inputs up to 512 encoded bytes and a maximum live depth of 24 normal components when capacity fits.
  • Dirty owned inputs that cannot use that arena use a separate output allocation. On non-Unix targets, the first overflow past 32 live components profiles the input once. Inputs within 64 parsed components and 512 encoded bytes then use bounded allocation-free replay; inputs outside either bound reserve the component stack once for the profiled maximum live depth and finish in linear total work without growing that stack again.
  • Exact-capacity owned cwd cases add no allocation or reallocation.
  • Windows 24-component paths reuse the original buffer; the 25- and 33-component fallbacks use one output allocation. The 64-component replay boundary also uses one output allocation, while 65 components and a long 33-component input intentionally add one temporary stack allocation and no reallocation.
  • In an equivalent CodSpeed build, the Unix borrowed normalizer retains main's opcode sequence and out-of-line call boundary; address-dependent layout and immediates may differ. The consuming normalizer intentionally uses the new owned-buffer fast path.
  • Native path reconstruction has one private path-encoding unsafe statement, and its lint exception applies only to that statement. Its unforgeable fragments always cover complete OsStr::as_encoded_bytes() values; callers only use safe APIs.

Intentionally not changed

  • Dirty join through borrowed normalize
  • Borrowed collapsing normalize().into_owned()
  • Fused relative-to-string / ArcStr APIs
  • Additional SIMD

Test plan

  • cargo test --locked --workspace --all-features
  • Workspace/all-target Clippy with -D warnings
  • Documentation with RUSTDOCFLAGS=-D warnings
  • Windows MSVC/GNU, UEFI, and WASI library cross-target checks
  • Unix arbitrary-byte and Windows unpaired-surrogate normalization matrices
  • Native Windows deep-spill cases covering 32/33 oscillation, cancellation, drive-like first survivors, invalid Verbatim UNC prefixes, and trailing separators
  • Test-only profile counter proving one profile pass per 64/65-component, >512-byte, and 32/33-oscillation normalization
  • Native Linux and Windows allocation snapshots cover the 24/25/33-component boundaries; Windows additionally covers 64/65 components and a >512-byte 33-component spill
  • Exact 512/513-byte correctness and owned-buffer reuse boundaries
  • Linux, macOS, and Windows CI tests
  • CodSpeed simulation, memory, and performance analysis

@codspeed-hq

codspeed-hq Bot commented Jul 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 41%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 16 improved benchmarks
✅ 212 untouched benchmarks
⏩ 38 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation normalize/current_directory_spellings[empty] 1,112.8 ns 243.3 ns ×4.6
Memory owned_receiver/pathbuf_result 154 B 77 B ×2
Memory owned_receiver/string_result 154 B 77 B ×2
Memory relative_dirty 149 B 76 B +96.05%
Simulation owned_receiver/pathbuf_result 1,276.1 ns 996.4 ns +28.07%
Simulation normalize/current_directory_spellings[dot] 439.2 ns 343.9 ns +27.71%
Simulation normalize/current_directory_spellings[dot_separator] 440 ns 344.7 ns +27.64%
Simulation non_utf8_lexically_clean 1,010 ns 855.8 ns +18.01%
Simulation normalize/clean_rolldown[module_loader_hot] 1,057.2 ns 903.6 ns +17%
Simulation normalize/leading_parent_prescan[clean_68b] 1,100 ns 946.4 ns +16.23%
Simulation owned_receiver/pathbuf_result 3.4 µs 2.9 µs +15.62%
Simulation normalize/clean_rolldown[fixture_p50] 1,079.2 ns 954.7 ns +13.03%
Simulation string 255.8 ns 226.7 ns +12.87%
Simulation borrowed_receiver/pathbuf_result 1.7 µs 1.5 µs +12.3%
Simulation normalize/clean_rolldown[short_source] 974.2 ns 878.9 ns +10.84%
Simulation normalize/needs_work[trailing_separator] 995.6 ns 900.3 ns +10.58%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing opt/general-path-sweep (691a4fd) with main (c387d72)2

Open in CodSpeed

Footnotes

  1. 38 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (2c2b185) during the generation of this report, so c387d72 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@hyfdev
hyfdev marked this pull request as ready for review July 12, 2026 10:42
Copilot AI review requested due to automatic review settings July 12, 2026 10:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR continues the crate’s performance work on general-path operations by reducing heap allocations in owned-path normalization and relative-path fallbacks, and by updating allocation baselines/docs to reflect the improved call counts.

Changes:

  • Reworks owned PathBuf normalization to rebuild “dirty” paths into the original buffer using a small stack arena (with a fallback to the existing normalize_inner path for longer/complex cases).
  • Adjusts relative slow-path plumbing to retain Cow<Path> longer, avoiding unnecessary .into_owned() clones in absolute fallbacks.
  • Adds pre-reserve in absolutize_with for the appended relative suffix and updates allocation snapshot gates + performance strategy doc.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/impl_sugar_path.rs Introduces owned-buffer reuse for dirty normalize; keeps Cow in relative fallbacks; reserves capacity in absolutize_with.
benchmarks/allocations/x86_64-unknown-linux-gnu.snap Updates allocation hard-gate snapshot rows to reflect reduced allocations.
benchmarks/allocations/x86_64-pc-windows-msvc.snap Updates allocation hard-gate snapshot rows to reflect reduced allocations (including Windows-specific rows).
.agents/docs/performance-strategy.md Records the newly achieved allocation reductions and clarifies remaining optimization targets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/impl_sugar_path.rs Outdated
@hyfdev
hyfdev marked this pull request as draft July 13, 2026 03:18
hyfdev added 14 commits July 15, 2026 17:54
Rewrite short dirty into_normalized results into the existing PathBuf
storage, so dirty join and absolutize_with drop an intermediate alloc.
Keep Cow on the absolute relative fallback so clean bases are not
cloned against invalid-encoding targets. Update allocation hard-gate
call counts for the intentional rows.
Pass Cow into relative_from_resolved so Windows different-prefix and
unrepresentable-suffix returns use into_owned (free on Cow::Owned).
Update hard-gate rows for owned invalid into_normalized (0) and Windows
root-relative relative (1).
…w ends

Avoid moving PathBuf while path.components() is live; set a flag and fall
back to normalize_inner only after the iterator is dropped.
Adversarial review flagged the dual dirty-normalize algorithms with only
three fixtures. Expand the corpus and add overflow-fallback cases
(depth > 24 and length > 512) that must match borrowed normalize.
Document the std encoding contract and that only as_encoded_bytes plus
ASCII separators/dots feed the unchecked OsString constructors.
@hyfdev
hyfdev force-pushed the opt/general-path-sweep branch from d5cfb81 to bba4bfc Compare July 15, 2026 10:00
@hyfdev
hyfdev force-pushed the opt/general-path-sweep branch 2 times, most recently from 64f4f7f to 691a4fd Compare July 15, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants