-
Notifications
You must be signed in to change notification settings - Fork 7.8k
zend_compile: Optimize sprintf()
into a rope
#14546
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
Conversation
This just extracts the implementation as-is into a dedicated function to make it reusable in preparation of a future commit.
… a single constant string Without this Opcache will trigger a use-after-free in `zend_optimizer_compact_literals()`. Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
This optimization will compile `sprintf()` using only `%s` placeholders into a rope at compile time, effectively making those calls equivalent to the use of string interpolation, with the added benefit of supporting arbitrary expressions instead of just expressions starting with a `$`. For a synthetic test using: <?php $a = 'foo'; $b = 'bar'; for ($i = 0; $i < 100_000_000; $i++) { sprintf("%s-%s", $a, $b); } This optimization yields a 2.1× performance improvement: $ hyperfine 'sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php' \ '/tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php' Benchmark 1: sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php Time (mean ± σ): 1.869 s ± 0.033 s [User: 1.865 s, System: 0.003 s] Range (min … max): 1.840 s … 1.945 s 10 runs Benchmark 2: /tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php Time (mean ± σ): 4.011 s ± 0.034 s [User: 4.006 s, System: 0.005 s] Range (min … max): 3.964 s … 4.079 s 10 runs Summary sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php ran 2.15 ± 0.04 times faster than /tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php This optimization comes with a small and probably insignificant behavioral change: If one of the values cannot be (cleanly) converted to a string, for example when attempting to insert an object that is not `Stringable`, the resulting Exception will naturally not show the `sprintf()` call in the resulting stack trace, because there is no call to `sprintf()`. Nevertheless it will correctly point out the line of the `sprintf()` call as the source of the Exception, pointing the user towards the correct location.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Didn't find any mistakes. Maybe request a review from Dmitry too.
if (rope_elements == 0) { | ||
rope_init_lineno = get_next_op_number(); | ||
} | ||
opline = zend_compile_rope_add(result, rope_elements++, &const_node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: For the case of %%
, the merging of constant strings would actually be beneficial within zend_compile_rope_add()
. Otherwise we're compiling sprintf('%%foo')
this to '%' . 'foo'
, essentially. Although the optimizer probably takes care of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect %%
to be fairly rare within format strings. The previous remark about “clarity” also applies here: Merging of consecutive constants should ideally happen in a generic fashion, instead of requiring every user to reimplement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. zend_compile_encaps_list()
essentially does the same thing, which is what I was referring to. Hence, moving it to zend_compile_rope_add()
would make it usable for both. That said, I wasn't expecting you to do that, but just noted it as a possible follow-up, and/or a potential // TODO
to be added.
Regarding the “initial version” remark: Once this is reviewed and merged, I hope to also add support for plain “Named placeholders” (i.e. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't comment about the rope stuff, as I frankly don't understand it, but this looks great as a performance improvement!
if (Z_TYPE(elements[i].u.constant) != IS_ARRAY) { | ||
convert_to_string(&elements[i].u.constant); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could potentially check if it is an object if it implements the Stringable
interface? It would miss oddities like GMP
not implementing the interface, although it can be cast to a string (which we really should fix...) but should work except if the __toString()
method throws an error.
EDIT: I realized the issue is that one would need to possibly trigger autoloading to check the class....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are compile time const expressions, they cannot currently contain objects (not even enums).
@@ -4712,6 +4712,171 @@ static void zend_compile_ns_call(znode *result, znode *name_node, zend_ast *args | |||
} | |||
/* }}} */ | |||
|
|||
static zend_op *zend_compile_rope_add(znode *result, uint32_t num, znode *elem_node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please explain how this change would be able to affect bench.php
.
Please also explain how the addition of two comments to the source code significantly changed the results compared to these: https://github.com/php/php-src/actions/runs/9484406283
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe the performance degradation is comming from #14054 and the base branch used is detected wrongly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any problems
Hi! This fail just popped in Symfony CI: https://github.com/symfony/symfony/actions/runs/9497428991/job/26173977979 (detected in symfony/symfony#57379, for the record). It seems this PR triggered it (older CI runs don't have the failure).
Could this be considered a BC break? It's just this test that's causing the problem, maybe it's a fix to be made on the Symfony side. Just thought I'd give you a heads-up in case it alerts you to something obvious. 🙂 Direct link to the test case, if it helps: https://github.com/symfony/symfony/blob/678abb4b128c0bd9f6db83a280bd5355ce234aec/src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php#L205 |
@alexandre-daubois Thanks for the report! This shouldn't change behavior, so the issue is legit. @TimWolla Please have a look when you have time. |
@alexandre-daubois Thank you for the report. I've taken a look and it appears the issue is unrelated. It also happens when I disable the optimization by inserting Note that I am only able to reproduce the issue with a production build, a debug build of PHP ( |
Oh right, that's weird. I'll have a deeper look at the test case, thank you for having a look at it! |
25360ef is the first bad commit
/cc @arnaud-lb |
I believe it's not a bug in php-src. I've reported a symfony issue: symfony/symfony#57387. Thank you @alexandre-daubois @TimWolla |
@TimWolla I have two questions about this PR:
[1] https://github.com/php/php-src/blob/e4a8d5b16f/ext/standard/basic_functions.stub.php#L2999 |
@mvorisek Because they are locale-dependent for floating points. |
From my experience For that reason I don't plan to do that, but feel free to send a PR if you have data that indicates it would be worth it. |
That's incorrect since 8.0 with the Locale-independent float to string cast RFC getting accepted. |
This optimization will compile
sprintf()
using only%s
placeholders into a rope at compile time, effectively making those calls equivalent to the use of string interpolation, with the added benefit of supporting arbitrary expressions instead of just expressions starting with a$
.For a synthetic test using:
This optimization yields a 2.1× performance improvement:
This optimization comes with a small and probably insignificant behavioral change: If one of the values cannot be (cleanly) converted to a string, for example when attempting to insert an object that is not
Stringable
, the resulting Exception will naturally not show thesprintf()
call in the resulting stack trace, because there is no call tosprintf()
.Nevertheless it will correctly point out the line of the
sprintf()
call as the source of the Exception, pointing the user towards the correct location.I'd like to thank Ilija for his preliminary review and great feedback during development of this 😃