Skip to content

Commit bf8433c

Browse files
committed
Speed up call frame leaving by grouping refcounted CVs
Leaving the call frame must destroy all the CVs. It loops over all the vars and then uses i_zval_ptr_dtor on them. We can skip the non-refcounted CVs in bulk to speed up that loop.
1 parent c96900e commit bf8433c

14 files changed

Lines changed: 126 additions & 28 deletions

Zend/Optimizer/compact_vars.c

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,69 @@
1818
#include "zend_bitset.h"
1919
#include "zend_observer.h"
2020

21+
/* Types a CV may have, excluding array kinds. */
22+
#define CV_TYPE_KINDS (MAY_BE_UNDEF|MAY_BE_ANY|MAY_BE_REF)
23+
/* Type kinds that are not refcounted. */
24+
#define CV_TYPE_KINDS_NO_RC (MAY_BE_UNDEF|MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE)
25+
26+
/* Fills in `rc_cvs` with the CV numbers that may be refcounted, based on SSA info (if available). */
27+
static void compute_rc_cvs(
28+
const zend_op_array *op_array, const zend_ssa *ssa, zend_bitset rc_cvs)
29+
{
30+
if (!ssa || !ssa->var_info) {
31+
/* No type information: every CV has to be assumed refcounted. */
32+
zend_bitset_fill(rc_cvs, zend_bitset_len(op_array->last_var));
33+
return;
34+
}
35+
36+
zend_bitset_clear(rc_cvs, zend_bitset_len(op_array->last_var));
37+
38+
/* TODO: Argument slots right now have to be always destroyed because
39+
* when ZEND_RECV fails, they can be refcounted (e.g. passing an array to an int parameter).
40+
* This can be improved by making the ZEND_RECV op destroy the argument iself. */
41+
for (uint32_t i = 0; i < MIN((uint32_t) op_array->last_var, op_array->num_args); i++) {
42+
zend_bitset_incl(rc_cvs, i);
43+
}
44+
for (int i = 0; i < ssa->vars_count; i++) {
45+
int cv = ssa->vars[i].var;
46+
47+
if (cv < 0 || cv >= op_array->last_var || zend_bitset_in(rc_cvs, cv)) {
48+
continue;
49+
}
50+
if (ssa->var_info[i].type & CV_TYPE_KINDS & ~CV_TYPE_KINDS_NO_RC) {
51+
zend_bitset_incl(rc_cvs, cv);
52+
}
53+
}
54+
55+
/* ZEND_RETURN_BY_REF turns its op1 into a reference without being recorded as
56+
* such in the SSA. */
57+
/* TODO: should this be recorded by type inference and SSA construction via a new def? */
58+
if (op_array->fn_flags & ZEND_ACC_RETURN_REFERENCE) {
59+
for (uint32_t i = 0; i < op_array->last; i++) {
60+
const zend_op *opline = &op_array->opcodes[i];
61+
62+
if (opline->opcode == ZEND_RETURN_BY_REF && opline->op1_type == IS_CV) {
63+
zend_bitset_incl(rc_cvs, EX_VAR_TO_NUM(opline->op1.var));
64+
}
65+
}
66+
}
67+
}
68+
2169
/* This pass removes all CVs and temporaries that are completely unused. It does *not* merge any CVs or TMPs.
22-
* This pass does not operate on SSA form anymore. */
23-
void zend_optimizer_compact_vars(zend_op_array *op_array) {
70+
*
71+
* It also sorts the refcounted CVs so they appear at the front, to fill in `op_array->last_var_to_free`.
72+
* This improves call frame cleanup performance by skipping (in bulk) the CVs that are not refcounted. */
73+
void zend_optimizer_compact_vars(zend_op_array *op_array, const zend_ssa *ssa) {
2474
int i;
2575

2676
ALLOCA_FLAG(use_heap1);
2777
ALLOCA_FLAG(use_heap2);
78+
ALLOCA_FLAG(use_heap3);
2879
uint32_t used_vars_len = zend_bitset_len(op_array->last_var + op_array->T);
2980
zend_bitset used_vars = ZEND_BITSET_ALLOCA(used_vars_len, use_heap1);
3081
uint32_t *vars_map = do_alloca((op_array->last_var + op_array->T) * sizeof(uint32_t), use_heap2);
31-
uint32_t num_cvs, num_tmps;
82+
uint32_t rc_cvs_len = zend_bitset_len(op_array->last_var);
83+
zend_bitset rc_cvs = ZEND_BITSET_ALLOCA(rc_cvs_len, use_heap3);
3284

3385
/* Determine which CVs are used */
3486
zend_bitset_clear(used_vars, used_vars_len);
@@ -52,16 +104,36 @@ void zend_optimizer_compact_vars(zend_op_array *op_array) {
52104
}
53105
}
54106

55-
num_cvs = 0;
107+
compute_rc_cvs(op_array, ssa, rc_cvs);
108+
109+
uint32_t num_cvs = 0;
110+
uint32_t last_var_to_free = 0;
56111
for (i = 0; i < op_array->last_var; i++) {
57-
if (zend_bitset_in(used_vars, i)) {
58-
vars_map[i] = num_cvs++;
59-
} else {
112+
/* Parameters have a fixed position because other components depend on that ordering. */
113+
if (i < op_array->num_args) {
114+
ZEND_ASSERT(zend_bitset_in(used_vars, i)
115+
&& "A parameter CV is written by its RECV and cannot be unused");
116+
} else if (!zend_bitset_in(used_vars, i) || !zend_bitset_in(rc_cvs, i)) {
60117
vars_map[i] = (uint32_t) -1;
118+
continue;
119+
}
120+
vars_map[i] = num_cvs++;
121+
if (zend_bitset_in(rc_cvs, i)) {
122+
/* Parameters keep their slot even when they are not refcounted,
123+
* so take the highest refcounted slot rather than the group size. */
124+
last_var_to_free = vars_map[i] + 1;
125+
}
126+
}
127+
128+
/* The CVs that can never hold a refcounted value go last, after `last_var_to_free`. */
129+
for (i = op_array->num_args; i < op_array->last_var; i++) {
130+
if (vars_map[i] == (uint32_t) -1 && zend_bitset_in(used_vars, i)) {
131+
vars_map[i] = num_cvs++;
61132
}
62133
}
134+
ZEND_ASSERT(last_var_to_free <= num_cvs);
63135

64-
num_tmps = 0;
136+
uint32_t num_tmps = 0;
65137
for (i = op_array->last_var; i < op_array->last_var + op_array->T; i++) {
66138
if (zend_bitset_in(used_vars, i)) {
67139
vars_map[i] = num_cvs + num_tmps++;
@@ -71,9 +143,24 @@ void zend_optimizer_compact_vars(zend_op_array *op_array) {
71143
}
72144

73145
free_alloca(used_vars, use_heap1);
146+
free_alloca(rc_cvs, use_heap3);
147+
148+
op_array->last_var_to_free = last_var_to_free;
149+
150+
/* Nothing was removed and no CV moved: the rewrite below would be a no-op.
151+
* Check if anything got moved, as the number of vars could still be the same in that case. */
74152
if (num_cvs == op_array->last_var && num_tmps == op_array->T) {
75-
free_alloca(vars_map, use_heap2);
76-
return;
153+
bool identity = true;
154+
for (i = 0; i < op_array->last_var; i++) {
155+
if (vars_map[i] != (uint32_t) i) {
156+
identity = false;
157+
break;
158+
}
159+
}
160+
if (identity) {
161+
free_alloca(vars_map, use_heap2);
162+
return;
163+
}
77164
}
78165

79166
ZEND_ASSERT(num_cvs <= op_array->last_var);
@@ -93,8 +180,8 @@ void zend_optimizer_compact_vars(zend_op_array *op_array) {
93180
}
94181
}
95182

96-
/* Update CV name table */
97-
if (num_cvs != op_array->last_var) {
183+
/* Update CV name table, either because of CVs being removed or being moved. */
184+
{
98185
if (num_cvs) {
99186
zend_string **names = safe_emalloc(sizeof(zend_string *), num_cvs, 0);
100187
for (i = 0; i < op_array->last_var; i++) {

Zend/Optimizer/zend_dump.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,9 @@ ZEND_API void zend_dump_op_array(const zend_op_array *op_array, uint32_t dump_fl
968968
op_array->last,
969969
op_array->num_args);
970970
fprintf(stderr, ", vars=%d, tmps=%d", op_array->last_var, op_array->T);
971+
if (op_array->last_var_to_free != (uint32_t) op_array->last_var) {
972+
fprintf(stderr, ", vars_to_free=%d", op_array->last_var_to_free);
973+
}
971974
if (ssa) {
972975
fprintf(stderr, ", ssa_vars=%d", ssa->vars_count);
973976
}

Zend/Optimizer/zend_optimizer.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,7 +1159,9 @@ static void zend_optimize(zend_op_array *op_array,
11591159
if ((ZEND_OPTIMIZER_PASS_13 & ctx->optimization_level) &&
11601160
(!(ZEND_OPTIMIZER_PASS_6 & ctx->optimization_level) ||
11611161
!(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level))) {
1162-
zend_optimizer_compact_vars(op_array);
1162+
/* Reached only when the DFA pass did not run, so there is no type
1163+
* information to decide which CVs need to be destroyed. */
1164+
zend_optimizer_compact_vars(op_array, NULL);
11631165
if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_13) {
11641166
zend_dump_op_array(op_array, 0, "after pass 13", NULL);
11651167
}
@@ -1685,7 +1687,9 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
16851687

16861688
if (ZEND_OPTIMIZER_PASS_13 & optimization_level) {
16871689
for (i = 0; i < call_graph.op_arrays_count; i++) {
1688-
zend_optimizer_compact_vars(call_graph.op_arrays[i]);
1690+
func_info = ZEND_FUNC_INFO(call_graph.op_arrays[i]);
1691+
zend_optimizer_compact_vars(call_graph.op_arrays[i],
1692+
func_info ? &func_info->ssa : NULL);
16891693
if (debug_level & ZEND_DUMP_AFTER_PASS_13) {
16901694
zend_dump_op_array(call_graph.op_arrays[i], 0, "after pass 13", NULL);
16911695
}

Zend/Optimizer/zend_optimizer_internal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx
116116
void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx);
117117
void zend_optimizer_nop_removal(zend_op_array *op_array, zend_optimizer_ctx *ctx);
118118
void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx *ctx);
119-
void zend_optimizer_compact_vars(zend_op_array *op_array);
119+
void zend_optimizer_compact_vars(zend_op_array *op_array, const zend_ssa *ssa);
120120
zend_function *zend_optimizer_get_called_func(
121121
const zend_script *script, const zend_op_array *op_array, zend_op *opline, bool *is_prototype);
122122
uint32_t zend_optimizer_classify_function(const zend_string *name, uint32_t num_args);

Zend/zend_compile.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ struct _zend_op_array {
552552
uint32_t cache_size; /* number of run_time_cache_slots * sizeof(void*) */
553553
int last_var; /* number of CV variables */
554554
uint32_t last; /* number of opcodes */
555+
/* Number of leading CV slots that have to be destroyed when the call frame is left. */
556+
uint32_t last_var_to_free;
555557

556558
zend_op *opcodes;
557559
ZEND_MAP_PTR_DEF(HashTable *, static_variables_ptr);

Zend/zend_execute.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4298,7 +4298,7 @@ ZEND_API void zend_clean_and_cache_symbol_table(zend_array *symbol_table) /* {{{
42984298
static zend_always_inline void i_free_compiled_variables(zend_execute_data *execute_data) /* {{{ */
42994299
{
43004300
zval *cv = EX_VAR_NUM(0);
4301-
int count = EX(func)->op_array.last_var;
4301+
uint32_t count = EX(func)->op_array.last_var_to_free;
43024302
while (EXPECTED(count != 0)) {
43034303
i_zval_ptr_dtor(cv);
43044304
cv++;

Zend/zend_object_handlers.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,7 @@ ZEND_API ZEND_ATTRIBUTE_NONNULL zend_function *zend_get_call_trampoline_func(
18271827
* value so that it doesn't contain garbage when the engine allocates space for the next stack
18281828
* frame. This didn't cause any issues until now due to "lucky" structure layout. */
18291829
func->last_var = 0;
1830+
func->last_var_to_free = 0;
18301831
uint32_t min_T = 2 + ZEND_OBSERVER_ENABLED;
18311832
func->T = (fbc->type == ZEND_USER_FUNCTION)? MAX(fbc->op_array.last_var + fbc->op_array.T, min_T) : min_T;
18321833
func->filename = (fbc->type == ZEND_USER_FUNCTION)? fbc->op_array.filename : ZSTR_EMPTY_ALLOC();

Zend/zend_opcode.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void init_op_array(zend_op_array *op_array, zend_function_type type, int initial
5757
op_array->opcodes = emalloc(initial_ops_size * sizeof(zend_op));
5858

5959
op_array->last_var = 0;
60+
op_array->last_var_to_free = 0;
6061
op_array->vars = NULL;
6162

6263
op_array->T = 0;
@@ -1120,6 +1121,9 @@ ZEND_API void pass_two(zend_op_array *op_array)
11201121
CG(context).vars_size = op_array->last_var;
11211122
}
11221123

1124+
/* Without type inference every CV has to be assumed refcounted; this can be lowered via the optimizer. */
1125+
op_array->last_var_to_free = op_array->last_var;
1126+
11231127
#if ZEND_USE_ABS_CONST_ADDR
11241128
if (CG(context).opcodes_size != op_array->last) {
11251129
op_array->opcodes = (zend_op *) erealloc(op_array->opcodes, sizeof(zend_op)*op_array->last);

ext/opcache/jit/zend_jit.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2930,17 +2930,15 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
29302930
zend_jit_common_return(jit);
29312931

29322932
bool left_frame = false;
2933-
if (op_array->last_var > 100) {
2933+
if (op_array->last_var_to_free > 100) {
29342934
/* To many CVs to unroll */
29352935
if (!zend_jit_free_cvs(&ctx)) {
29362936
goto jit_failure;
29372937
}
29382938
left_frame = true;
29392939
}
29402940
if (!left_frame) {
2941-
int j;
2942-
2943-
for (j = 0 ; j < op_array->last_var; j++) {
2941+
for (uint32_t j = 0; j < op_array->last_var_to_free; j++) {
29442942
uint32_t info = zend_ssa_cv_info(op_array, ssa, j);
29452943

29462944
if (info & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) {

ext/opcache/jit/zend_jit_trace.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5597,23 +5597,22 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer,
55975597
goto jit_failure;
55985598
}
55995599
} else {
5600-
int j;
56015600
int may_throw = 0;
56025601
bool left_frame = 0;
56035602

56045603
if (!zend_jit_return(&ctx, opline, op_array,
56055604
op1_info, OP1_REG_ADDR())) {
56065605
goto jit_failure;
56075606
}
5608-
if (op_array->last_var > 100) {
5607+
if (op_array->last_var_to_free > 100) {
56095608
/* To many CVs to unroll */
56105609
if (!zend_jit_free_cvs(&ctx)) {
56115610
goto jit_failure;
56125611
}
56135612
left_frame = 1;
56145613
}
56155614
if (!left_frame) {
5616-
for (j = 0 ; j < op_array->last_var; j++) {
5615+
for (uint32_t j = 0; j < op_array->last_var_to_free; j++) {
56175616
uint32_t info;
56185617
uint8_t type;
56195618

0 commit comments

Comments
 (0)