-
Notifications
You must be signed in to change notification settings - Fork 7.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stripos with large haystack has bad performance #7847
Comments
Not sure if this qualifies as a bug but there's definitely room for improvement. |
I'd rather treat this as feature request; we probably don't want to replace the current implementation of That said, consider the following drastically simplified but naive patch for master: ext/standard/string.c | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/ext/standard/string.c b/ext/standard/string.c
index 7c2dce4595..c350ebdd0a 100644
--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -1911,19 +1911,20 @@ PHP_FUNCTION(stripos)
RETURN_FALSE;
}
- haystack_dup = zend_string_tolower(haystack);
- needle_dup = zend_string_tolower(needle);
- found = (char*)php_memnstr(ZSTR_VAL(haystack_dup) + offset,
- ZSTR_VAL(needle_dup), ZSTR_LEN(needle_dup), ZSTR_VAL(haystack_dup) + ZSTR_LEN(haystack));
+ char *pn = ZSTR_VAL(needle);
+ char *eh = ZSTR_VAL(haystack) + ZSTR_LEN(haystack) - ZSTR_LEN(needle);
+ for (char *ph = ZSTR_VAL(haystack); ph <= eh; ++ph) {
+ if (*ph == *pn || *ph & ~0x40 == *pn & ~0x40) {
+ found = ph;
+ break;
+ }
+ }
if (found) {
- RETVAL_LONG(found - ZSTR_VAL(haystack_dup));
+ RETVAL_LONG(found - ZSTR_VAL(haystack));
} else {
RETVAL_FALSE;
}
-
- zend_string_release_ex(haystack_dup, 0);
- zend_string_release_ex(needle_dup, 0);
}
/* }}} */ The given test script outputs here:
This is even slightly slower than the unpatched variant, so the |
Closes phpGH-7847 Previously stripos would lowercase both the haystack and needle to reuse strpos. The approach in this PR is similar to strpos. memchr is highly optimized so we're using it to search for the first character of the needle in the haystack. If we find it we compare the remaining characters of the needle manually. The new implementation seems to perform about half as well as strpos (as two memchr calls are necessary).
Closes phpGH-7847 Previously stripos would lowercase both the haystack and needle to reuse strpos. The approach in this PR is similar to strpos. memchr is highly optimized so we're using it to search for the first character of the needle in the haystack. If we find it we compare the remaining characters of the needle manually. The new implementation seems to perform about half as well as strpos (as two memchr calls are necessary).
Closes phpGH-7847 Previously stripos/stristr would lowercase both the haystack and the needle to reuse strpos. The approach in this PR is similar to strpos. memchr is highly optimized so we're using it to search for the first character of the needle in the haystack. If we find it we compare the remaining characters of the needle manually. The new implementation seems to perform about half as well as strpos (as two memchr calls are necessary to find the next candidate).
function test($haystack, $needle) {
$repetitions = 0;
$start = microtime(true);
do {
stripos($haystack, $needle);
$repetitions += 1;
} while (microtime(true) - $start < 5);
$rate = round($repetitions / (microtime(true) - $start));
$rate = str_pad($rate, 8, ' ', STR_PAD_LEFT);
echo "$rate / second\n";
}
// Worst case
test(str_repeat('A', 1e+7) . 'BB', 'bb');
test(str_repeat('aA', 1e+6) . 'aB', 'ab');
test(str_repeat('A', 1e+1) . 'BB', 'bb');
test(str_repeat('aA', 1e+1) . 'aB', 'ab');
// Best case
test(str_repeat('A', 1e+7), 'aa');
test(str_repeat('aA', 1e+6), 'aa');
test(str_repeat('A', 1e+1), 'aa');
test(str_repeat('aA', 1e+1), 'aa');
// Real life cases
test(file_get_contents(__DIR__ . '/php_net.html'), '<article'); // Content of https://www.php.net/
test('content-type: application/json', 'content-type:');
test('int', 'int');
The new algorithm seems to be faster in all cases. Even in the worst case (compared to the old implementation) it is faster, likely because there is no allocation happening. The larger the haystack the more dramatic the difference. There is some variance when executing the benchmark but the difference in the benchmark is way above the margin of error. |
Is there any impact on the memory usage? |
@kamil-tekiela It avoids copying of both the haystack and needle so two allocations less essentially. But I haven't measured it. It will likely be insignificant unless the string is very very large as the memory is freed before the function returns. |
Closes phpGH-7847 Closes phpGH-7852 Previously stripos/stristr would lowercase both the haystack and the needle to reuse strpos. The approach in this PR is similar to strpos. memchr is highly optimized so we're using it to search for the first character of the needle in the haystack. If we find it we compare the remaining characters of the needle manually. The new implementation seems to perform about half as well as strpos (as two memchr calls are necessary to find the next candidate).
Closes phpGH-7847 Closes phpGH-7852 Previously stripos/stristr would lowercase both the haystack and the needle to reuse strpos. The approach in this PR is similar to strpos. memchr is highly optimized so we're using it to search for the first character of the needle in the haystack. If we find it we compare the remaining characters of the needle manually. The new implementation seems to perform about half as well as strpos (as two memchr calls are necessary to find the next candidate).
Description
stripos is slow because both input strings are always lowercased completely. This also entails unnecessary memory consumption.
preg_match with case-insensitive flag is a much faster alternative.
Source code:
php-src/ext/standard/string.c
Line 1914 in 9362d6f
Test case:
Example output:
PHP Version
PHP 8.0.14
Operating System
No response
The text was updated successfully, but these errors were encountered: