Skip to content

Commit d8c9c66

Browse files
committed
Implement implicit move optimisation
## 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.
1 parent 56f916e commit d8c9c66

9 files changed

+262
-74
lines changed

Zend/Optimizer/dfa_pass.c

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,10 @@ int zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
466466
int var_num = ssa_op->op1_use;
467467
zend_ssa_var *var = ssa->vars + var_num;
468468

469-
ZEND_ASSERT(ssa_op->op1_def < 0);
469+
if (ssa_op->op1_def >= 0) {
470+
zend_ssa_replace_op1_def_op1_use(ssa, ssa_op);
471+
}
472+
470473
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
471474
ssa_op->op1_use = -1;
472475
ssa_op->op1_use_chain = -1;
@@ -1066,6 +1069,50 @@ static bool zend_dfa_try_to_replace_result(zend_op_array *op_array, zend_ssa *ss
10661069
return 0;
10671070
}
10681071

1072+
/* Sets a flag on SEND ops when a copy can be a avoided. */
1073+
static void zend_dfa_optimize_send_copies(zend_op_array *op_array, zend_ssa *ssa)
1074+
{
1075+
/* func_get_args(), indirect accesses and exceptions could make the optimization observable.
1076+
* The latter two cases are already tested before applying the DFA pass. */
1077+
if (ssa->cfg.flags & ZEND_FUNC_VARARG) {
1078+
return;
1079+
}
1080+
1081+
for (uint32_t i = 0; i < op_array->last; i++) {
1082+
zend_op *opline = op_array->opcodes + i;
1083+
if ((opline->opcode != ZEND_SEND_VAR && opline->opcode != ZEND_SEND_VAR_EX) || opline->op2_type != IS_UNUSED || opline->op1_type != IS_CV) {
1084+
continue;
1085+
}
1086+
1087+
zend_ssa_op *ssa_op = ssa->ops + i;
1088+
int op1_def = ssa_op->op1_def;
1089+
if (op1_def == -1) {
1090+
continue;
1091+
}
1092+
1093+
int ssa_cv = ssa_op->op1_use;
1094+
1095+
/* Argument move must not be observable in backtraces */
1096+
if (ssa->vars[ssa_cv].var < op_array->num_args) {
1097+
continue;
1098+
}
1099+
1100+
/* Unsetting a CV is always fine if it gets overwritten afterwards.
1101+
* Since type inference often infers very wide types, we are very loose in matching types. */
1102+
uint32_t type = ssa->var_info[ssa_cv].type;
1103+
if ((type & (MAY_BE_REF|MAY_BE_UNDEF)) || !(type & MAY_BE_RC1) || !(type & (MAY_BE_STRING|MAY_BE_ARRAY))) {
1104+
continue;
1105+
}
1106+
1107+
zend_ssa_var *ssa_var = ssa->vars + op1_def;
1108+
1109+
if (ssa_var->no_val && !ssa_var->alias) {
1110+
/* Flag will be used by VM type spec handler */
1111+
opline->extended_value = 1;
1112+
}
1113+
}
1114+
}
1115+
10691116
void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa, zend_call_info **call_map)
10701117
{
10711118
if (ctx->debug_level & ZEND_DUMP_BEFORE_DFA_PASS) {
@@ -1124,6 +1171,14 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx
11241171
#endif
11251172
}
11261173

1174+
/* Optimization should not be done on main because of globals. */
1175+
if (op_array->function_name) {
1176+
zend_dfa_optimize_send_copies(op_array, ssa);
1177+
#if ZEND_DEBUG_DFA
1178+
ssa_verify_integrity(op_array, ssa, "after optimize send copies");
1179+
#endif
1180+
}
1181+
11271182
for (v = op_array->last_var; v < ssa->vars_count; v++) {
11281183

11291184
op_1 = ssa->vars[v].definition;

Zend/Optimizer/sccp.c

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ typedef struct _sccp_ctx {
9898
#define MAKE_TOP(zv) (Z_TYPE_INFO_P(zv) = TOP)
9999
#define MAKE_BOT(zv) (Z_TYPE_INFO_P(zv) = BOT)
100100

101-
static void scp_dump_value(zval *zv) {
101+
static void scp_dump_value(const zval *zv) {
102102
if (IS_TOP(zv)) {
103103
fprintf(stderr, " top");
104104
} else if (IS_BOT(zv)) {
@@ -1050,6 +1050,12 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
10501050
case ZEND_SEND_VAL:
10511051
case ZEND_SEND_VAR:
10521052
{
1053+
SKIP_IF_TOP(op1);
1054+
1055+
if (opline->opcode == ZEND_SEND_VAR) {
1056+
SET_RESULT(op1, op1);
1057+
}
1058+
10531059
/* If the value of a SEND for an ICALL changes, we need to reconsider the
10541060
* ICALL result value. Otherwise we can ignore the opcode. */
10551061
zend_call_info *call;
@@ -1058,7 +1064,7 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
10581064
}
10591065

10601066
call = ctx->call_map[opline - ctx->scdf.op_array->opcodes];
1061-
if (IS_TOP(op1) || !call || !call->caller_call_opline
1067+
if (!call || !call->caller_call_opline
10621068
|| call->caller_call_opline->opcode != ZEND_DO_ICALL) {
10631069
return;
10641070
}
@@ -2034,8 +2040,14 @@ static int remove_call(sccp_ctx *ctx, zend_op *opline, zend_ssa_op *ssa_op)
20342040
&ssa->ops[call->caller_init_opline - op_array->opcodes]);
20352041

20362042
for (i = 0; i < call->num_args; i++) {
2037-
zend_ssa_remove_instr(ssa, call->arg_info[i].opline,
2038-
&ssa->ops[call->arg_info[i].opline - op_array->opcodes]);
2043+
zend_op *op = call->arg_info[i].opline;
2044+
zend_ssa_op *this_ssa_op = &ssa->ops[op - op_array->opcodes];
2045+
2046+
if (op->opcode == ZEND_SEND_VAR && this_ssa_op->op1_def >= 0) {
2047+
zend_ssa_replace_op1_def_op1_use(ssa, this_ssa_op);
2048+
}
2049+
2050+
zend_ssa_remove_instr(ssa, op, this_ssa_op);
20392051
}
20402052

20412053
// TODO: remove call_info completely???
@@ -2188,6 +2200,10 @@ static int try_remove_definition(sccp_ctx *ctx, int var_num, zend_ssa_var *var,
21882200
return 0;
21892201
}
21902202

2203+
if (opline->opcode == ZEND_SEND_VAR) {
2204+
return 0;
2205+
}
2206+
21912207
/* Compound assign or incdec -> convert to direct ASSIGN */
21922208

21932209
if (!value) {
@@ -2330,6 +2346,12 @@ static int replace_constant_operands(sccp_ctx *ctx) {
23302346
FOREACH_USE(var, use) {
23312347
zend_op *opline = &op_array->opcodes[use];
23322348
zend_ssa_op *ssa_op = &ssa->ops[use];
2349+
/* Removing the def in try_remove_definition() may reduce optimisation opportunities.
2350+
* We want to keep the no_val definition until we actually replace it with a constant. */
2351+
if (opline->opcode == ZEND_SEND_VAR && ssa_op->op1_use == i && ssa_op->op1_def >= 0) {
2352+
zend_ssa_replace_op1_def_op1_use(ssa, ssa_op);
2353+
opline->extended_value = 0;
2354+
}
23332355
if (try_replace_op1(ctx, opline, ssa_op, i, value)) {
23342356
if (opline->opcode == ZEND_NOP) {
23352357
removed_ops++;

Zend/Optimizer/zend_dfg.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ static zend_always_inline void _zend_dfg_add_use_def_op(const zend_op_array *op_
174174
}
175175
break;
176176
case ZEND_SEND_VAR:
177+
if (opline->op1_type == IS_CV && ((build_flags & ZEND_SSA_RC_INFERENCE) || opline->op2_type == IS_UNUSED)) {
178+
goto add_op1_def;
179+
}
180+
break;
177181
case ZEND_CAST:
178182
case ZEND_QM_ASSIGN:
179183
case ZEND_JMP_SET:

Zend/Optimizer/zend_inference.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,6 +2950,10 @@ static zend_always_inline zend_result _zend_update_type_info(
29502950
if (t1 & (MAY_BE_RC1|MAY_BE_REF)) {
29512951
tmp |= MAY_BE_RCN;
29522952
}
2953+
if ((t1 & (MAY_BE_ARRAY|MAY_BE_STRING)) && (t1 & MAY_BE_RC1) && !(t1 & (MAY_BE_UNDEF|MAY_BE_REF)) && ssa_vars[ssa_op->op1_def].no_val && !ssa_vars[ssa_op->op1_def].alias) {
2954+
/* implicit move may make value undef */
2955+
tmp |= MAY_BE_UNDEF;
2956+
}
29532957
UPDATE_SSA_TYPE(tmp, ssa_op->op1_def);
29542958
COPY_SSA_OBJ_TYPE(ssa_op->op1_use, ssa_op->op1_def);
29552959
}
@@ -2991,6 +2995,10 @@ static zend_always_inline zend_result _zend_update_type_info(
29912995
case ZEND_SEND_FUNC_ARG:
29922996
if (ssa_op->op1_def >= 0) {
29932997
tmp = (t1 & MAY_BE_UNDEF)|MAY_BE_REF|MAY_BE_RC1|MAY_BE_RCN|MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
2998+
if (opline->opcode == ZEND_SEND_VAR_EX && (t1 & (MAY_BE_ARRAY|MAY_BE_STRING)) && (t1 & MAY_BE_RC1) && !(t1 & (MAY_BE_UNDEF|MAY_BE_REF)) && ssa_vars[ssa_op->op1_def].no_val && !ssa_vars[ssa_op->op1_def].alias) {
2999+
/* implicit move may make value undef */
3000+
tmp |= MAY_BE_UNDEF;
3001+
}
29943002
UPDATE_SSA_TYPE(tmp, ssa_op->op1_def);
29953003
}
29963004
break;

Zend/Optimizer/zend_ssa.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ static zend_always_inline int _zend_ssa_rename_op(const zend_op_array *op_array,
703703
}
704704
break;
705705
case ZEND_SEND_VAR:
706+
if (opline->op1_type == IS_CV && ((build_flags & ZEND_SSA_RC_INFERENCE) || opline->op2_type == IS_UNUSED)) {
707+
goto add_op1_def;
708+
}
709+
break;
706710
case ZEND_CAST:
707711
case ZEND_QM_ASSIGN:
708712
case ZEND_JMP_SET:
@@ -1680,3 +1684,13 @@ void zend_ssa_rename_var_uses(zend_ssa *ssa, int old, int new, bool update_types
16801684
old_var->phi_use_chain = NULL;
16811685
}
16821686
/* }}} */
1687+
1688+
void zend_ssa_replace_op1_def_op1_use(zend_ssa *ssa, zend_ssa_op *ssa_op)
1689+
{
1690+
int op1_new = ssa_op->op1_use;
1691+
ZEND_ASSERT(op1_new >= 0);
1692+
ZEND_ASSERT(ssa_op->op1_def >= 0);
1693+
/* zend_ssa_rename_var_uses() clear use_chain & phi_use_chain for us */
1694+
zend_ssa_rename_var_uses(ssa, ssa_op->op1_def, op1_new, true);
1695+
zend_ssa_remove_op1_def(ssa, ssa_op);
1696+
}

Zend/Optimizer/zend_ssa.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ void zend_ssa_remove_uses_of_var(zend_ssa *ssa, int var_num);
159159
void zend_ssa_remove_block(zend_op_array *op_array, zend_ssa *ssa, int b);
160160
void zend_ssa_rename_var_uses(zend_ssa *ssa, int old_var, int new_var, bool update_types);
161161
void zend_ssa_remove_block_from_cfg(zend_ssa *ssa, int b);
162+
void zend_ssa_replace_op1_def_op1_use(zend_ssa *ssa, zend_ssa_op *ssa_op);
162163

163164
static zend_always_inline void _zend_ssa_remove_def(zend_ssa_var *var)
164165
{

Zend/zend_vm_def.h

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9900,7 +9900,7 @@ ZEND_VM_C_LABEL(fetch_dim_r_index_undef):
99009900
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
99019901
}
99029902

9903-
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR, op->op2_type == IS_UNUSED && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0, ZEND_SEND_VAR_SIMPLE, CV|VAR, NUM)
9903+
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR, op->op2_type == IS_UNUSED && !op->extended_value && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0, ZEND_SEND_VAR_SIMPLE, CV|VAR, NUM)
99049904
{
99059905
USE_OPLINE
99069906
zval *varptr, *arg;
@@ -9917,7 +9917,21 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR, op->op2_type == IS_UNUSED && (op1_i
99179917
ZEND_VM_NEXT_OPCODE();
99189918
}
99199919

9920-
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR_EX, op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0, ZEND_SEND_VAR_EX_SIMPLE, CV|VAR, UNUSED|NUM)
9920+
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR, op->extended_value /* extended_value implies here OP2 UNUSED and OP1 not UNDEF or REF */, ZEND_SEND_VAR_SIMPLE_EXT, CV, NUM)
9921+
{
9922+
USE_OPLINE
9923+
zval *varptr, *arg;
9924+
9925+
varptr = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
9926+
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
9927+
9928+
ZVAL_COPY_VALUE(arg, varptr);
9929+
ZVAL_UNDEF(varptr);
9930+
9931+
ZEND_VM_NEXT_OPCODE();
9932+
}
9933+
9934+
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR_EX, !op->extended_value && op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0, ZEND_SEND_VAR_EX_SIMPLE, CV|VAR, UNUSED|NUM)
99219935
{
99229936
USE_OPLINE
99239937
zval *varptr, *arg;
@@ -9939,6 +9953,25 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR_EX, op->op2_type == IS_UNUSED && op-
99399953
ZEND_VM_NEXT_OPCODE();
99409954
}
99419955

9956+
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAR_EX, op->extended_value && op->op2.num <= MAX_ARG_FLAG_NUM /* extended_value implies here OP2 UNUSED and OP1 not UNDEF or REF */, ZEND_SEND_VAR_EX_SIMPLE_EXT, CV, UNUSED|NUM)
9957+
{
9958+
USE_OPLINE
9959+
zval *varptr, *arg;
9960+
uint32_t arg_num = opline->op2.num;
9961+
9962+
if (QUICK_ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
9963+
ZEND_VM_DISPATCH_TO_HANDLER(ZEND_SEND_REF);
9964+
}
9965+
9966+
varptr = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
9967+
arg = ZEND_CALL_VAR(EX(call), opline->result.var);
9968+
9969+
ZVAL_COPY_VALUE(arg, varptr);
9970+
ZVAL_UNDEF(varptr);
9971+
9972+
ZEND_VM_NEXT_OPCODE();
9973+
}
9974+
99429975
ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_SEND_VAL, op->op1_type == IS_CONST && op->op2_type == IS_UNUSED && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)), ZEND_SEND_VAL_SIMPLE, CONST, NUM)
99439976
{
99449977
USE_OPLINE

0 commit comments

Comments
 (0)