string.c: compare the first byte before memcmp() searching backward - #7186
Conversation
The needle's first byte has to match wherever the rest does, and comparing it settles every position but the ones that carry it. Handing each position to `memcmp()` instead pays for a call at all of them, which is what a backward search spends nearly all of its time on where the needle is not there to find. `str_char_rindex()` reads that byte to step back from anyway.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughBackward byte and UTF-8 substring searches now check the needle’s first byte before calling ChangesBackward substring search
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This localized optimization reduces unnecessary backward-search work without changing string-search results; no actionable merge-blocking risk remains after normal checks and review. Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
A backward search tries the needle at every position from
posdown to the head, and hands each one tomemcmp():Where the needle is not there, that is a call per byte of the string, and the call is what the search spends nearly all of its time on:
memcmp()has to be entered, has its own head to run before it looks at anything, and then almost always disagrees on the first byte it reads.The needle's first byte has to match wherever the rest of it does. Reading it inline settles every position but the ones that carry that byte, and only those reach
memcmp():str_char_rindex()searches the same way over character boundaries, and gets the same guard. There the byte is read at that position anyway, since stepping back to the previous character starts by looking at it.Both functions have already returned by then for an empty needle, so
t[0]is a byte of the needle in both. Instr_char_rindex()the clamp above the loop putssat the last byte the needle fits at, and the walk back only lowers it, so*sis inside the string at every turn.Measurements
The
bench-utf8build under Environment at the end, whose compile line isgcc -O3, best of 5 runs of best of 5:a.rindex("zzz")a.byterindex("zzz")u.rindex("zzz")The multi-byte receiver gains least because there the positions tried are characters rather than bytes, a third as many here, and stepping between them is work of its own that stays.
A search that finds its needle at the first position tried pays one byte comparison it did not pay before. A search that has to look for it is the case this is about.
Generated code
.textover every.o, against the same objects built from master, for each of the four buildsci/gcc-clangmakes. Their compile lines are under Environment at the end:full-debugis-O0, the other three are-O3.byte-stringis the build withoutmruby-encoding.str_byterindex()is what a string answers with there, so it gains the guard too, andstr_char_rindex()is not compiled.Testing
rake -m testoverci/gcc-clang, all four builds green, 0 KO, 0 crash, no new warnings:rake -m testoverbuild_config/asan.rbis green too, address and undefined sanitizers both, tests and bintests. The reordered read is the reason to run it: the guard reads*sbefore the length check that used to come first.Against master over 6720 cases of
String#indexandString#rindex, spanning ASCII, multi-byte, broken and binary receivers and needles with offsets on both sides of every edge, the answers are identical byte for byte, under the sanitizers as well.No test accompanies the change. Both searches answer what they answered, so there is nothing new to pin.
Environment
Versions, and the compile line of every build named above
build_config/asan.rbpicksg++forcxx_abiThe timings were taken on a build that is not one of the shipped configs, the default gembox with
mruby-encodingadded so that strings index by character:What each build actually compiles
src/string.cwith,-MMD -c, the-Ipaths and-ostripped:-g -O3is what thegcctoolchain sets.full-debugand the sanitizer build then append-g3 -O0throughenable_debug(), so those two are-O0, not-O3.bench-utf8appends a second-O3of its own, which changes nothing over the toolchain's.cxx_abiis the C compiler driven as C++ with-x c++ -std=gnu++03, andg++links it.