Repository navigation
ext/mbstring: Optimize valid UTF-8 in mb_substr() - #24080
kamil-tekiela wants to merge 2 commits into
Conversation
|
@kamil-tekiela Fuzzing is 100% necessary. Otherwise, it is very, very easy for subtle bugs to slip through. It's good that you optimized |
|
Do you have a fuzzer available? If not can you give me hints on how to build one? |
|
@kamil-tekiela When I have some time, I could fuzz this for you. Or, if you want to do it yourself, that might be a very good idea... fuzzing is an extremely powerful technique for finding bugs, and is an awesome tool for any programmer to know how to use. I once wrote up my workflow for fuzzing new code in mbstring here: #10828 (comment) |
|
I tried fuzzing as you said and it didn't find anything. Not sure I've done it properly, but when I intentionally introduced a bug it found it immediately. |
|
@kamil-tekiela OK, that is a good sign! However, how meaningful it is depends on what you asserted in the fuzzer. At the least, this shows that you probably don't have memory corruption bugs, which is good. But we can check more. A powerful way to test refactoring/optimization changes like this is: cut-and-paste the original code for the function into your mbstring.c file, and give the old function a different name, perhaps like
Then when you run the fuzzer, it will search for inputs which produce different results from the old and new implementations of the same function. |
|
@alexdowad Thanks. I run the fuzzer as you suggested now and it found one regression, but I cannot understand why it works that way. https://3v4l.org/Q8OB3# Until PHP 8.3.1, it worked the way that my optimisation works but since then it returns a different output. I think I can restrict my optimization to skip DOCOMO encoding, but I don't understand why there's a difference, so now I doubt whether my optimization is correct for other UTF-8 encodings too. The https://www.php.net/ChangeLog-8.php#8.3.2 doesn't mention any bug fix in mbstring either. |
|
I don't know that details, PHP 8.3 was overhaul mbstring(https://github.com/php/php-src/pulls?q=is%3Apr+major+overhaul+of). Maybe there is no changelogs. |
|
Ah, I got it. U+00AE (®、Registerd trade mark) is Therefore, PHP 8.3.2 and younger(8.3.2-master) is correct. |
|
|
Maybe, it's correct, but it's not very consistent https://3v4l.org/0D7hG#v |
ArtUkrainskiy
left a comment
There was a problem hiding this comment.
This needs a fix before merge: the fast path is also taken for the UTF-8-Mobile encodings, and for them the result now depends on whether the string was checked before.
This is the difference your fuzzer found, and it applies to all four variants that php_mb_is_no_encoding_utf8() accepts: UTF-8-Mobile#DOCOMO, #KDDI-A, #KDDI-B and #SOFTBANK. Since ec348a1 (PHP 8.3.2, the commit @youkidearitai bisected to), mb_substr() decodes and encodes again for them, which turns U+00AE into the carrier's code point; before that commit it copied bytes using the mblen_table, which is why PHP 8.2 and 8.3.0–8.3.1 give c2ae. With the PR, a string with the valid-UTF-8 flag skips the conversion, and a negative offset is enough to set the flag:
$s = str_repeat("a\u{AE}b", 2);
var_dump(bin2hex(mb_substr($s, 1, 1, 'UTF-8-Mobile#DOCOMO'))); // "ee9cb6"
mb_substr($s, -1, null, 'UTF-8-Mobile#DOCOMO'); // counts the length, which marks $s as valid UTF-8
var_dump(bin2hex(mb_substr($s, 1, 1, 'UTF-8-Mobile#DOCOMO'))); // master: "ee9cb6", PR: "c2ae"On master, mb_str_split() and mb_strcut() already return the input bytes for all four variants, while mb_substr(), mb_strstr() and mb_convert_encoding() encode again, so the inconsistency you noticed is older than this PR. Which behaviour is right for the mobile variants is a separate question. Keeping them on the decoding path (suggestion below) makes the output the same as master in all my checks.
Co-authored-by: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
|
I measured the current head 6d9250e on master 1c9242e (it merges cleanly) with GCC 13.3, GCC 14.2, clang 18.1 and clang 20.1 on an i7-13700H, built with the flags of the official Docker image. With the Change in CPU cycles per call with the PR, relative to master (negative = fewer cycles, faster); median of five alternating runs. The PR only changes strings with the internal valid-UTF-8 flag, so the rows run on a string that went through
Correctness: I compared the output with master for the functions that go through Code: I found nothing to change. The chunk loop runs only while Tests: the PR adds none, and No correctness issues found. LGTM. Full tables, scripts and data: https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/pr-24080-mb-substr |
This PR adds a fast path for valid UTF-8 strings in
mb_substr. It skips bytes in 256 chunks. The performance improvement is noticeable with longer strings and only ones that are flagged as valid UTF-8.@alexdowad What do you think? Is this worth doing? I haven't got a fuzzer, but I don't anticipate any behavioural changes.