Skip to content

perf: skip impossible prefixes in splitPos - #2754

Closed
andreybolonin wants to merge 5 commits into
php:mainfrom
andreybolonin:perf/split-pos-index-byte
Closed

andreybolonin wants to merge 5 commits into
php:mainfrom
andreybolonin:perf/split-pos-index-byte

Conversation

@andreybolonin

Copy link
Copy Markdown
Contributor

splitPos checks every path byte before reaching an extension marker, even when the marker starts with a case-sensitive byte such as .. Find the first possible start with strings.IndexByte, then retain the existing ASCII case-insensitive scan and segment-boundary checks.

Lowercase letter-prefixed markers keep the original search. Marker ordering, empty-marker handling, rejection of Unicode equivalents, and stopping after the first complete match inside a filename remain unchanged. The prefix search is performed once per marker to limit overhead on repeated partial matches.

Draft: this improves common .php searches but is not strictly memory-neutral: the generated arm64 stack frame grows by 64 bytes, despite zero heap allocations. Dense-dot and letter-prefix workloads also regress; the measurements below expose these tradeoffs.

Validation

  • Added focused cases for prefix searching, mixed case, marker order, segment boundaries, empty/oversized markers, and invalid UTF-8.
  • Existing TestSplitPos and Unicode security regression tests, new tests, and fuzz seed cases pass with -race in an isolated harness containing the production splitPos function extracted verbatim, the existing relevant tests extracted verbatim, and the new test file.
  • Differential fuzzing against the previous implementation: 7,517,150 executions in 30 seconds, four workers, no mismatches. Both markers and path bytes are fuzzed, including unvalidated marker bytes.
  • gofmt and git diff --check pass.
  • Native package test attempted with ./go.sh test -tags nowatcher -run '^TestSplitPos' -count=1 .; compilation is blocked by missing sapi/embed/php_embed.h. Local PHP 8.5.12 is NTS. Full FrankenPHP/Caddy tests and end-to-end load tests have not been run locally.

Performance measurements

Measured on Apple M2, darwin/arm64, macOS 27.0, Go 1.27.3. Baseline: d138a78717413a589cb9af097483024c19f6ae92.

Each version was built into a separate optimized Go test binary. Ten before/after pairs, alternating which version runs first; -test.cpu=1 -test.benchtime=250ms -test.benchmem. The same committed BenchmarkSplitPos runs against each version. Inputs and split-marker slices are prepared outside the timed loop; results are consumed through a package-level sink. No race instrumentation in timed runs.

Values are medians with observed min–max across all ten samples, in ns/op. Positive time reduction means faster; negative means slower. These are function microbenchmarks, not end-to-end HTTP throughput. Min–max is an observed range, not a confidence interval.

Scenario Before ns/op (min–max) After ns/op (min–max) Time reduction B/op before → after allocs/op before → after
short 12.575 (12.420–14.040) 8.365 (8.320–8.838) +33.5% 0 → 0 0 → 0
long 75.195 (74.420–83.260) 8.741 (8.614–8.956) +88.4% 0 → 0 0 → 0
no_match 68.260 (67.510–76.010) 6.856 (6.807–6.997) +90.0% 0 → 0 0 → 0
uppercase 12.215 (12.130–13.660) 8.104 (8.058–8.797) +33.7% 0 → 0 0 → 0
many_dots 134.950 (133.500–154.400) 159.250 (158.400–166.100) -18.0% 0 → 0 0 → 0
partial_matches 140.100 (139.200–147.200) 153.600 (153.100–160.000) -9.6% 0 → 0 0 → 0
invalid_first 9.326 (9.211–9.743) 8.782 (8.682–9.079) +5.8% 0 → 0 0 → 0
second_marker 30.170 (29.960–33.870) 26.940 (26.680–27.530) +10.7% 0 → 0 0 → 0
letter_prefix 12.480 (12.330–12.800) 13.615 (13.580–13.970) -9.1% 0 → 0 0 → 0
unicode_path 40.405 (40.060–45.500) 8.983 (8.935–9.145) +77.8% 0 → 0 0 → 0
empty_path 1.974 (1.931–2.008) 1.933 (1.896–1.953) +2.1% 0 → 0 0 → 0
empty_markers 1.937 (1.929–2.730) 1.935 (1.932–1.947) +0.1% 0 → 0 0 → 0

All 240 timed samples reported 0 B/op and 0 allocs/op. No cache, buffer, retained data, or new dependency is introduced. This measures Go heap allocations, not whole-process RSS or PHP memory. Stack-frame sizes are reported separately below.

The common .php cases improve substantially, but this is not a universal speedup: dense dots, repeated partial matches, and letter-prefixed markers can regress. The one-time prefix search deliberately retains the original scan after its first candidate, avoiding the much larger dense-dot regression of repeatedly calling IndexByte.

Additional memory and code-size measurements

Optimized compiler assembly (go test -c -gcflags=-S) for the same isolated before/after sources:

Metric Before After Difference
Go heap bytes per operation 0 0 0
Go heap allocations per operation 0 0 0
Direct splitPos stack-frame size, arm64 16 B 80 B +64 B
splitPos generated instruction size 320 B 432 B +112 B

The stack numbers are this function's generated frame, not peak nested stack usage or a per-request heap allocation. No whole-process RSS or PHP/C memory measurement was performed; zero B/op does not establish unchanged process memory. These results supersede the initial narrower microbenchmark by including the final implementation and adverse inputs.

Reproduction

With a supported PHP embed/ZTS development environment, run the committed benchmark on this branch and on the baseline with the same benchmark file:

./go.sh test -run '^$' -bench '^BenchmarkSplitPos$' -benchmem -count=10 -benchtime=250ms -cpu=1 .
./go.sh test -race -run '^TestSplitPos|^FuzzSplitPos' .
./go.sh test -run '^$' -fuzz '^FuzzSplitPosEquivalent$' -fuzztime=30s -parallel=4 .

The actual timings above used the following standalone reproduction to avoid linking PHP. Save it outside the checkout and run it from the PR checkout with Python 3. It extracts the baseline and current function without modifying either implementation, builds separate test binaries, then alternates execution order across ten pairs:

Standalone benchmark reproduction script
#!/usr/bin/env python3
"""Run from the PR checkout. Needs only Go and Python, no PHP embed build."""
from pathlib import Path
import os
import subprocess
import tempfile

base = "d138a78717413a589cb9af097483024c19f6ae92"
root = Path(tempfile.mkdtemp(prefix="frankenphp-splitpos-"))
env = dict(os.environ, GO111MODULE="off", GOCACHE=str(root / "cache"))
for variant, source in [
    ("before", subprocess.check_output(["git", "show", base + ":cgi.go"], text=True)),
    ("after", Path("cgi.go").read_text()),
]:
    directory = root / variant
    directory.mkdir()
    start = source.index("func splitPos(")
    function = source[start:source.index("// go_update_request_info", start)]
    imports = '\nimport "unicode/utf8"\n'
    if variant == "after":
        imports += 'import "strings"\n'
    (directory / "splitpos.go").write_text("package frankenphp\n" + imports + function)
    (directory / "cgi_splitpos_test.go").write_text(Path("cgi_splitpos_test.go").read_text())
    subprocess.run(["go", "test", "-c", "-o", str(root / (variant + ".test")),
                    str(directory / "splitpos.go"), str(directory / "cgi_splitpos_test.go")],
                   env=env, check=True)
for iteration in range(10):
    for variant in (("before", "after") if iteration % 2 == 0 else ("after", "before")):
        result = subprocess.run([str(root / (variant + ".test")), "-test.run=^$",
                                 "-test.bench=^BenchmarkSplitPos$", "-test.benchmem",
                                 "-test.benchtime=250ms", "-test.cpu=1"],
                                capture_output=True, text=True, check=True)
        with (root / (variant + ".txt")).open("a") as stream:
            stream.write(result.stdout)
    print("Completed pair", iteration + 1, flush=True)
print("Raw results:", root)
All 120 baseline benchmark samples
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	24090658	        12.67 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3958204	        74.77 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4351467	        68.08 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24783230	        12.20 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2246880	       133.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2159085	       139.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32776736	         9.235 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9967576	        29.97 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24146244	        12.33 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7487566	        40.12 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	151936108	         1.979 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155185623	         1.936 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	24022340	        12.47 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3989973	        75.00 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4417168	        67.83 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24787070	        12.20 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2243920	       133.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2148124	       139.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32823057	         9.225 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9945822	        30.12 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24416296	        12.36 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7505980	        40.06 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	151563312	         1.997 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	145937553	         2.730 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23366911	        13.38 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3353324	        83.26 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4294604	        69.32 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24045204	        12.44 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2235687	       154.4 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2099408	       147.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	29537853	         9.528 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	10031221	        29.96 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24304780	        12.40 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7457259	        40.13 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	153130009	         1.963 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155398174	         1.933 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23899939	        12.48 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 4006158	        74.83 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4442469	        67.65 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24487962	        12.19 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2236771	       134.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2142051	       140.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32730841	         9.303 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9927952	        30.07 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24152482	        12.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7301578	        40.53 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	152493134	         1.978 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154719259	         1.942 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23754537	        13.78 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3986714	        75.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4240362	        68.96 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24524746	        12.28 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2166616	       136.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2150443	       139.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	29980011	         9.348 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9972478	        33.87 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	23319232	        12.80 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7339711	        45.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	148028232	         2.008 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154984160	         1.936 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23921218	        14.04 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3748363	        76.67 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4412026	        76.01 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24167805	        12.23 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2208943	       135.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2149048	       139.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	31951432	         9.211 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	10023219	        30.00 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24302155	        12.37 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7459964	        40.30 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	153493610	         1.958 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	153463641	         1.945 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23600211	        12.68 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3960412	        75.39 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4404862	        68.44 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24660826	        13.66 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2224316	       134.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2143411	       140.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32753919	         9.295 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9792613	        30.25 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24254182	        12.67 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7353038	        40.31 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	152931570	         1.968 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	151593531	         1.976 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23991922	        12.45 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3986317	        74.89 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4452526	        67.53 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24832210	        12.14 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2237850	       134.3 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2126254	       141.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32338765	         9.358 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9936022	        30.22 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	23467292	        12.51 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7421860	        40.55 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	152299242	         1.969 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155406896	         1.930 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	24070927	        12.48 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 3991998	        76.69 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4304234	        69.56 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24433698	        12.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2223693	       135.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2082900	       143.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	31277964	         9.482 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9695950	        31.43 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22589865	        12.71 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7247236	        41.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	151593914	         1.995 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154926535	         1.937 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	23964771	        12.42 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	 4031133	        74.42 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	 4418578	        67.51 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	24787754	        12.13 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 2254184	       136.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 2136980	       144.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	30704018	         9.743 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	 9864202	        30.24 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	24162045	        12.46 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	 7231260	        40.50 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	155665240	         1.931 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155587974	         1.929 ns/op	       0 B/op	       0 allocs/op
PASS
All 120 optimized benchmark samples
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35175142	         8.838 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	33976046	         8.856 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	42848976	         6.885 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37308212	         8.109 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1886862	       158.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1953231	       153.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34887607	         8.705 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11089222	        26.76 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22067544	        13.59 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33324694	         8.935 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	153939865	         1.949 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155272068	         1.932 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35896437	         8.320 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34872398	         8.614 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	43000734	         6.807 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37423010	         8.058 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1887658	       158.7 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1954303	       153.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	32269632	         8.688 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11209155	        26.79 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22105213	        13.63 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33167953	         8.962 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	154583230	         1.930 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155257202	         1.932 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35798629	         8.342 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34763966	         8.732 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	44148758	         6.840 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37401624	         8.099 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1896927	       160.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1955100	       153.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34824163	         8.808 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	10754808	        26.98 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22253541	        13.58 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33588042	         9.028 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	158492593	         1.896 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154904402	         1.937 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35854789	         8.394 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34184788	         8.956 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	44164467	         6.831 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37501564	         8.200 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1882810	       159.3 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1944433	       153.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34885242	         8.707 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11196448	        26.68 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22258908	        13.59 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33449476	         8.977 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	157881649	         1.898 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	153976144	         1.947 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	34715022	         8.354 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	35006854	         8.737 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	43640330	         6.866 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37390748	         8.086 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1898545	       158.4 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1954864	       153.4 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34277714	         8.776 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11177832	        26.76 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	22125048	        13.61 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33760182	         8.943 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	154967848	         1.936 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155112206	         1.936 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35854964	         8.330 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34830898	         8.657 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	44216262	         6.822 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37044852	         8.058 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1879953	       159.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1957255	       153.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34912137	         8.796 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	10949320	        27.20 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	21868812	        13.62 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33684997	         8.989 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	154648009	         1.940 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154947638	         1.935 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	34140696	         8.382 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34762117	         8.951 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	43049326	         6.997 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37164547	         8.176 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1802328	       166.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1882725	       160.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	33338737	         8.919 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	10964377	        27.53 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	21424744	        13.97 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	32873109	         9.023 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	153988231	         1.929 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155122666	         1.933 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35754547	         8.328 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34441024	         8.745 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	43663888	         6.854 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	37044088	         8.100 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1885053	       158.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1947998	       153.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34890991	         8.682 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11204115	        26.90 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	21818841	        13.60 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	32545903	         8.939 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	152347934	         1.953 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155150712	         1.935 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35372840	         8.376 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34215979	         8.755 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	43641390	         6.858 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	36534871	         8.153 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1865414	       160.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1925968	       154.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	29155107	         9.079 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11024564	        27.36 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	21785305	        13.77 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33649734	         9.049 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	157786327	         1.901 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	154783752	         1.936 ns/op	       0 B/op	       0 allocs/op
PASS
goos: darwin
goarch: arm64
cpu: Apple M2
BenchmarkSplitPos/short         	35779599	         8.494 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/long          	34197289	         8.656 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/no_match      	44165274	         6.942 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/uppercase     	33594730	         8.797 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/many_dots     	 1849380	       162.7 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/partial_matches         	 1906363	       156.3 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/invalid_first           	34544966	         8.787 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/second_marker           	11193489	        27.38 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/letter_prefix           	21525049	        13.65 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/unicode_path            	33712759	         9.145 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_path              	154890604	         1.949 ns/op	       0 B/op	       0 allocs/op
BenchmarkSplitPos/empty_markers           	155017062	         1.935 ns/op	       0 B/op	       0 allocs/op
PASS
Differential fuzzing output
fuzz: elapsed: 0s, gathering baseline coverage: 0/7 completed
fuzz: elapsed: 0s, gathering baseline coverage: 7/7 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 573105 (190968/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 6s, execs: 1158648 (195190/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 9s, execs: 1754022 (198458/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 12s, execs: 2845878 (364055/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 15s, execs: 3635463 (263107/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 18s, execs: 4417791 (260783/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 21s, execs: 5196259 (259501/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 24s, execs: 5970871 (258196/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 27s, execs: 6742526 (257294/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 30s, execs: 7517150 (258153/sec), new interesting: 38 (total: 45)
fuzz: elapsed: 30s, execs: 7517150 (0/sec), new interesting: 38 (total: 45)
PASS
ok  	command-line-arguments	30.602s

@dunglas

dunglas commented Oct 23, 2026

Copy link
Copy Markdown
Member

The regressions come from the per-byte comparison run at each candidate, not from the IndexByte skip. Since normalizeSplitPath guarantees lowercase ASCII markers, case folding can be a mask (c|0x20 == s for letters, c == s otherwise), and a first-byte filter avoids entering the inner loop. Keeping your single IndexByte on top of that removes every regression:

// foldMask lets c|mask == s match both cases of a lowercase ASCII letter.
func foldMask(s byte) byte {
	if 'a' <= s && s <= 'z' {
		return 0x20
	}
	return 0
}

func hasFoldedPrefix(p, split string) bool {
	for j := 0; j < len(split); j++ {
		if s := split[j]; p[j]|foldMask(s) != s {
			return false
		}
	}
	return true
}

	for _, split := range splitPath {
		n := len(split)
		if n == 0 || n > len(path) {
			continue
		}
		first, mask := split[0], foldMask(split[0])
		last := len(path) - n
		i := 0
		if mask == 0 {
			if i = strings.IndexByte(path[:last+1], first); i < 0 {
				continue
			}
		}
		for ; i <= last; i++ {
			if path[i]|mask != first || !hasFoldedPrefix(path[i+1:i+n], split[1:]) {
				continue
			}
			if pos := i + n; pos == len(path) || path[pos] == '/' {
				return pos
			}
			break
		}
	}

Your BenchmarkSplitPos cases, Apple M5 Pro, Go 1.27.3, -count 8 -cpu 1, benchstat delta vs main:

Case main this PR suggestion
short 8.79 ns 6.72 ns (−24%) 5.17 ns (−41%)
long 44.38 ns 6.45 ns (−85%) 5.37 ns (−88%)
no_match 37.68 ns 6.07 ns (−84%) 5.07 ns (−87%)
uppercase 7.74 ns 6.00 ns (−22%) 4.60 ns (−41%)
many_dots 100.85 ns 93.65 ns (~) 63.42 ns (−37%)
partial_matches 88.60 ns 131.30 ns (+48%) 47.82 ns (−46%)
invalid_first 5.12 ns 6.95 ns (+36%) 4.79 ns (−6%)
second_marker 23.29 ns 17.34 ns (−26%) 10.26 ns (−56%)
letter_prefix 7.36 ns 7.88 ns (+7%) 4.73 ns (−36%)
unicode_path 28.66 ns 7.40 ns (−74%) 4.81 ns (−83%)
empty_path 1.82 ns 1.84 ns (~) 1.70 ns (−6%)
empty_markers 1.83 ns 1.83 ns (~) 1.73 ns (−5%)
geomean 14.48 ns 9.37 ns (−35%) 6.57 ns (−55%)

Differentially fuzzed against the main implementation: 17M executions, no mismatch, with markers lowercased and restricted to ASCII as normalizeSplitPath does.

One behavior difference to document: main rejects non-ASCII path bytes in splitPos itself, while this version relies on normalizeSplitPath having rejected non-ASCII markers. An unvalidated non-ASCII marker would match its raw bytes exactly. That cannot reintroduce the Unicode-equivalence bypass (GHSA-3g8v-8r37-cgjm), since exact byte equality never folds, but the comment above splitPos should state the dependency.

Could you also move the tests and benchmark into the existing cgi_test.go, which already has TestSplitPos, and shorten the comment to one line?

@andreybolonin
andreybolonin marked this pull request as ready for review October 23, 2026 14:12
@dunglas

dunglas commented Oct 23, 2026

Copy link
Copy Markdown
Member

LGTM, this should also be backported to Caddy

Comment thread cgi.go Resolved
Comment thread cgi.go Resolved
Comment thread cgi_test.go Resolved
@henderkes

Copy link
Copy Markdown
Contributor

I would generally prefer we keep in-sync with Caddy upstream here, so if you want this merged, you should manually (notify your owner) take the time to understand why the changes were denied there outside of the PR being opened autonomously.

@andreybolonin

Copy link
Copy Markdown
Contributor Author

ok, I dont want make PRs to Caddy

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.

3 participants