Skip to content

perf: optimize normalize() to avoid allocation for already-clean paths#32

Merged
hyf0 merged 1 commit intomainfrom
hyf_3284290384
Feb 23, 2026
Merged

perf: optimize normalize() to avoid allocation for already-clean paths#32
hyf0 merged 1 commit intomainfrom
hyf_3284290384

Conversation

@hyf0
Copy link
Copy Markdown
Owner

@hyf0 hyf0 commented Feb 23, 2026

Closes #16

Summary

  • normalize() now returns Cow<'_, Path> instead of PathBuf — already-clean paths are returned by reference with zero allocation
  • Added memchr-accelerated needs_normalization() fast-path check (both Unix and Windows)
  • Callers (absolutize_with, relative) use .into_owned() where PathBuf is needed
  • Fixed temporary lifetime issues in tests/deep_paths.rs

Test plan

  • cargo test — all existing + new tests pass
  • cargo fmt --check — clean
  • cargo clippy --all-targets — no warnings
  • cargo bench -- normalize — confirm speedup for already-clean paths

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 23, 2026 12:34
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 23, 2026

Merging this PR will improve performance by ×2.1

⚡ 8 improved benchmarks
✅ 16 untouched benchmarks
⏩ 1 skipped benchmark1

Performance Changes

Benchmark BASE HEAD Efficiency
absolutize_with 13.3 µs 10.4 µs +27.88%
absolutize 27 µs 24.3 µs +11.34%
to_slash_deep_nesting 815.8 ns 728.3 ns +12.01%
normalize_short_clean 5.4 µs 2.8 µs +91%
normalize_mixed_workload 45.3 µs 35.9 µs +26.05%
normalize_deep_clean 9.6 µs 4.6 µs ×2.1
normalize_already_clean_absolute 12.7 µs 6.4 µs +98.16%
normalize_already_clean 23 µs 11.3 µs ×2

Comparing hyf_3284290384 (d139766) with main (ce5e297)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

Copy link
Copy Markdown

Copilot AI left a comment

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 optimizes the normalize() method to avoid allocations for already-clean paths by changing the return type from PathBuf to Cow<'_, Path>. When a path doesn't need normalization (no consecutive slashes, no . or .. components, no trailing slashes), it returns Cow::Borrowed with zero allocation. Otherwise, it normalizes the path and returns Cow::Owned.

Changes:

  • Changed normalize() return type from PathBuf to Cow<'_, Path> for zero-allocation fast path
  • Added needs_normalization() function for Unix and Windows to detect clean paths
  • Updated all call sites to use .into_owned() where PathBuf is still needed
  • Added comprehensive tests for allocation behavior of clean vs dirty paths

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/sugar_path.rs Updated trait definition to return Cow<'_, Path> instead of PathBuf
src/impl_sugar_path.rs Added needs_normalization() check functions, updated normalize_inner() return type, and added .into_owned() calls at all usage sites
tests/normalize.rs Added tests verifying clean paths return Cow::Borrowed and dirty paths return Cow::Owned
tests/deep_paths.rs Refactored to store intermediate strings in variables due to lifetime requirements of the new API

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

@hyf0 hyf0 merged commit 28a3dd5 into main Feb 23, 2026
5 checks passed
@github-actions github-actions bot mentioned this pull request Feb 23, 2026
graphite-app bot pushed a commit to rolldown/rolldown that referenced this pull request Feb 23, 2026
## Summary

Bump `sugar_path` from 1.2.1 to 2^

### What changed in sugar_path 2.0

The 2.0 release is focused on reducing allocations in hot paths. The key optimizations:

- **`normalize()` returns `Cow<'_, Path>` instead of `PathBuf`** — a `needs_normalization()` fast-path check (using `memchr`) detects already-clean paths and returns `Cow::Borrowed` with zero allocation ([#32](hyf0/sugar_path#32))
- **`absolutize()` / `absolutize_with()` return `Cow<'_, Path>`** — same idea: already-absolute clean paths are returned borrowed ([#34](hyf0/sugar_path#34))
- **`memchr`-accelerated fast path for `relative()`** — replaces the component-iterator approach with SIMD-accelerated `/` scanning, avoids the `absolutize()` → `current_dir()` syscall when both paths are already absolute, and uses `SmallVec<[&str; 8]>` to stay on the stack ([#27](hyf0/sugar_path#27))
- **Reduced allocations across the board** — reuse buffers, `SmallVec` for component lists, avoid `collect()` into `Vec` ([#26](hyf0/sugar_path#26))

### Breaking change

`normalize()`, `absolutize()`, and `absolutize_with()` now return `Cow<'_, Path>` instead of `PathBuf`. Call sites that need an owned `PathBuf` require `.into_owned()`, and chained operations like `.join().normalize().to_slash_lossy()` need to be split so the intermediate `Cow` lives long enough.

## Test plan

- [x] CI passes (same API surface, just `Cow` unwrapping at call sites)
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.

Path::components strip . under the hood

2 participants