Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/docs/performance-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The current suite treats `relative` itself as the borrowing API. The relative-re

The committed Linux and Windows snapshots record the current clean targets: a canonical descendant `relative -> Cow<Path>` uses zero allocation calls; `Cow::into_owned` uses one; the descendant and upward ordinary strict final-`String` compositions each use one; clean `PathBuf::into_normalized` and valid-Unicode `PathBuf::into_slash` use zero; and `absolutize_with` with a clean relative receiver and an owned cwd uses no fresh allocation and one buffer-growth reallocation. Requested byte totals remain target-specific. Local prints on other hosts may be used for investigation but are not continuous gates.

On macOS AArch64, the existing NEON common-prefix scan makes it profitable to determine the shared component prefix before normalization classification for absolute `relative` calls. An upward result is necessarily owned; when both unmatched suffixes are clean, identical dirty components inside the shared prefix cancel on both sides and do not need a second scan. Descendants still classify both full inputs before borrowing. Exhaustive short-path comparison against unconditional full normalization covers both result text and the `Cow` variant, with separate multibyte UTF-8 prefix and mismatch cases. The optimized dispatch is limited to macOS AArch64 with NEON. macOS x86_64, Windows, and Linux retain the previous implementation until the affected target has its own evidence; other operating systems retain the previous implementation and are not performance targets.
On macOS AArch64, the existing NEON common-prefix scan makes it profitable to determine the shared component prefix before normalization classification for absolute `relative` calls. An upward result is necessarily owned; when both unmatched suffixes are clean, identical dirty components inside the shared prefix cancel on both sides and do not need a second scan. Exact component-prefix descendants and same-path results classify only the full target before borrowing because a clean target proves that its contained base prefix is also clean; other zero-up cases without that prefix proof still classify both full inputs. Exhaustive short-path comparison against unconditional full normalization covers both result text and the `Cow` variant, with separate multibyte UTF-8 prefix and mismatch cases. The optimized dispatch is limited to macOS AArch64 with NEON. macOS x86_64, Windows, and Linux retain the previous implementation until the affected target has its own evidence; other operating systems retain the previous implementation and are not performance targets.

Runtime evidence compares freshly built binaries in isolated target directories against accepted baseline commit `886fc357f228e8a89e49103fdaa88db7eab70877`. On one Apple M3 Pro run using only the existing `relative` benchmark, 150 samples, a two-second warm-up, and a five-second measurement, `short_common_prefix` improved by 9.41% for the natural result and 9.37% after ownership, `deep_siblings` by 68.31% and 68.58%, and `different_subtrees` by 27.83% and 29.84%. The ordinary descendant control was unchanged for the natural result and stayed within the 1% noise threshold after ownership; the same-directory control stayed within noise for the natural result and improved by 3.17% after ownership. Two full `dot_slow_path` comparisons moved in opposite directions, including one 2.73% owned-result regression followed by a 1.94% improvement. A balanced `main/candidate/candidate/main/main/candidate/candidate/main` sequence of shorter 50-sample runs did not reproduce the regression: the candidate averages were 1.9% lower for the natural result and 2.3% lower after ownership, so this row supports only a no-demonstrated-regression conclusion rather than a gain claim. The AArch64 release IR grows from 16,665 lines and 430 copies to 16,777 and 431, so the change is justified by runtime rather than code size; macOS x86_64 produces the same 15,469 lines and 385 copies as the baseline.

Expand Down Expand Up @@ -145,6 +145,8 @@ Each vector load is bounded by the shorter input rounded down to 16 bytes, accep

On the fixed Apple ARM64 host, the short-prefix control remains statistically unchanged. The formal paired runs improve `module_to_cwd` by about 7.8%, `same_directory` by 16.2%, `deep_siblings` by 20.2%, and `same_path` by 22.6%; the dotted normalization path is unchanged. `different_subtrees` was noisy and had no significant change on the longer rerun, so it is not counted as a win. Generated assembly changes from a byte `ldrb`/compare loop to 16-byte `ldr q`, `cmeq.16b`, and `uminv.16b`; the measured `relative_str` text grows by 176 bytes. Allocation snapshots remain byte-for-byte unchanged because this substitutes CPU work inside the existing absolute fast path.

A follow-up measured against merged baseline `7123f3c` removes the second normalization scan from the NEON descendant and same-path branch. That branch has already proved that `base` is an exact component-boundary prefix of `target`; therefore every base component and separator is contained in target, and a clean target also proves that base is clean. Existing exhaustive short-path result and Cow-variant oracles cover all 224,676 pairs from 474 absolute spellings, while multibyte UTF-8 and invalid native encoding tests retain their existing fallbacks. On the same Apple M3 Pro, paired 150-sample Criterion runs of the final source improve `module_to_cwd` by 9.4% for the natural Cow result and 9.5% after conversion to `PathBuf`, `same_directory` by 18.7% and 18.6%, and `same_path` by 25.1%. The package-sideEffects-shaped final text result improves by 7.2% when borrowed and 7.1% after forcing ownership. `different_subtrees`, which does not enter the changed branch, shows no regression; a `strip_prefix` success control produced opposite 2%–3% signs when measurement order was reversed and is treated as host drift rather than a change. Release LLVM IR shrinks by two lines with the same 431 copies, and allocation behavior is unchanged. An initial Linux CodSpeed comparison exposed that shifting later panic source locations could perturb unrelated linked benchmark layout even though Linux instruction sequences were unchanged; the final source keeps those non-target locations stable, and its optimized Linux x86_64 library IR is byte-for-byte identical to the baseline. The production change remains limited to macOS ARM64 with NEON; every other target compiles the previous implementation unchanged.

## Recorded superseded consuming and fused API experiment

This section preserves evidence from an unpublished branch experiment; its method set is not the final API. The prototype added `SugarPathBuf::into_normalize`, `SugarPathBuf::into_slash_lossy`, and `SugarPath::relative_to_slash_lossy`. This was not a restoration of the old API unchanged. [PR #15](https://github.com/hyf0/sugar_path/pull/15) added the former `SugarPathBuf` and promised no-op reuse, but [issue #16](https://github.com/hyf0/sugar_path/issues/16) showed that `Path::components` strips `.` and made its classifier unreliable. [PR #17](https://github.com/hyf0/sugar_path/pull/17) removed that on-demand behavior, after which `into_normalize` always collected components and built a fresh `PathBuf`; commit [`61361ab`](https://github.com/hyf0/sugar_path/commit/61361abfd390b8a5088b1790f3df724f090040f3) later removed the trait. The experiment deliberately limited its trait to operations where consuming ownership changed measured allocation behavior. It did not restore `into_absolutize`, whose old implementation cloned or constructed the current directory and then normalized rather than demonstrating general in-place reuse.
Expand Down
8 changes: 4 additions & 4 deletions src/impl_sugar_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1567,8 +1567,6 @@ fn windows_prefixes_eq_ignore_ascii_case(
}
}

/// String-based relative path computation. Dispatches to the fast path when
/// the component spelling is already canonical, otherwise normalizes first.
#[cfg(all(target_os = "macos", target_arch = "aarch64", target_feature = "neon"))]
fn relative_str<'a>(target: &'a str, base: &str) -> Cow<'a, str> {
let target = target.trim_end_matches('/');
Expand All @@ -1592,7 +1590,9 @@ fn relative_str_suffix_validated<'a>(target: &'a str, base: &str) -> Cow<'a, str
};

if at_boundary && common_byte_len == base.len() {
if needs_relative_normalization(target) || needs_relative_normalization(base) {
// `base` is an exact component-boundary prefix of `target`, so every base
// component and separator has already been validated when `target` is clean.
if needs_relative_normalization(target) {
return Cow::Owned(relative_str_slow(target, base));
}
return Cow::Borrowed(target[common_prefix..].trim_start_matches('/'));
Expand All @@ -1615,7 +1615,7 @@ fn relative_str_suffix_validated<'a>(target: &'a str, base: &str) -> Cow<'a, str

// Upward results are always owned. Shared dirty components normalize to the
// same prefix on both sides, so only the unmatched suffixes can change the
// result. Descendants still scan both full inputs before borrowing a suffix.
// result. Non-prefix zero-up cases still scan both full inputs.
let needs_normalization = if ups == 0 {
needs_relative_normalization(target) || needs_relative_normalization(base)
} else {
Expand Down
Loading