-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Implement implicit move optimisation #11166
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
base: master
Are you sure you want to change the base?
Conversation
## Introduction This implements a more generic version of the in-place modification I first tried in phpGH-11060. We decided to limit that PR to RC1 optimisations of some array functions only, and not do the in-place $variable optimisation in that way because of issues. This patch overcomes those issues and builds on the previous one. With this patch, any internal function that supports RC1 optimisations automatically gets the optimisation for in-place variable modifications. Contrary to the previous approach, this is compatible with exceptions. Furthermore, this approach also allows userland functions to benefit from this optimisation. e.g. the following code will not take a copy of the array with this patch, whereas previously it would due to the copy-on-write characteristic of arrays: ``` function foo($array) { $array[1] = 1; } function bar() { $array = ...; $array = foo($array); } ``` Right now the impact on the benchmark suite isn't that high. The reason is that only a handful of functions within PHP optimise for RC1 cases, and the array sizes for those cases are fairly small. When more support for these cases are added, the benefit from this patch will increase. I've added a micro benchmark for array operations that shows the effect of this optimisation. ## Implementation The optimiser already tracks which SSA variables have a value that doesn't matter with the no_val field. By changing ZEND_SEND_VAR to redefine op1, we automatically know if the variable will ever be used again without being overwritten by looking at the no_val field. If the no_val field is set, the variable may hold a string/array and the refcount may be 1, we set a flag on the ZEND_SEND_VAR(_EX) opline to indicate that it may avoid a copy. The flag is stored in extended_value. There are two new VM type spec handlers for this that check for the flag: one for ZEND_SEND_VAR and one for ZEND_SEND_VAR_EX. ## Limitations * The optimisation isn't performed on arguments passed to other functions. This is because the optimisation would be externally visible through backtraces, which is undesirable. Unfortunately, this is also the case where a lot of optimisation opportunity lies. Nonetheless, even with this limitation it seems like the optimisation can help a lot. * The optimisation does not apply to functions using indirect variable access (e.g. variable-variables, compact()) and vararg functions.
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 see a mesh with ZEND_SEND_VAR_EX
support. It's supported in some places but not in the others. It seems you never create op1_def SSA var for it, but then expect it in zend_dfa_optimize_send_copies()
and ZEND_SEND_VAR_EX_SIMPLE_EXT
.
I see a very slight improvement on Symfony and PHP-Parser, but more serious degradation on Wordpress (it's small anyway - 0.2% measured with callgrind)
SKIP_IF_TOP(op1); | ||
|
||
if (opline->opcode == ZEND_SEND_VAR) { | ||
SET_RESULT(op1, op1); | ||
} | ||
|
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.
Should we also add support for ZEND_SEND_VAR_EX
?
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.
Probably. That's somewhat separate from this PR, so do you want me to do it here or in another PR?
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 see. SCCP didn't support ZEND_SEND_VAR_EX
before. So this is not a problem.
Zend/Optimizer/zend_ssa.c
Outdated
@@ -1680,3 +1684,13 @@ void zend_ssa_rename_var_uses(zend_ssa *ssa, int old, int new, bool update_types | |||
old_var->phi_use_chain = NULL; | |||
} | |||
/* }}} */ | |||
|
|||
void zend_ssa_replace_op1_def_op1_use(zend_ssa *ssa, zend_ssa_op *ssa_op) |
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.
The name is not clear. May be it should be zend_ssa_replace_op1_def_by_op1_use
. May be this should be a more general function.
zend_op *op = call->arg_info[i].opline; | ||
zend_ssa_op *this_ssa_op = &ssa->ops[op - op_array->opcodes]; | ||
|
||
if (op->opcode == ZEND_SEND_VAR && this_ssa_op->op1_def >= 0) { |
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 suppose ZEND_SEND_VAR_EX is missed, because it can't be a part of a call to a "known" target.
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.
Yes, compile-time-calls cannot be done with ZEND_SEND_VAR_EX. I can add a comment to explain why this is though.
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 add a comment to explain why this is though.
No problem. I just liked to be sure.
Zend/Optimizer/sccp.c
Outdated
/* Removing the def in try_remove_definition() may reduce optimisation opportunities. | ||
* We want to keep the no_val definition until we actually replace it with a constant. */ | ||
if (opline->opcode == ZEND_SEND_VAR && ssa_op->op1_use == i && ssa_op->op1_def >= 0) { | ||
zend_ssa_replace_op1_def_op1_use(ssa, ssa_op); | ||
opline->extended_value = 0; | ||
} |
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.
This should be done for SEND_VAR_EX
as well.
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 tried this, I can't do it. This is because SEND_VAR_EX can pass a variable as a reference. Then if we replace the def with the use then the variable at the callsite will become a ref. See for example Zend/tests/method_argument_binding.phpt
This degradation comes from the extra work in Optimizer. So it's not critical. |
Thanks for the review!
This makes senes to me.
This is already done in the current code : php-src/Zend/Optimizer/zend_ssa.c Lines 684 to 704 in e2f477c
Am I missing something? |
int ssa_cv = ssa_op->op1_use; | ||
|
||
/* Argument move must not be observable in backtraces */ | ||
if (ssa->vars[ssa_cv].var < op_array->num_args) { |
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.
How much is this a problem - without it, will it corrupt something? Backtraces does normally create a CoW copy [1], so removing this condition can improve performance.
[1] https://3v4l.org/nbiOF/rfc#vgit.master_jit and https://3v4l.org/gSHDb/rfc#vgit.master
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.
Does not cause memory corruption, but causes a semantic change.
Let's say you remove the condition, then the following can happen:
If you have $a = array_merge($a, ...);
where $a is an argument, and the call causes an exception, then $a will be NULL in the backtrace because its value has been moved into the function call argument.
No. I missed that. Sorry. |
I addressed the review comments. I put one more additional comment here: #11166 (comment) |
I afraid, this patch may bring many problems. <?php
class Bomb {
function __destruct() {
var_dump(2);
}
}
function foo($array) {
$array[1] = 1;
}
function bar() {
$array = [new Bomb];
foo($array);
var_dump(1);
$array = null;
}
bar(); |
I see, the destructors execute earlier. In fact I remember Ilija mentioning something like this but I forgot about it. |
@dstogov Do you know of a particular use-case where this would lead to issues? Delaying can also be caused by internal cycles, like storing a non-static closure inside a property, so relying on this behavior already seems a bit risky. I do admit that the performance win is a bit smaller than we have hoped... |
@iluuu1994 I didn't try to dig for other problems, but I expect them.
|
This shouldn't be a concern, as this optimization isn't performed on "main" (due to globals or inherited scope), or functions utilizing functions that access/modify local scope (e.g.
I see. My question was more whether there are real cases where destructor order. I understand this is hard to answer definitively. As for the JIT and optimizer issues, I don't have a well enough picture to argue on those. 🙂 |
The destructor ordering is the only fundamental issue I could think of when I read this patch a week ago. I think the move semantics are valuable enough to justify rethinking these. Just like a C compiler will move values off the stack if they're not going to be used anymore. E.g. C# has the fixed statement, which has for effect that the variable will never be destroyed within that function (or scope the fixed applies to). (Yes, that's entering RFC territory, but TBF it might not be that a bad thing to explicitly indicate locations where within a function you rely on destructors not executing early.) This PR can probably not merged as is due to the destructor behaviour change, so @nielsdos I'll tag the PR as requires RFC, unless you can come up with a magical way not exposing that issue. :-D |
We don't make guarantees about destructor order anyway, do we? I think this optimization could be done in PHP 9. |
No idea. But Hyrum's law may apply. I learned that this could be a significant break to Wikimedia as they have a class to defer work that relies on deterministic destruction |
@nielsdos I suppose, if we got type tracking in arrays, we could trivially exclude arrays with objects&resources... We technically could land this optimization for strings already today though (or for arrays where we might statically determine to not contain objects), even if not including arrays yet, it might be making an impact already (e.g. str_replace with equally sized search and replace, or strtoupper/lower etc.). |
Even for strings, there's another subtle issue though. |
@nielsdos You might assign it the empty string (i.e. zend_empty_string) upon move, which will never be freed. |
Introduction
This implements a more generic version of the in-place modification I
first tried in #11060. We decided to limit that PR to RC1
optimisations of some array functions only, and not do the in-place
$variable optimisation in that way because of issues.
This patch overcomes those issues and builds on the previous patch.
With this patch, any internal function that supports RC1 optimisations
automatically gets the optimisation for in-place variable modifications.
Contrary to the previous approach, this is compatible with exceptions.
Furthermore, this approach also allows userland functions to benefit
from this optimisation.
e.g. the following code will not take a copy of the array with this
patch, whereas previously it would due to the copy-on-write
characteristic of arrays:
Right now the impact on the benchmark suite isn't that high. The reason
is that only a handful of functions within PHP optimise for RC1 cases, and
the array sizes for those cases are fairly small.
When more support for these cases are added, the benefit from this patch
will increase.
I've added a micro benchmark for array operations that shows the effect
of this optimisation.
Implementation
The optimiser already tracks which SSA variables have a value that
doesn't matter with the no_val field. By changing ZEND_SEND_VAR to
redefine op1, we automatically know if the variable will ever be used
again without being overwritten by looking at the no_val field.
If the no_val field is set, the variable may hold a string/array and the
refcount may be 1, we set a flag on the ZEND_SEND_VAR(_EX) opline to
indicate that it may avoid a copy. The flag is stored in extended_value.
There are two new VM type spec handlers for this that check for the
flag: one for ZEND_SEND_VAR and one for ZEND_SEND_VAR_EX.
Limitations
functions. This is because the optimisation would be externally
visible through backtraces, which is undesirable.
Unfortunately, this is also the case where a lot of optimisation
opportunity lies. Nonetheless, even with this limitation it seems like
the optimisation can help a lot.
access (e.g. variable-variables, compact()) and vararg functions.
Benchmark results
Zend/array_micro_bench.php
without this optimisation:Zend/array_micro_bench.php
with this optimisation:As you can see there is virtually no difference for the
array_merge nest
case. This is expected: the optimisation cannot be done because the original array is still required for the addition after thearray_merge
function is executed.However, for the other cases there is a notable difference. The performance of
array_unique
is dominated by sorting, so we don't see a very big win there, but still a win. Foruser_func
we see a small win, but the total time is not a lot to begin with because the array is small.I can see a nice benefit from this optimisation in data-heavy processes (e.g. ETL processes).
Acknowledgements
This optimisation is inspired by C++'s & Rust's "move semantics", hence the name: implicitly adding moves.
During the development of this patch I had some conversations about the implementation with @iluuu1994 , so I would like to thank him as well.