[WIP] Auto-extend live-ranges #11880
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Instead of relying on the optimizer to retain FREE/FE_FREE opcodes, extend live-ranges to the end of the try block.
Just some experimentation to solve issues that arise with
matchblocks. This attempts to solve the issue described in #5448, which arises with the following scenario:Because
THROWwill jump out of the current context, it needs to free all the temporaryVARs (andTMPVARs) that would normally be consumed by the skipped opcodes. We calculate live-ranges for this purpose. A live-range defines the "time" (sequence of opcodes) in which theVARholds a valid value which has to be freed if exited prematurely. Live-ranges are calculated by scanning the opcodes backwards to find the first (i.e. lowest) usage, and the first (i.e. lowest) definition of theVAR. In this case,V0(the result of thefoo()function call) lives from opcode0002to0005(exclusive). TheTHROWopcode at position0004will look at which values are live at its position, and won't be used after (i.e. don't outlive) thetryblock, if available. This will freeV0in this case.Normally, DCE would eliminate everything after
THROWsince it is unreachable, removingADD V0 bool(true)which is the only use ofV0. This would break calculation of the live-range of thisV0with the current implementation. As such, theVARwould no longer be freed. This issue only arose withthrowexpressions because in statement context noVARs are live (with some exceptions liveswitchconditions, loop iterators, etc). The current solution (#5450) is to treatthrowexpressions as non-terminators when building the CFG. This will persist the unreachable opcodes that follow.With
matchblocks, we will once again experience this issue, even forthrowstatements.With DCE, this is essentially equivalent to
foo() + throw new Exception(). Our current solution does not work for this case however, because thethrowstatement is treated as a terminator. We might also disable splitting CFG nodes for allthrowstatements inmatchblocks, that might actually be the easiest solution.I've tried a different approach in this PR. Instead of relying on the retention of the dead consuming opcodes, the live-range of unused
VARs may be extended until the end of the currenttryblock, if available. The idea is that the consuming opcode will only be eliminated if the current context is guaranteed to exit. A exception will always at least skip over the opcodes in the currenttryblock. We also rely on the fact that aVARcan't begin outside of atryblock and end inside of it, and vice versa. The upside is that we may get rid of some other hack that retain dead opcodes exclusively for the sake of live-range calculation.There are a few problems with this approach too.
zend_optimize_temporary_variables().zend_optimize_temporary_variables()will reuse theVARof a definition with no use. This will eliminate the live-range for the first definition. This algorithm would need to be adjusted.VARin different branches.THROWmust not eliminate the use ofV1in the lower branch, because that will shorten its live-range to the first usage. I'm unaware of such a sequence of opcodes being emitted in PHP, but it's easy to imagine. To my understanding, all other cases can eliminate the used opcodes.COALESCEopcode only conditionally initializes the result, in case the null branch is skipped. If the null branch contains athrow, we will optimize away the second definition of theVAR, elongating the live-range. However, in the null branch theVARisn't actually live, leading to a use-of-uninitialized-value. We could initialize result inCOALESCEtoIS_UNDEF, but that will lead to a slight performance regression. I'm not yet sure what the best way is to solve this.A similar problem exists for
return,continue,break,goto, etc. inmatchblocks. These will need to emitFREEopcodes forVARs that are live. This is similar to #5448. That's a different issue to tackle.