From 8c0de53e5f599c83fa03c78931527ab4ff14cf93 Mon Sep 17 00:00:00 2001 From: Dmitry Stogov Date: Tue, 11 Jul 2017 21:54:36 +0300 Subject: [PATCH] Initial integration of Dead Code Elimination (DCE) and unused variable removing passes, originally developed in https://github.com/nikic/php-src/tree/opt, into DFA optimization pass. --- ext/opcache/Optimizer/compact_vars.c | 112 ++++ ext/opcache/Optimizer/dce.c | 548 ++++++++++++++++++ ext/opcache/Optimizer/dfa_pass.c | 9 + ext/opcache/Optimizer/zend_inference.c | 288 +++++++++ ext/opcache/Optimizer/zend_inference.h | 5 + ext/opcache/Optimizer/zend_optimizer.c | 18 + ext/opcache/Optimizer/zend_optimizer.h | 5 +- .../Optimizer/zend_optimizer_internal.h | 7 + ext/opcache/Optimizer/zend_ssa.c | 129 +++++ ext/opcache/Optimizer/zend_ssa.h | 58 ++ ext/opcache/config.m4 | 2 + 11 files changed, 1179 insertions(+), 2 deletions(-) create mode 100644 ext/opcache/Optimizer/compact_vars.c create mode 100644 ext/opcache/Optimizer/dce.c diff --git a/ext/opcache/Optimizer/compact_vars.c b/ext/opcache/Optimizer/compact_vars.c new file mode 100644 index 00000000000..c5a9b79553d --- /dev/null +++ b/ext/opcache/Optimizer/compact_vars.c @@ -0,0 +1,112 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine, Removing unused variables | + +----------------------------------------------------------------------+ + | Copyright (c) 1998-2017 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Nikita Popov | + +----------------------------------------------------------------------+ +*/ + +#include "ZendAccelerator.h" +#include "Optimizer/zend_optimizer_internal.h" +#include "zend_bitset.h" + +/* This pass removes all CVs that are completely unused. It does *not* merge any CVs. + * This pass does not operate on SSA form anymore. */ +void zend_optimizer_compact_vars(zend_op_array *op_array) { + int i; + + ALLOCA_FLAG(use_heap1); + ALLOCA_FLAG(use_heap2); + uint32_t used_cvs_len = zend_bitset_len(op_array->last_var); + zend_bitset used_cvs = ZEND_BITSET_ALLOCA(used_cvs_len, use_heap1); + uint32_t *cv_map = do_alloca(op_array->last_var * sizeof(uint32_t), use_heap2); + uint32_t num_cvs, tmp_offset; + + /* Determine which CVs are used */ + zend_bitset_clear(used_cvs, used_cvs_len); + for (i = 0; i < op_array->last; i++) { + zend_op *opline = &op_array->opcodes[i]; + if (opline->op1_type == IS_CV) { + zend_bitset_incl(used_cvs, VAR_NUM(opline->op1.var)); + } + if (opline->op2_type == IS_CV) { + zend_bitset_incl(used_cvs, VAR_NUM(opline->op2.var)); + } + if (opline->result_type == IS_CV) { + zend_bitset_incl(used_cvs, VAR_NUM(opline->result.var)); + } + } + + num_cvs = 0; + for (i = 0; i < op_array->last_var; i++) { + if (zend_bitset_in(used_cvs, i)) { + cv_map[i] = num_cvs++; + } else { + cv_map[i] = (uint32_t) -1; + } + } + + free_alloca(used_cvs, use_heap1); + if (num_cvs == op_array->last_var) { + free_alloca(cv_map, use_heap2); + return; + } + + ZEND_ASSERT(num_cvs < op_array->last_var); + tmp_offset = op_array->last_var - num_cvs; + + /* Update CV and TMP references in opcodes */ + for (i = 0; i < op_array->last; i++) { + zend_op *opline = &op_array->opcodes[i]; + if (opline->op1_type == IS_CV) { + opline->op1.var = NUM_VAR(cv_map[VAR_NUM(opline->op1.var)]); + } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) { + opline->op1.var -= sizeof(zval) * tmp_offset; + } + if (opline->op2_type == IS_CV) { + opline->op2.var = NUM_VAR(cv_map[VAR_NUM(opline->op2.var)]); + } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) { + opline->op2.var -= sizeof(zval) * tmp_offset; + } + if (opline->result_type == IS_CV) { + opline->result.var = NUM_VAR(cv_map[VAR_NUM(opline->result.var)]); + } else if (opline->result_type & (IS_VAR|IS_TMP_VAR)) { + opline->result.var -= sizeof(zval) * tmp_offset; + } + } + + /* Update TMP references in live ranges */ + if (op_array->live_range) { + for (i = 0; i < op_array->last_live_range; i++) { + op_array->live_range[i].var -= sizeof(zval) * tmp_offset; + } + } + + /* Update CV name table */ + { + zend_string **names = safe_emalloc(sizeof(zend_string *), num_cvs, 0); + for (i = 0; i < op_array->last_var; i++) { + if (cv_map[i] != (uint32_t) -1) { + names[cv_map[i]] = op_array->vars[i]; + } else { + zend_string_release(op_array->vars[i]); + } + } + efree(op_array->vars); + op_array->vars = names; + } + + op_array->last_var = num_cvs; + + free_alloca(cv_map, use_heap2); +} diff --git a/ext/opcache/Optimizer/dce.c b/ext/opcache/Optimizer/dce.c new file mode 100644 index 00000000000..6d11d438cfa --- /dev/null +++ b/ext/opcache/Optimizer/dce.c @@ -0,0 +1,548 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine, DCE - Dead Code Elimination | + +----------------------------------------------------------------------+ + | Copyright (c) 1998-2017 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Nikita Popov | + +----------------------------------------------------------------------+ +*/ + +#include "ZendAccelerator.h" +#include "Optimizer/zend_optimizer_internal.h" +#include "Optimizer/zend_inference.h" +#include "Optimizer/zend_ssa.h" +#include "Optimizer/zend_func_info.h" +#include "Optimizer/zend_call_graph.h" +#include "zend_bitset.h" + +/* This pass implements a form of dead code elimination (DCE). The algorithm optimistically assumes + * that all instructions and phis are dead. Instructions with immediate side-effects are then marked + * as live. We then recursively (using a worklist) propagate liveness to the instructions that def + * the used operands. + * + * Notes: + * * This pass does not perform unreachable code elimination. This happens as part of the SCCP + * pass. + * * The DCE is performed without taking control-dependence into account, i.e. all conditional + * branches are assumed to be live. It's possible to take control-dependence into account using + * the DCE algorithm described by Cytron et al., however it requires the construction of a + * postdominator tree and of postdominance frontiers, which does not seem worthwhile at this + * point. + * * We separate intrinsic side-effects from potential side-effects in the form of notices thrown + * by the instruction (in case we want to make this configurable). See may_have_side_effect() and + * zend_may_throw(). + * * We often cannot DCE assignments and unsets while guaranteeing that dtors run in the same + * order. There is an optimization option to allow reordering of dtor effects. + */ + +typedef struct { + zend_ssa *ssa; + zend_op_array *op_array; + zend_bitset instr_dead; + zend_bitset phi_dead; + zend_bitset instr_worklist; + zend_bitset phi_worklist; + uint32_t instr_worklist_len; + uint32_t phi_worklist_len; + unsigned reorder_dtor_effects : 1; +} context; + +static inline zend_bool is_bad_mod(const zend_ssa *ssa, int use, int def) { + if (def < 0) { + /* This modification is not tracked by SSA, assume the worst */ + return 1; + } + if (ssa->var_info[use].type & MAY_BE_REF) { + /* Modification of reference may have side-effect */ + return 1; + } + return 0; +} + +static inline zend_bool may_have_side_effects( + const context *ctx, const zend_op *opline, const zend_ssa_op *ssa_op) { + zend_op_array *op_array = ctx->op_array; + zend_ssa *ssa = ctx->ssa; + if (zend_may_throw(opline, op_array, ssa)) { + return 1; + } + + switch (opline->opcode) { + case ZEND_NOP: + case ZEND_IS_IDENTICAL: + case ZEND_IS_NOT_IDENTICAL: + case ZEND_QM_ASSIGN: + case ZEND_FREE: + case ZEND_TYPE_CHECK: + case ZEND_DEFINED: + case ZEND_ADD: + case ZEND_SUB: + case ZEND_MUL: + case ZEND_POW: + case ZEND_BW_OR: + case ZEND_BW_AND: + case ZEND_BW_XOR: + case ZEND_CONCAT: + case ZEND_FAST_CONCAT: + case ZEND_DIV: + case ZEND_MOD: + case ZEND_BOOL_XOR: + case ZEND_BOOL: + case ZEND_BOOL_NOT: + case ZEND_BW_NOT: + case ZEND_SL: + case ZEND_SR: + case ZEND_IS_EQUAL: + case ZEND_IS_NOT_EQUAL: + case ZEND_IS_SMALLER: + case ZEND_IS_SMALLER_OR_EQUAL: + case ZEND_CASE: + case ZEND_CAST: + case ZEND_ROPE_INIT: + case ZEND_ROPE_ADD: + case ZEND_ROPE_END: + case ZEND_INIT_ARRAY: + case ZEND_ADD_ARRAY_ELEMENT: + /* No side effects */ + return 0; + case ZEND_JMP: + case ZEND_JMPZ: + case ZEND_JMPNZ: + case ZEND_JMPZNZ: + case ZEND_JMPZ_EX: + case ZEND_JMPNZ_EX: + case ZEND_JMP_SET: + case ZEND_COALESCE: + case ZEND_ASSERT_CHECK: + /* For our purposes a jumps and branches are side effects. */ + return 1; + case ZEND_BEGIN_SILENCE: + case ZEND_END_SILENCE: + case ZEND_ECHO: + case ZEND_INCLUDE_OR_EVAL: + case ZEND_THROW: + case ZEND_EXT_STMT: + case ZEND_EXT_FCALL_BEGIN: + case ZEND_EXT_FCALL_END: + case ZEND_EXT_NOP: + case ZEND_TICKS: + case ZEND_YIELD: + case ZEND_YIELD_FROM: + /* Intrinsic side effects */ + return 1; + case ZEND_DO_FCALL: + case ZEND_DO_FCALL_BY_NAME: + case ZEND_DO_ICALL: + case ZEND_DO_UCALL: + /* For now assume all calls have side effects */ + return 1; + case ZEND_RECV: + case ZEND_RECV_INIT: + /* Even though RECV_INIT can be side-effect free, these cannot be simply dropped + * due to the prologue skipping code. */ + return 1; + case ZEND_ASSIGN_REF: + return 1; + case ZEND_ASSIGN: + { + uint32_t t1 = OP1_INFO(); + if (is_bad_mod(ssa, ssa_op->op1_use, ssa_op->op1_def)) { + return 1; + } + if (!ctx->reorder_dtor_effects) { + if (t1 & MAY_HAVE_DTOR) { + /* DCE might extend lifetime */ + return 1; + } + if (opline->op2_type != IS_CONST && (OP2_INFO() & MAY_HAVE_DTOR)) { + /* DCE might shorten lifetime */ + return 1; + } + } + return 0; + } + case ZEND_UNSET_VAR: + { + uint32_t t1 = OP1_INFO(); + if (!(opline->extended_value & ZEND_QUICK_SET)) { + return 1; + } + if (t1 & MAY_BE_REF) { + /* We don't consider uses as the LHS of an assignment as real uses during DCE, so + * an unset may be considered dead even if there is a later assignment to the + * variable. Removing the unset in this case would not be correct if the variable + * is a reference, because unset breaks references. */ + return 1; + } + if (!ctx->reorder_dtor_effects && (t1 & MAY_HAVE_DTOR)) { + /* DCE might extend lifetime */ + return 1; + } + return 0; + } + case ZEND_PRE_INC: + case ZEND_POST_INC: + case ZEND_PRE_DEC: + case ZEND_POST_DEC: + return is_bad_mod(ssa, ssa_op->op1_use, ssa_op->op1_def); + case ZEND_ASSIGN_ADD: + case ZEND_ASSIGN_SUB: + case ZEND_ASSIGN_MUL: + case ZEND_ASSIGN_DIV: + case ZEND_ASSIGN_MOD: + case ZEND_ASSIGN_SL: + case ZEND_ASSIGN_SR: + case ZEND_ASSIGN_CONCAT: + case ZEND_ASSIGN_BW_OR: + case ZEND_ASSIGN_BW_AND: + case ZEND_ASSIGN_BW_XOR: + case ZEND_ASSIGN_POW: + if (opline->extended_value) { + /* ASSIGN_DIM has no side-effect, but we can't deal with OP_DATA anyway */ + return 1; + } + return is_bad_mod(ssa, ssa_op->op1_use, ssa_op->op1_def); + default: + /* For everything we didn't handle, assume a side-effect */ + return 1; + } +} + +static inline void add_to_worklists(context *ctx, int var_num) { + zend_ssa_var *var = &ctx->ssa->vars[var_num]; + if (var->definition >= 0) { + if (zend_bitset_in(ctx->instr_dead, var->definition)) { + zend_bitset_incl(ctx->instr_worklist, var->definition); + } + } else if (var->definition_phi) { + if (zend_bitset_in(ctx->phi_dead, var_num)) { + zend_bitset_incl(ctx->phi_worklist, var_num); + } + } +} + +static inline void add_to_phi_worklist_only(context *ctx, int var_num) { + zend_ssa_var *var = &ctx->ssa->vars[var_num]; + if (var->definition_phi && zend_bitset_in(ctx->phi_dead, var_num)) { + zend_bitset_incl(ctx->phi_worklist, var_num); + } +} + +static inline void add_operands_to_worklists(context *ctx, zend_op *opline, zend_ssa_op *ssa_op) { + if (ssa_op->result_use >= 0) { + add_to_worklists(ctx, ssa_op->result_use); + } + if (ssa_op->op1_use >= 0 && !zend_has_improper_op1_use(opline)) { + add_to_worklists(ctx, ssa_op->op1_use); + } + if (ssa_op->op2_use >= 0) { + add_to_worklists(ctx, ssa_op->op2_use); + } +} + +static inline void add_phi_sources_to_worklists(context *ctx, zend_ssa_phi *phi) { + zend_ssa *ssa = ctx->ssa; + int source; + FOREACH_PHI_SOURCE(phi, source) { + add_to_worklists(ctx, source); + } FOREACH_PHI_SOURCE_END(); +} + +static inline zend_bool is_var_dead(context *ctx, int var_num) { + zend_ssa_var *var = &ctx->ssa->vars[var_num]; + if (var->definition_phi) { + return zend_bitset_in(ctx->phi_dead, var_num); + } else if (var->definition >= 0) { + return zend_bitset_in(ctx->instr_dead, var->definition); + } else { + /* Variable has no definition, so either the definition has already been removed (var is + * dead) or this is one of the implicit variables at the start of the function (for our + * purposes live) */ + return var_num >= ctx->op_array->last_var; + } +} + +/* Returns whether the instruction has been DCEd */ +static zend_bool dce_instr(context *ctx, zend_op *opline, zend_ssa_op *ssa_op) { + zend_ssa *ssa = ctx->ssa; + int free_var = -1; + zend_uchar free_var_type; + + if (opline->opcode == ZEND_NOP) { + return 0; + } + + /* We mark FREEs as dead, but they're only really dead if the destroyed var is dead */ + if (opline->opcode == ZEND_FREE && !is_var_dead(ctx, ssa_op->op1_use)) { + return 0; + } + + // TODO Two free vars? + if ((opline->op1_type & (IS_VAR|IS_TMP_VAR)) && !is_var_dead(ctx, ssa_op->op1_use)) { + free_var = ssa_op->op1_use; + free_var_type = opline->op1_type; + } else if ((opline->op2_type & (IS_VAR|IS_TMP_VAR)) && !is_var_dead(ctx, ssa_op->op2_use)) { + free_var = ssa_op->op2_use; + free_var_type = opline->op2_type; + } + + zend_ssa_rename_defs_of_instr(ctx->ssa, ssa_op); + zend_ssa_remove_instr(ctx->ssa, opline, ssa_op); + + if (free_var >= 0) { + // TODO Sometimes we can mark the var as EXT_UNUSED + opline->opcode = ZEND_FREE; + opline->op1.var = (uintptr_t) ZEND_CALL_VAR_NUM(NULL, ssa->vars[free_var].var); + opline->op1_type = free_var_type; + + ssa_op->op1_use = free_var; + ssa_op->op1_use_chain = ssa->vars[free_var].use_chain; + ssa->vars[free_var].use_chain = ssa_op - ssa->ops; + } + return 1; +} + +// TODO Move this somewhere else (CFG simplification?) +static int simplify_jumps(zend_ssa *ssa, zend_op_array *op_array) { + int removed_ops = 0; + zend_basic_block *block; + FOREACH_BLOCK(block) { + int block_num = block - ssa->cfg.blocks; + zend_op *opline = &op_array->opcodes[block->start + block->len - 1]; + zend_ssa_op *ssa_op = &ssa->ops[block->start + block->len - 1]; + zval *op1; + + if (block->len == 0) { + continue; + } + + /* Convert jump-and-set into jump if result is not used */ + switch (opline->opcode) { + case ZEND_JMPZ_EX: + ZEND_ASSERT(ssa_op->result_def >= 0); + if (ssa->vars[ssa_op->result_def].use_chain < 0 + && ssa->vars[ssa_op->result_def].phi_use_chain == NULL) { + opline->opcode = ZEND_JMPZ; + opline->result_type = IS_UNUSED; + zend_ssa_remove_result_def(ssa, ssa_op); + } + break; + case ZEND_JMPNZ_EX: + case ZEND_JMP_SET: + ZEND_ASSERT(ssa_op->result_def >= 0); + if (ssa->vars[ssa_op->result_def].use_chain < 0 + && ssa->vars[ssa_op->result_def].phi_use_chain == NULL) { + opline->opcode = ZEND_JMPNZ; + opline->result_type = IS_UNUSED; + zend_ssa_remove_result_def(ssa, ssa_op); + } + break; + } + + /* Convert jump-and-set to QM_ASSIGN/BOOL if the "else" branch is not taken. */ + switch (opline->opcode) { + case ZEND_JMPZ_EX: + case ZEND_JMPNZ_EX: + if (block->successors[1] < 0 && block->successors[0] != block_num + 1) { + opline->opcode = ZEND_BOOL; + } + break; + case ZEND_JMP_SET: + case ZEND_COALESCE: + if (block->successors[1] < 0 && block->successors[0] != block_num + 1) { + opline->opcode = ZEND_QM_ASSIGN; + } + break; + } + + if (opline->op1_type != IS_CONST) { + continue; + } + + /* Convert constant conditional jump to unconditional jump */ + op1 = &ZEND_OP1_LITERAL(opline); + switch (opline->opcode) { + case ZEND_JMPZ: + if (!zend_is_true(op1)) { + literal_dtor(op1); + opline->op1_type = IS_UNUSED; + opline->op1.num = opline->op2.num; + opline->opcode = ZEND_JMP; + } else { + MAKE_NOP(opline); + removed_ops++; + } + break; + case ZEND_JMPNZ: + if (zend_is_true(op1)) { + literal_dtor(op1); + opline->op1_type = IS_UNUSED; + opline->op1.num = opline->op2.num; + opline->opcode = ZEND_JMP; + } else { + MAKE_NOP(opline); + removed_ops++; + } + break; + case ZEND_COALESCE: + ZEND_ASSERT(ssa_op->result_def >= 0); + if (ssa->vars[ssa_op->result_def].use_chain >= 0 + || ssa->vars[ssa_op->result_def].phi_use_chain != NULL) { + break; + } + + zend_ssa_remove_result_def(ssa, ssa_op); + if (Z_TYPE_P(op1) != IS_NULL) { + literal_dtor(op1); + opline->op1_type = IS_UNUSED; + opline->op1.num = opline->op2.num; + opline->opcode = ZEND_JMP; + opline->result_type = IS_UNUSED; + } else { + MAKE_NOP(opline); + removed_ops++; + } + break; + } + } FOREACH_BLOCK_END(); + return removed_ops; +} + +static inline int get_common_phi_source(zend_ssa *ssa, zend_ssa_phi *phi) { + int common_source = -1; + int source; + FOREACH_PHI_SOURCE(phi, source) { + if (common_source == -1) { + common_source = source; + } else if (common_source != source && source != phi->ssa_var) { + return -1; + } + } FOREACH_PHI_SOURCE_END(); + ZEND_ASSERT(common_source != -1); + return common_source; +} + +static void try_remove_trivial_phi(context *ctx, zend_ssa_phi *phi) { + zend_ssa *ssa = ctx->ssa; + if (phi->pi < 0) { + /* Phi assignment with identical source operands */ + int common_source = get_common_phi_source(ssa, phi); + if (common_source >= 0) { + zend_ssa_rename_var_uses(ssa, phi->ssa_var, common_source, 1); + zend_ssa_remove_phi(ssa, phi); + } + } else { + /* Pi assignment that is only used in Phi/Pi assignments */ + // TODO What if we want to rerun type inference after DCE? Maybe separate this? + /*ZEND_ASSERT(phi->sources[0] != -1); + if (ssa->vars[phi->ssa_var].use_chain < 0) { + zend_ssa_rename_var_uses_keep_types(ssa, phi->ssa_var, phi->sources[0], 1); + zend_ssa_remove_phi(ssa, phi); + }*/ + } +} + +int dce_optimize_op_array(zend_op_array *op_array, zend_ssa *ssa, zend_bool reorder_dtor_effects) { + int i; + zend_ssa_phi *phi; + int removed_ops = 0; + + /* DCE of CV operations may affect vararg functions. For now simply treat all instructions + * as live if varargs in use and only collect dead phis. */ + zend_bool has_varargs = (ZEND_FUNC_INFO(op_array)->flags & ZEND_FUNC_VARARG) != 0; + + context ctx; + ctx.ssa = ssa; + ctx.op_array = op_array; + ctx.reorder_dtor_effects = reorder_dtor_effects; + + /* We have no dedicated phi vector, so we use the whole ssa var vector instead */ + ctx.instr_worklist_len = zend_bitset_len(op_array->last); + ctx.instr_worklist = alloca(sizeof(zend_ulong) * ctx.instr_worklist_len); + memset(ctx.instr_worklist, 0, sizeof(zend_ulong) * ctx.instr_worklist_len); + ctx.phi_worklist_len = zend_bitset_len(ssa->vars_count); + ctx.phi_worklist = alloca(sizeof(zend_ulong) * ctx.phi_worklist_len); + memset(ctx.phi_worklist, 0, sizeof(zend_ulong) * ctx.phi_worklist_len); + + /* Optimistically assume all instructions and phis to be dead */ + ctx.instr_dead = alloca(sizeof(zend_ulong) * ctx.instr_worklist_len); + memset(ctx.instr_dead, 0xff, sizeof(zend_ulong) * ctx.instr_worklist_len); + ctx.phi_dead = alloca(sizeof(zend_ulong) * ctx.phi_worklist_len); + memset(ctx.phi_dead, 0xff, sizeof(zend_ulong) * ctx.phi_worklist_len); + + /* Mark instruction with side effects as live */ + FOREACH_INSTR_NUM(i) { + if (may_have_side_effects(&ctx, &op_array->opcodes[i], &ssa->ops[i]) || has_varargs) { + zend_bitset_excl(ctx.instr_dead, i); + add_operands_to_worklists(&ctx, &op_array->opcodes[i], &ssa->ops[i]); + } + } FOREACH_INSTR_NUM_END(); + + /* Propagate liveness backwards to all definitions of used vars */ + while (!zend_bitset_empty(ctx.instr_worklist, ctx.instr_worklist_len) + || !zend_bitset_empty(ctx.phi_worklist, ctx.phi_worklist_len)) { + while ((i = zend_bitset_pop_first(ctx.instr_worklist, ctx.instr_worklist_len)) >= 0) { + zend_bitset_excl(ctx.instr_dead, i); + add_operands_to_worklists(&ctx, &op_array->opcodes[i], &ssa->ops[i]); + } + while ((i = zend_bitset_pop_first(ctx.phi_worklist, ctx.phi_worklist_len)) >= 0) { + zend_bitset_excl(ctx.phi_dead, i); + add_phi_sources_to_worklists(&ctx, ssa->vars[i].definition_phi); + } + } + + /* Eliminate dead instructions */ + FOREACH_INSTR_NUM(i) { + if (zend_bitset_in(ctx.instr_dead, i)) { + if (dce_instr(&ctx, &op_array->opcodes[i], &ssa->ops[i])) { + removed_ops++; + } + } + } FOREACH_INSTR_NUM_END(); + + /* Improper uses don't count as "uses" for the purpose of instruction elimination, + * but we have to retain phis defining them. Push those phis to the worklist. */ + FOREACH_INSTR_NUM(i) { + if (zend_has_improper_op1_use(&op_array->opcodes[i])) { + ZEND_ASSERT(ssa->ops[i].op1_use >= 0); + add_to_phi_worklist_only(&ctx, ssa->ops[i].op1_use); + } + } FOREACH_INSTR_NUM_END(); + + /* Propagate this information backwards, marking any phi with an improperly used + * target as non-dead. */ + while ((i = zend_bitset_pop_first(ctx.phi_worklist, ctx.phi_worklist_len)) >= 0) { + zend_ssa_phi *phi = ssa->vars[i].definition_phi; + int source; + zend_bitset_excl(ctx.phi_dead, i); + FOREACH_PHI_SOURCE(phi, source) { + add_to_phi_worklist_only(&ctx, source); + } FOREACH_PHI_SOURCE_END(); + } + + /* Now collect the actually dead phis */ + FOREACH_PHI(phi) { + if (zend_bitset_in(ctx.phi_dead, phi->ssa_var)) { + zend_ssa_remove_uses_of_var(ssa, phi->ssa_var); + zend_ssa_remove_phi(ssa, phi); + } + } FOREACH_PHI_END(); + + /* Remove trivial phis (phis with identical source operands) */ + FOREACH_PHI(phi) { + try_remove_trivial_phi(&ctx, phi); + } FOREACH_PHI_END(); + + removed_ops += simplify_jumps(ssa, op_array); + + return removed_ops; +} diff --git a/ext/opcache/Optimizer/dfa_pass.c b/ext/opcache/Optimizer/dfa_pass.c index 28b74f34634..105827af898 100644 --- a/ext/opcache/Optimizer/dfa_pass.c +++ b/ext/opcache/Optimizer/dfa_pass.c @@ -719,6 +719,15 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx } } + if (ZEND_OPTIMIZER_PASS_14 & ctx->optimization_level) { + if (dce_optimize_op_array(op_array, ssa, 0)) { + remove_nops = 1; + } + if (ctx->debug_level & ZEND_DUMP_AFTER_DCE_PASS) { + zend_dump_op_array(op_array, ZEND_DUMP_SSA, "after dce pass", ssa); + } + } + if (remove_nops) { zend_ssa_remove_nops(op_array, ssa); } diff --git a/ext/opcache/Optimizer/zend_inference.c b/ext/opcache/Optimizer/zend_inference.c index a3d1be177e0..a74abec2603 100644 --- a/ext/opcache/Optimizer/zend_inference.c +++ b/ext/opcache/Optimizer/zend_inference.c @@ -3927,6 +3927,294 @@ void zend_inference_check_recursive_dependencies(zend_op_array *op_array) free_alloca(worklist, use_heap); } +int zend_may_throw(const zend_op *opline, zend_op_array *op_array, zend_ssa *ssa) +{ + uint32_t t1 = OP1_INFO(); + uint32_t t2 = OP2_INFO(); + + if (opline->op1_type == IS_CV) { + if (t1 & MAY_BE_UNDEF) { + switch (opline->opcode) { + case ZEND_UNSET_VAR: + case ZEND_ISSET_ISEMPTY_VAR: + if (opline->extended_value & ZEND_QUICK_SET) { + break; + } + case ZEND_ISSET_ISEMPTY_DIM_OBJ: + case ZEND_ISSET_ISEMPTY_PROP_OBJ: + case ZEND_ASSIGN: + case ZEND_ASSIGN_DIM: + case ZEND_ASSIGN_REF: + case ZEND_BIND_GLOBAL: + case ZEND_FETCH_DIM_IS: + case ZEND_FETCH_OBJ_IS: + case ZEND_SEND_REF: + break; + default: + /* undefined variable warning */ + return 1; + } + } + } else if (opline->op1_type & (IS_TMP_VAR|IS_VAR)) { + if (t1 & (MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_ARRAY_OF_RESOURCE|MAY_BE_ARRAY_OF_ARRAY)) { + switch (opline->opcode) { + case ZEND_CASE: + case ZEND_FE_FETCH_R: + case ZEND_FE_FETCH_RW: + case ZEND_FETCH_LIST: + case ZEND_QM_ASSIGN: + case ZEND_SEND_VAL: + case ZEND_SEND_VAL_EX: + case ZEND_SEND_VAR: + case ZEND_SEND_VAR_EX: + case ZEND_SEND_VAR_NO_REF: + case ZEND_SEND_VAR_NO_REF_EX: + case ZEND_SEND_REF: + case ZEND_SEPARATE: + case ZEND_END_SILENCE: + break; + default: + /* destructor may be called */ + return 1; + } + } + } + + if (opline->op2_type == IS_CV) { + if (t2 & MAY_BE_UNDEF) { + switch (opline->opcode) { + case ZEND_ASSIGN_REF: + break; + default: + /* undefined variable warning */ + return 1; + } + } + } else if (opline->op2_type & (IS_TMP_VAR|IS_VAR)) { + if (t2 & (MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_ARRAY_OF_RESOURCE|MAY_BE_ARRAY_OF_ARRAY)) { + switch (opline->opcode) { + case ZEND_ASSIGN: + break; + default: + /* destructor may be called */ + return 1; + } + } + } + + switch (opline->opcode) { + case ZEND_NOP: + case ZEND_IS_IDENTICAL: + case ZEND_IS_NOT_IDENTICAL: + case ZEND_QM_ASSIGN: + case ZEND_JMP: + case ZEND_CHECK_VAR: + case ZEND_MAKE_REF: + case ZEND_SEND_VAR: + case ZEND_BEGIN_SILENCE: + case ZEND_END_SILENCE: + case ZEND_SEND_VAL: + case ZEND_SEND_REF: + case ZEND_SEND_VAR_EX: + case ZEND_FREE: + case ZEND_SEPARATE: + case ZEND_TYPE_CHECK: + case ZEND_DEFINED: + case ZEND_ISSET_ISEMPTY_THIS: + case ZEND_COALESCE: + case ZEND_SWITCH_LONG: + case ZEND_SWITCH_STRING: + return 0; + case ZEND_INIT_FCALL: + /* can't throw, because call is resolved at compile time */ + return 0; + case ZEND_BIND_GLOBAL: + if ((opline+1)->opcode == ZEND_BIND_GLOBAL) { + return zend_may_throw(opline + 1, op_array, ssa); + } + return 0; + case ZEND_ADD: + if ((t1 & MAY_BE_ANY) == MAY_BE_ARRAY + && (t2 & MAY_BE_ANY) == MAY_BE_ARRAY) { + return 0; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_DIV: + case ZEND_MOD: + if (!OP2_HAS_RANGE() || + (OP2_MIN_RANGE() <= 0 && OP2_MAX_RANGE() >= 0)) { + /* Division by zero */ + return 1; + } + /* break missing intentionally */ + case ZEND_SUB: + case ZEND_MUL: + case ZEND_POW: + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_SL: + case ZEND_SR: + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + !OP2_HAS_RANGE() || + OP2_MIN_RANGE() < 0; + case ZEND_CONCAT: + case ZEND_FAST_CONCAT: + return (t1 & (MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_BW_OR: + case ZEND_BW_AND: + case ZEND_BW_XOR: + if ((t1 & MAY_BE_ANY) == MAY_BE_STRING + && (t2 & MAY_BE_ANY) == MAY_BE_STRING) { + return 0; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_BW_NOT: + return (t1 & (MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_BOOL_NOT: + case ZEND_PRE_INC: + case ZEND_POST_INC: + case ZEND_PRE_DEC: + case ZEND_POST_DEC: + case ZEND_JMPZ: + case ZEND_JMPNZ: + case ZEND_JMPZNZ: + case ZEND_JMPZ_EX: + case ZEND_JMPNZ_EX: + case ZEND_BOOL: + case ZEND_JMP_SET: + return (t1 & MAY_BE_OBJECT); + case ZEND_BOOL_XOR: + return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_OBJECT); + case ZEND_IS_EQUAL: + case ZEND_IS_NOT_EQUAL: + case ZEND_IS_SMALLER: + case ZEND_IS_SMALLER_OR_EQUAL: + case ZEND_CASE: + case ZEND_SPACESHIP: + if ((t1 & MAY_BE_ANY) == MAY_BE_NULL + || (t2 & MAY_BE_ANY) == MAY_BE_NULL) { + return 0; + } + return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_OBJECT); + case ZEND_ASSIGN_ADD: + if (opline->extended_value != 0) { + return 1; + } + if ((t1 & MAY_BE_ANY) == MAY_BE_ARRAY + && (t2 & MAY_BE_ANY) == MAY_BE_ARRAY) { + return 0; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_ASSIGN_DIV: + case ZEND_ASSIGN_MOD: + if (opline->extended_value != 0) { + return 1; + } + if (!OP2_HAS_RANGE() || + (OP2_MIN_RANGE() <= 0 && OP2_MAX_RANGE() >= 0)) { + /* Division by zero */ + return 1; + } + /* break missing intentionally */ + case ZEND_ASSIGN_SUB: + case ZEND_ASSIGN_MUL: + case ZEND_ASSIGN_POW: + if (opline->extended_value != 0) { + return 1; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_ASSIGN_SL: + case ZEND_ASSIGN_SR: + if (opline->extended_value != 0) { + return 1; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + !OP2_HAS_RANGE() || + OP2_MIN_RANGE() < 0; + case ZEND_ASSIGN_CONCAT: + if (opline->extended_value != 0) { + return 1; + } + return (t1 & (MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_ASSIGN_BW_OR: + case ZEND_ASSIGN_BW_AND: + case ZEND_ASSIGN_BW_XOR: + if (opline->extended_value != 0) { + return 1; + } + if ((t1 & MAY_BE_ANY) == MAY_BE_STRING + && (t2 & MAY_BE_ANY) == MAY_BE_STRING) { + return 0; + } + return (t1 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)) || + (t2 & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_ASSIGN: + return (t1 & (MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_ARRAY_OF_RESOURCE|MAY_BE_ARRAY_OF_ARRAY)); + case ZEND_ASSIGN_DIM: + return (t1 & (MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_TRUE|MAY_BE_STRING|MAY_BE_LONG|MAY_BE_DOUBLE)) || opline->op2_type == IS_UNUSED || + (t2 & (MAY_BE_UNDEF|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE)); + case ZEND_ROPE_INIT: + case ZEND_ROPE_ADD: + case ZEND_ROPE_END: + return t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT); + case ZEND_INIT_ARRAY: + case ZEND_ADD_ARRAY_ELEMENT: + return (opline->op2_type != IS_UNUSED) && (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE)); + case ZEND_STRLEN: + return (t1 & MAY_BE_ANY) != MAY_BE_STRING; + case ZEND_RECV_INIT: + if (Z_CONSTANT_P(CRT_CONSTANT_EX(op_array, opline->op2, ssa->rt_constants))) { + return 1; + } + if (op_array->fn_flags & ZEND_ACC_HAS_TYPE_HINTS) { + uint32_t arg_num = opline->op1.num; + zend_arg_info *cur_arg_info; + + if (EXPECTED(arg_num <= op_array->num_args)) { + cur_arg_info = &op_array->arg_info[arg_num-1]; + } else if (UNEXPECTED(op_array->fn_flags & ZEND_ACC_VARIADIC)) { + cur_arg_info = &op_array->arg_info[op_array->num_args]; + } else { + return 0; + } + return ZEND_TYPE_IS_SET(cur_arg_info->type); + } else { + return 0; + } + case ZEND_ISSET_ISEMPTY_DIM_OBJ: + case ZEND_FETCH_DIM_IS: + return (t1 & MAY_BE_OBJECT) || (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT)); + case ZEND_CAST: + switch (opline->extended_value) { + case IS_NULL: + return 0; + case _IS_BOOL: + return (t1 & MAY_BE_OBJECT); + case IS_LONG: + case IS_DOUBLE: + return (t1 & (MAY_BE_STRING|MAY_BE_OBJECT)); + case IS_STRING: + return (t1 & (MAY_BE_ARRAY|MAY_BE_OBJECT)); + case IS_ARRAY: + return (t1 & MAY_BE_OBJECT); + case IS_OBJECT: + return (t1 & MAY_BE_ARRAY); + default: + return 1; + } + default: + return 1; + } +} + /* * Local variables: * tab-width: 4 diff --git a/ext/opcache/Optimizer/zend_inference.h b/ext/opcache/Optimizer/zend_inference.h index fecb124142c..c10946a6385 100644 --- a/ext/opcache/Optimizer/zend_inference.h +++ b/ext/opcache/Optimizer/zend_inference.h @@ -32,6 +32,9 @@ #define MAY_BE_RC1 (1<<27) /* may be non-reference with refcount == 1 */ #define MAY_BE_RCN (1<<28) /* may be non-reference with refcount > 1 */ +#define MAY_HAVE_DTOR \ + (MAY_BE_OBJECT|MAY_BE_RESOURCE \ + |MAY_BE_ARRAY_OF_ARRAY|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_ARRAY_OF_RESOURCE) #define DEFINE_SSA_OP_HAS_RANGE(opN) \ static zend_always_inline zend_bool _ssa_##opN##_has_range(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline) \ @@ -263,6 +266,8 @@ void zend_func_return_info(const zend_op_array *op_array, int widening, zend_ssa_var_info *ret); +int zend_may_throw(const zend_op *opline, zend_op_array *op_array, zend_ssa *ssa); + END_EXTERN_C() #endif /* ZEND_INFERENCE_H */ diff --git a/ext/opcache/Optimizer/zend_optimizer.c b/ext/opcache/Optimizer/zend_optimizer.c index 03558d8e9c5..afd1c15597e 100644 --- a/ext/opcache/Optimizer/zend_optimizer.c +++ b/ext/opcache/Optimizer/zend_optimizer.c @@ -1068,6 +1068,15 @@ static void zend_optimize(zend_op_array *op_array, } } + if ((ZEND_OPTIMIZER_PASS_13 & ctx->optimization_level) && + (!(ZEND_OPTIMIZER_PASS_6 & ctx->optimization_level) || + !(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level))) { + zend_optimizer_compact_vars(op_array); + if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_13) { + zend_dump_op_array(op_array, 0, "after pass 13", NULL); + } + } + if (ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level) { return; } @@ -1283,6 +1292,15 @@ int zend_optimize_script(zend_script *script, zend_long optimization_level, zend } } + if (ZEND_OPTIMIZER_PASS_13 & optimization_level) { + for (i = 0; i < call_graph.op_arrays_count; i++) { + zend_optimizer_compact_vars(call_graph.op_arrays[i]); + if (debug_level & ZEND_DUMP_AFTER_PASS_13) { + zend_dump_op_array(call_graph.op_arrays[i], 0, "after pass 13", NULL); + } + } + } + if (ZEND_OPTIMIZER_PASS_12 & optimization_level) { for (i = 0; i < call_graph.op_arrays_count; i++) { zend_adjust_fcall_stack_size_graph(call_graph.op_arrays[i]); diff --git a/ext/opcache/Optimizer/zend_optimizer.h b/ext/opcache/Optimizer/zend_optimizer.h index f166093f312..b3e28afc29a 100644 --- a/ext/opcache/Optimizer/zend_optimizer.h +++ b/ext/opcache/Optimizer/zend_optimizer.h @@ -37,8 +37,8 @@ #define ZEND_OPTIMIZER_PASS_10 (1<<9) /* NOP removal */ #define ZEND_OPTIMIZER_PASS_11 (1<<10) /* Merge equal constants */ #define ZEND_OPTIMIZER_PASS_12 (1<<11) /* Adjust used stack */ -#define ZEND_OPTIMIZER_PASS_13 (1<<12) -#define ZEND_OPTIMIZER_PASS_14 (1<<13) +#define ZEND_OPTIMIZER_PASS_13 (1<<12) /* Remove unused variables */ +#define ZEND_OPTIMIZER_PASS_14 (1<<13) /* DCE (dead code elimination) */ #define ZEND_OPTIMIZER_PASS_15 (1<<14) /* Collect constants */ #define ZEND_OPTIMIZER_PASS_16 (1<<15) /* Inline functions */ @@ -78,6 +78,7 @@ #define ZEND_DUMP_DFA_SSA (1<<27) #define ZEND_DUMP_DFA_SSA_VARS (1<<28) #define ZEND_DUMP_AFTER_SCCP_PASS (1<<29) +#define ZEND_DUMP_AFTER_DCE_PASS (1<<30) typedef struct _zend_script { zend_string *filename; diff --git a/ext/opcache/Optimizer/zend_optimizer_internal.h b/ext/opcache/Optimizer/zend_optimizer_internal.h index 407154f55c3..a4b2d137900 100644 --- a/ext/opcache/Optimizer/zend_optimizer_internal.h +++ b/ext/opcache/Optimizer/zend_optimizer_internal.h @@ -104,6 +104,7 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimizer_nop_removal(zend_op_array *op_array); void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx *ctx); +void zend_optimizer_compact_vars(zend_op_array *op_array); int zend_optimizer_is_disabled_func(const char *name, size_t len); zend_function *zend_optimizer_get_called_func( zend_script *script, zend_op_array *op_array, zend_op *opline, zend_bool rt_constants); @@ -112,5 +113,11 @@ void zend_optimizer_migrate_jump(zend_op_array *op_array, zend_op *new_opline, z void zend_optimizer_shift_jump(zend_op_array *op_array, zend_op *opline, uint32_t *shiftlist); zend_uchar zend_compound_assign_to_binary_op(zend_uchar opcode); int sccp_optimize_op_array(zend_optimizer_ctx *ctx, zend_op_array *op_arrya, zend_ssa *ssa, zend_call_info **call_map); +int dce_optimize_op_array(zend_op_array *op_array, zend_ssa *ssa, zend_bool reorder_dtor_effects); + +static inline zend_bool zend_has_improper_op1_use(zend_op *opline) { + return opline->opcode == ZEND_ASSIGN + || (opline->opcode == ZEND_UNSET_VAR && opline->extended_value & ZEND_QUICK_SET); +} #endif diff --git a/ext/opcache/Optimizer/zend_ssa.c b/ext/opcache/Optimizer/zend_ssa.c index 3547e2bc9d2..b0eb87995d2 100644 --- a/ext/opcache/Optimizer/zend_ssa.c +++ b/ext/opcache/Optimizer/zend_ssa.c @@ -1450,6 +1450,135 @@ void zend_ssa_remove_block(zend_op_array *op_array, zend_ssa *ssa, int i) /* {{{ } /* }}} */ +static void propagate_phi_type_widening(zend_ssa *ssa, int var) /* {{{ */ +{ + zend_ssa_phi *phi; + FOREACH_PHI_USE(&ssa->vars[var], phi) { + if (ssa->var_info[var].type & ~ssa->var_info[phi->ssa_var].type) { + ssa->var_info[phi->ssa_var].type |= ssa->var_info[var].type; + propagate_phi_type_widening(ssa, phi->ssa_var); + } + } FOREACH_PHI_USE_END(); +} +/* }}} */ + +void zend_ssa_rename_var_uses(zend_ssa *ssa, int old, int new, zend_bool update_types) /* {{{ */ +{ + zend_ssa_var *old_var = &ssa->vars[old]; + zend_ssa_var *new_var = &ssa->vars[new]; + int use; + zend_ssa_phi *phi; + + ZEND_ASSERT(old >= 0 && new >= 0); + ZEND_ASSERT(old != new); + + /* Only a no_val is both variables are */ + new_var->no_val &= old_var->no_val; + + /* Update ssa_op use chains */ + FOREACH_USE(old_var, use) { + zend_ssa_op *ssa_op = &ssa->ops[use]; + + /* If the op already uses the new var, don't add the op to the use + * list again. Instead move the use_chain to the correct operand. */ + zend_bool add_to_use_chain = 1; + if (ssa_op->result_use == new) { + add_to_use_chain = 0; + } else if (ssa_op->op1_use == new) { + if (ssa_op->result_use == old) { + ssa_op->res_use_chain = ssa_op->op1_use_chain; + ssa_op->op1_use_chain = -1; + } + add_to_use_chain = 0; + } else if (ssa_op->op2_use == new) { + if (ssa_op->result_use == old) { + ssa_op->res_use_chain = ssa_op->op2_use_chain; + ssa_op->op2_use_chain = -1; + } else if (ssa_op->op1_use == old) { + ssa_op->op1_use_chain = ssa_op->op2_use_chain; + ssa_op->op2_use_chain = -1; + } + add_to_use_chain = 0; + } + + /* Perform the actual renaming */ + if (ssa_op->result_use == old) { + ssa_op->result_use = new; + } + if (ssa_op->op1_use == old) { + ssa_op->op1_use = new; + } + if (ssa_op->op2_use == old) { + ssa_op->op2_use = new; + } + + /* Add op to use chain of new var (if it isn't already). We use the + * first use chain of (result, op1, op2) that has the new variable. */ + if (add_to_use_chain) { + if (ssa_op->result_use == new) { + ssa_op->res_use_chain = new_var->use_chain; + new_var->use_chain = use; + } else if (ssa_op->op1_use == new) { + ssa_op->op1_use_chain = new_var->use_chain; + new_var->use_chain = use; + } else { + ZEND_ASSERT(ssa_op->op2_use == new); + ssa_op->op2_use_chain = new_var->use_chain; + new_var->use_chain = use; + } + } + } FOREACH_USE_END(); + old_var->use_chain = -1; + + /* Update phi use chains */ + FOREACH_PHI_USE(old_var, phi) { + int j; + zend_bool after_first_new_source = 0; + + /* If the phi already uses the new var, find its use chain, as we may + * need to move it to a different source operand. */ + zend_ssa_phi **existing_use_chain_ptr = NULL; + for (j = 0; j < ssa->cfg.blocks[phi->block].predecessors_count; j++) { + if (phi->sources[j] == new) { + existing_use_chain_ptr = &phi->use_chains[j]; + break; + } + } + + for (j = 0; j < ssa->cfg.blocks[phi->block].predecessors_count; j++) { + if (phi->sources[j] == new) { + after_first_new_source = 1; + } else if (phi->sources[j] == old) { + phi->sources[j] = new; + + /* Either move existing use chain to this source, or add the phi + * to the phi use chain of the new variables. Do this only once. */ + if (!after_first_new_source) { + if (existing_use_chain_ptr) { + phi->use_chains[j] = *existing_use_chain_ptr; + *existing_use_chain_ptr = NULL; + } else { + phi->use_chains[j] = new_var->phi_use_chain; + new_var->phi_use_chain = phi; + } + after_first_new_source = 1; + } + } + } + + /* Make sure phi result types are not incorrectly narrow after renaming. + * This should not normally happen, but can occur if we DCE an assignment + * or unset and there is an improper phi-indirected use lateron. */ + // TODO Alternatively we could rerun type-inference after DCE + if (update_types && (ssa->var_info[new].type & ~ssa->var_info[phi->ssa_var].type)) { + ssa->var_info[phi->ssa_var].type |= ssa->var_info[new].type; + propagate_phi_type_widening(ssa, phi->ssa_var); + } + } FOREACH_PHI_USE_END(); + old_var->phi_use_chain = NULL; +} +/* }}} */ + /* * Local variables: * tab-width: 4 diff --git a/ext/opcache/Optimizer/zend_ssa.h b/ext/opcache/Optimizer/zend_ssa.h index 1cfe58a1078..c7724fa0320 100644 --- a/ext/opcache/Optimizer/zend_ssa.h +++ b/ext/opcache/Optimizer/zend_ssa.h @@ -143,6 +143,7 @@ void zend_ssa_remove_instr(zend_ssa *ssa, zend_op *opline, zend_ssa_op *ssa_op); void zend_ssa_remove_phi(zend_ssa *ssa, zend_ssa_phi *phi); void zend_ssa_remove_uses_of_var(zend_ssa *ssa, int var_num); void zend_ssa_remove_block(zend_op_array *op_array, zend_ssa *ssa, int b); +void zend_ssa_rename_var_uses(zend_ssa *ssa, int old_var, int new_var, zend_bool update_types); static zend_always_inline void _zend_ssa_remove_def(zend_ssa_var *var) { @@ -214,6 +215,31 @@ static zend_always_inline zend_bool zend_ssa_is_no_val_use(const zend_op *opline return 0; } +static zend_always_inline void zend_ssa_rename_defs_of_instr(zend_ssa *ssa, zend_ssa_op *ssa_op) { + /* Rename def to use if possible. Mark variable as not defined otherwise. */ + if (ssa_op->op1_def >= 0) { + if (ssa_op->op1_use >= 0) { + zend_ssa_rename_var_uses(ssa, ssa_op->op1_def, ssa_op->op1_use, 1); + } + ssa->vars[ssa_op->op1_def].definition = -1; + ssa_op->op1_def = -1; + } + if (ssa_op->op2_def >= 0) { + if (ssa_op->op2_use >= 0) { + zend_ssa_rename_var_uses(ssa, ssa_op->op2_def, ssa_op->op2_use, 1); + } + ssa->vars[ssa_op->op2_def].definition = -1; + ssa_op->op2_def = -1; + } + if (ssa_op->result_def >= 0) { + if (ssa_op->result_use >= 0) { + zend_ssa_rename_var_uses(ssa, ssa_op->result_def, ssa_op->result_use, 1); + } + ssa->vars[ssa_op->result_def].definition = -1; + ssa_op->result_def = -1; + } +} + #define NUM_PHI_SOURCES(phi) \ ((phi)->pi >= 0 ? 1 : (ssa->cfg.blocks[(phi)->block].predecessors_count)) @@ -246,6 +272,38 @@ static zend_always_inline zend_bool zend_ssa_is_no_val_use(const zend_op *opline } \ } while (0) +#define FOREACH_PHI(phi) do { \ + int _i; \ + for (_i = 0; _i < ssa->cfg.blocks_count; _i++) { \ + phi = ssa->blocks[_i].phis; \ + for (; phi; phi = phi->next) { +#define FOREACH_PHI_END() \ + } \ + } \ +} while (0) + +#define FOREACH_BLOCK(block) do { \ + int _i; \ + for (_i = 0; _i < ssa->cfg.blocks_count; _i++) { \ + (block) = &ssa->cfg.blocks[_i]; \ + if (!((block)->flags & ZEND_BB_REACHABLE)) { \ + continue; \ + } +#define FOREACH_BLOCK_END() \ + } \ +} while (0) + +/* Does not support "break" */ +#define FOREACH_INSTR_NUM(i) do { \ + zend_basic_block *_block; \ + FOREACH_BLOCK(_block) { \ + uint32_t _end = _block->start + _block->len; \ + for ((i) = _block->start; (i) < _end; (i)++) { +#define FOREACH_INSTR_NUM_END() \ + } \ + } FOREACH_BLOCK_END(); \ +} while (0) + #endif /* ZEND_SSA_H */ /* diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 index f27ac8e5106..7b500f023d6 100644 --- a/ext/opcache/config.m4 +++ b/ext/opcache/config.m4 @@ -412,6 +412,8 @@ fi Optimizer/zend_call_graph.c \ Optimizer/sccp.c \ Optimizer/scdf.c \ + Optimizer/dce.c \ + Optimizer/compact_vars.c \ Optimizer/zend_dump.c, shared,,-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1,,yes)