[llvm] [ADT] Avoid generic dispatch in StringRef::split(char) (PR #214978)
Zhiyang Chen via llvm-commits
llvm-commits at lists.llvm.org
Sat Aug 8 08:50:06 PDT 2026
https://github.com/jeffchen006 created https://github.com/llvm/llvm-project/pull/214978
`StringRef::split(char)` builds a temporary one-byte `StringRef` and delegates to
`split(StringRef)`. For a one-byte needle `StringRef::find(StringRef)` already
reduces to `memchr`, but getting there costs an out-of-line call into
`libLLVMSupport` plus the generic needle-dispatch chain (`From > size()`,
`N == 0`, `Size < N`, `N == 2`, `Size < 16 || N > 255`). Taking the address of
the parameter also forces the separator byte to be spilled to the stack at every
call site:
```asm
; before
movb $0x2c,0x2f(%rsp) ; &Separator forces a stack store
call <_ZNK4llvm9StringRef4findES0_m> ; out-of-line, into libLLVMSupport
; after
call memchr at plt ; inlined body, direct
```
Calling `find(Separator)` directly inlines to a plain `memchr`. The overload was
already restricted to a single-character separator, so this is purely an
implementation change — the semantic domain is unchanged.
There are ~368 `split('c')` call sites across `llvm/`, `clang/`, `lld/` and
`mlir/` (most common separators: `=` 74, `;` 49, `.` 46, `:` 38, `\n` 33,
`,` 33).
## Performance
google-benchmark, GCC 13 `-O3`, Ryzen 7 3700X. The host is shared, so these are
medians over 14 interleaved A/B rounds pinned to one core; a paired per-round
reduction and a quietest-rounds reduction agree to within 0.5%.
| Benchmark | before | after | speedup |
| --- | ---: | ---: | ---: |
| separator at offset 16, 4 KiB haystack | 6.77 ns | 4.02 ns | 1.68x |
| `Triple.cpp` arch/vendor/OS/env splits | 36.70 ns | 19.78 ns | 1.86x |
| `CommandLine.cpp` `Arg.split('=')` | 6.26 ns | 3.64 ns | 1.72x |
| `CommandLine.cpp` help text `split('\n')` | 133.17 ns | 84.63 ns | 1.57x |
The saving is a fixed ~2.5 ns per call rather than a fixed ratio, so it shrinks
as the haystack grows. A match-distance sweep on a 4 KiB buffer:
| separator offset | before | after | speedup |
| ---: | ---: | ---: | ---: |
| 0 | 6.76 ns | 4.05 ns | 1.67x |
| 16 | 6.77 ns | 4.02 ns | 1.68x |
| 64 | 7.27 ns | 4.77 ns | 1.52x |
| 256 | 9.57 ns | 7.03 ns | 1.36x |
| 1024 | 13.74 ns | 11.25 ns | 1.22x |
| absent (full 4 KiB scan) | 27.81 ns | 25.60 ns | 1.09x |
Both the old and the new code bottom out in `memchr`, so no input class
regresses — the speedup is ≥ 1.0 everywhere in the sweep.
## Code size
The inlined form is smaller than the temporary construction plus call sequence
it replaces, so this does not trade size for speed despite inlining at ~368
sites. Sum of `.text*` sections, same TU recompiled with each header:
| TU | `split(char)` sites | before | after | delta |
| --- | ---: | ---: | ---: | ---: |
| `llvm/lib/TargetParser/Triple.cpp` | 11 | 62,502 | 58,144 | −6.97% |
| `llvm/lib/Support/CommandLine.cpp` | 5 | 90,659 | 90,738 | +0.09% |
| `llvm/lib/TextAPI/Target.cpp` | 1 | 2,891 | 2,891 | 0.00% |
## Testing
Existing coverage in `llvm/unittests/ADT/StringRefTest.cpp` (`Split`, `Split2`,
`SplitLiteral`) passes, as does all of `ADTTests` (2168 tests). No new tests are
added because there is no new behaviour to cover.
Additionally validated with a differential harness that cross-checks 7.7M
generated cases against two oracles — the previous body expressed literally as
`S.split(StringRef(&Sep, 1))`, compared on result `data()` pointers as well as
sizes, and a naive `std::string::find`/`substr` reference. Covered:
default-constructed `StringRef` (`data() == nullptr`), empty-but-non-null,
separator-only, leading/trailing separator, separator at index 0 and at the last
index, embedded-NUL separators, high-bit (negative `char`) separators, 4 KiB
haystacks, and non-NUL-terminated interior slices. Clean under
`-fsanitize=address,undefined`.
>From 35ec5416263ce29dacc70ab78a30771900c80bbf Mon Sep 17 00:00:00 2001
From: Zhiyang Chen <jeffchen006 at gmail.com>
Date: Sat, 8 Aug 2026 15:49:04 +0000
Subject: [PATCH] [ADT] Avoid generic dispatch in StringRef::split(char)
split(char) built a temporary one-byte StringRef and delegated to
split(StringRef). For a one-byte needle, StringRef::find(StringRef) already
reduces to memchr, but it is an out-of-line call into libLLVMSupport and it
first walks the generic needle-dispatch chain (From > size(), N == 0,
Size < N, N == 2, Size < 16 || N > 255). Taking the address of the parameter
also forces the separator byte to be spilled to the stack at every call site.
Call find(Separator) directly instead, which inlines to a plain memchr. This
is a pure implementation change: the overload was already restricted to a
single-character separator, so the semantic domain is unchanged.
Measured with google-benchmark (GCC -O3, Ryzen 7 3700X, 14 interleaved A/B
rounds, times are medians):
separator at offset 16, 4KiB haystack 6.77 ns -> 4.02 ns 1.68x
Triple.cpp arch/vendor/OS/env splits 36.70 ns -> 19.78 ns 1.86x
CommandLine.cpp Arg.split('=') 6.26 ns -> 3.64 ns 1.72x
CommandLine.cpp help text split('\n') 133.17 ns -> 84.63 ns 1.57x
The saving is a fixed ~2.5 ns per call rather than a fixed ratio, so it shrinks
as the haystack grows (1.09x for a full 4KiB scan that finds nothing). Both
the old and the new code bottom out in memchr, so no input class regresses.
Code size is neutral to slightly better, since the inlined form is smaller than
the temporary construction plus call sequence it replaces: .text for
llvm/lib/TargetParser/Triple.cpp shrinks 6.97%, llvm/lib/Support/CommandLine.cpp
grows 0.09%.
Existing coverage in llvm/unittests/ADT/StringRefTest.cpp (Split, Split2,
SplitLiteral) continues to pass, as does all of ADTTests.
---
llvm/include/llvm/ADT/StringRef.h | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/llvm/include/llvm/ADT/StringRef.h b/llvm/include/llvm/ADT/StringRef.h
index 724c27da0ee95..3cc0ea80bebc3 100644
--- a/llvm/include/llvm/ADT/StringRef.h
+++ b/llvm/include/llvm/ADT/StringRef.h
@@ -734,7 +734,10 @@ class LLVM_GSL_POINTER StringRef {
/// \param Separator The character to split on.
/// \returns The split substrings.
[[nodiscard]] std::pair<StringRef, StringRef> split(char Separator) const {
- return split(StringRef(&Separator, 1));
+ size_t Idx = find(Separator);
+ if (Idx == npos)
+ return {*this, StringRef()};
+ return {slice(0, Idx), substr(Idx + 1)};
}
/// Split into two substrings around the first occurrence of a separator
More information about the llvm-commits
mailing list