Skip to content

Commit 6a2c531

Browse files
authored
Optimize observers (#13649)
Inline the lookup whether a function is observed at all. This strategy is also used for FRAMELESS calls. If the frameless call is observed, we instead allocate a call frame and push the arguments, to call the the function afterwards. Doing so is still a performance benefit as opposed to executing individual INIT_FCALL+SEND_VAL ops. Thus, even if the frameless call turns out to be observed, the call overhead is slightly lower than before. If the internal function is not observed at all, the unavoidable overhead is fetching the FLF zend_function pointer and the run-time cache needs to be inspected. As part of this work, it turned out to be most viable to put the result operand on the ZEND_OP_DATA instead of ZEND_FRAMELESS_ICALL_3, allowing seamless interoperability with the DO_ICALL opcode. This is a bit unusual in comparison to all other ZEND_OP_DATA usages, but seems to not pose problems overall. There is also a small issue resolved: trampolines would always use the ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER function due to zend_observer_fcall_op_array_extension being set to -1 too late.
1 parent 6e825df commit 6a2c531

16 files changed

+1230
-706
lines changed

Zend/zend_builtin_functions.c

+10-2
Original file line numberDiff line numberDiff line change
@@ -1776,7 +1776,7 @@ ZEND_FUNCTION(debug_print_backtrace)
17761776

17771777
ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int options, int limit) /* {{{ */
17781778
{
1779-
zend_execute_data *call;
1779+
zend_execute_data *call, *last_call = NULL;
17801780
zend_object *object;
17811781
bool fake_frame = 0;
17821782
int lineno, frameno = 0;
@@ -1821,6 +1821,7 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int
18211821

18221822
if (skip_last) {
18231823
/* skip debug_backtrace() */
1824+
last_call = call;
18241825
call = call->prev_execute_data;
18251826
}
18261827

@@ -1857,9 +1858,15 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int
18571858
zval *arg = zend_get_zval_ptr(op_data, op_data->op1_type, &op_data->op1, call);
18581859
if (Z_TYPE_P(arg) == IS_UNDEF) goto not_frameless_call;
18591860
}
1861+
zend_function *func = ZEND_FLF_FUNC(opline);
1862+
/* Assume frameless functions are not recursive with themselves.
1863+
* This condition may be true when observers are enabled:
1864+
* Observers will put a call frame on top of the frameless opcode. */
1865+
if (last_call && last_call->func == func) {
1866+
goto not_frameless_call;
1867+
}
18601868
stack_frame = zend_new_array(8);
18611869
zend_hash_real_init_mixed(stack_frame);
1862-
zend_function *func = ZEND_FLF_FUNC(opline);
18631870
zend_string *name = func->common.function_name;
18641871
ZVAL_STRINGL(&tmp, ZSTR_VAL(name), ZSTR_LEN(name));
18651872
_zend_hash_append_ex(stack_frame, ZSTR_KNOWN(ZEND_STR_FUNCTION), &tmp, 1);
@@ -2068,6 +2075,7 @@ ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int
20682075
} else {
20692076
fake_frame = 0;
20702077
include_filename = filename;
2078+
last_call = call;
20712079
call = prev;
20722080
}
20732081
}

Zend/zend_compile.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -4575,7 +4575,7 @@ static uint32_t find_frameless_function_offset(uint32_t arity, void *handler)
45754575

45764576
static const zend_frameless_function_info *find_frameless_function_info(zend_ast_list *args, zend_function *fbc, uint32_t type)
45774577
{
4578-
if (ZEND_OBSERVER_ENABLED || zend_execute_internal) {
4578+
if (zend_execute_internal) {
45794579
return NULL;
45804580
}
45814581

Zend/zend_execute.c

+48
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,54 @@ static zend_never_inline void zend_assign_to_object_dim(zend_object *obj, zval *
15361536
}
15371537
}
15381538

1539+
static void frameless_observed_call_copy(zend_execute_data *call, uint32_t arg, zval *zv)
1540+
{
1541+
if (Z_ISUNDEF_P(zv)) {
1542+
ZVAL_NULL(ZEND_CALL_VAR_NUM(call, arg));
1543+
} else {
1544+
ZVAL_COPY_DEREF(ZEND_CALL_VAR_NUM(call, arg), zv);
1545+
}
1546+
}
1547+
1548+
ZEND_API void zend_frameless_observed_call(zend_execute_data *execute_data)
1549+
{
1550+
const zend_op *opline = EX(opline);
1551+
uint8_t num_args = ZEND_FLF_NUM_ARGS(opline->opcode);
1552+
zend_function *fbc = ZEND_FLF_FUNC(opline);
1553+
zval *result = EX_VAR(opline->result.var);
1554+
1555+
zend_execute_data *call = zend_vm_stack_push_call_frame_ex(zend_vm_calc_used_stack(num_args, fbc), ZEND_CALL_NESTED_FUNCTION, fbc, num_args, NULL);
1556+
call->prev_execute_data = execute_data;
1557+
1558+
switch (num_args) {
1559+
case 3: frameless_observed_call_copy(call, 2, zend_get_zval_ptr(opline+1, (opline+1)->op1_type, &(opline+1)->op1, execute_data)); ZEND_FALLTHROUGH;
1560+
case 2: frameless_observed_call_copy(call, 1, zend_get_zval_ptr(opline, opline->op2_type, &opline->op2, execute_data)); ZEND_FALLTHROUGH;
1561+
case 1: frameless_observed_call_copy(call, 0, zend_get_zval_ptr(opline, opline->op1_type, &opline->op1, execute_data));
1562+
}
1563+
1564+
EG(current_execute_data) = call;
1565+
1566+
zend_observer_fcall_begin_prechecked(call, ZEND_OBSERVER_DATA(fbc));
1567+
fbc->internal_function.handler(call, result);
1568+
zend_observer_fcall_end(call, result);
1569+
1570+
EG(current_execute_data) = execute_data;
1571+
1572+
if (UNEXPECTED(EG(exception) != NULL)) {
1573+
zend_rethrow_exception(execute_data);
1574+
}
1575+
1576+
zend_vm_stack_free_args(call);
1577+
1578+
uint32_t call_info = ZEND_CALL_INFO(call);
1579+
if (UNEXPECTED(call_info & ZEND_CALL_ALLOCATED)) {
1580+
zend_vm_stack_free_call_frame_ex(call_info, call);
1581+
} else {
1582+
EG(vm_stack_top) = (zval*)call;
1583+
}
1584+
}
1585+
1586+
15391587
static zend_always_inline int zend_binary_op(zval *ret, zval *op1, zval *op2 OPLINE_DC)
15401588
{
15411589
static const binary_op_type zend_binary_ops[] = {

Zend/zend_execute.h

+2
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@ ZEND_API void zend_cleanup_unfinished_execution(zend_execute_data *execute_data,
430430
ZEND_API ZEND_ATTRIBUTE_DEPRECATED HashTable *zend_unfinished_execution_gc(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer);
431431
ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer, bool suspended_by_yield);
432432

433+
ZEND_API void zend_frameless_observed_call(zend_execute_data *execute_data);
434+
433435
zval * ZEND_FASTCALL zend_handle_named_arg(
434436
zend_execute_data **call_ptr, zend_string *arg_name,
435437
uint32_t *arg_num_ptr, void **cache_slot);

Zend/zend_frameless_function.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ typedef void (*zend_frameless_function_3)(zval *return_value, zval *op1, zval *o
115115
extern size_t zend_flf_count;
116116
extern size_t zend_flf_capacity;
117117
ZEND_API extern void **zend_flf_handlers;
118-
extern zend_function **zend_flf_functions;
118+
ZEND_API extern zend_function **zend_flf_functions;
119119

120120
typedef struct {
121121
void *handler;

Zend/zend_globals.h

+2
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ struct _zend_executor_globals {
194194

195195
uint32_t jit_trace_num; /* Used by tracing JIT to reference the currently running trace */
196196

197+
zend_execute_data *current_observed_frame;
198+
197199
int ticks_count;
198200

199201
zend_long precision;

Zend/zend_observer.c

+62-43
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
+----------------------------------------------------------------------+
1515
| Authors: Levi Morrison <[email protected]> |
1616
| Sammy Kaye Powers <[email protected]> |
17+
| Bob Weinand <[email protected]> |
1718
+----------------------------------------------------------------------+
1819
*/
1920

@@ -23,15 +24,8 @@
2324
#include "zend_llist.h"
2425
#include "zend_vm.h"
2526

26-
#define ZEND_OBSERVER_DATA(function) \
27-
ZEND_OP_ARRAY_EXTENSION((&(function)->common), ZEND_USER_CODE((function)->type) \
28-
? zend_observer_fcall_op_array_extension : zend_observer_fcall_internal_function_extension)
29-
3027
#define ZEND_OBSERVER_NOT_OBSERVED ((void *) 2)
3128

32-
#define ZEND_OBSERVABLE_FN(function) \
33-
(ZEND_MAP_PTR(function->common.run_time_cache) && !(function->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE))
34-
3529
static zend_llist zend_observers_fcall_list;
3630
static zend_llist zend_observer_function_declared_callbacks;
3731
static zend_llist zend_observer_class_linked_callbacks;
@@ -46,8 +40,6 @@ bool zend_observer_errors_observed;
4640
bool zend_observer_function_declared_observed;
4741
bool zend_observer_class_linked_observed;
4842

49-
ZEND_TLS zend_execute_data *current_observed_frame;
50-
5143
// Call during minit/startup ONLY
5244
ZEND_API void zend_observer_fcall_register(zend_observer_fcall_init init)
5345
{
@@ -107,7 +99,7 @@ ZEND_API void zend_observer_post_startup(void)
10799

108100
ZEND_API void zend_observer_activate(void)
109101
{
110-
current_observed_frame = NULL;
102+
EG(current_observed_frame) = NULL;
111103
}
112104

113105
ZEND_API void zend_observer_shutdown(void)
@@ -127,21 +119,24 @@ static void zend_observer_fcall_install(zend_execute_data *execute_data)
127119
zend_function *function = execute_data->func;
128120

129121
ZEND_ASSERT(RUN_TIME_CACHE(&function->common));
130-
zend_observer_fcall_begin_handler *begin_handlers = (zend_observer_fcall_begin_handler *)&ZEND_OBSERVER_DATA(function);
122+
zend_observer_fcall_begin_handler *begin_handlers = ZEND_OBSERVER_DATA(function), *begin_handlers_start = begin_handlers;
131123
zend_observer_fcall_end_handler *end_handlers = (zend_observer_fcall_end_handler *)begin_handlers + list->count, *end_handlers_start = end_handlers;
132124

133125
*begin_handlers = ZEND_OBSERVER_NOT_OBSERVED;
134126
*end_handlers = ZEND_OBSERVER_NOT_OBSERVED;
127+
bool has_handlers = false;
135128

136129
for (zend_llist_element *element = list->head; element; element = element->next) {
137130
zend_observer_fcall_init init;
138131
memcpy(&init, element->data, sizeof init);
139132
zend_observer_fcall_handlers handlers = init(execute_data);
140133
if (handlers.begin) {
141134
*(begin_handlers++) = handlers.begin;
135+
has_handlers = true;
142136
}
143137
if (handlers.end) {
144138
*(end_handlers++) = handlers.end;
139+
has_handlers = true;
145140
}
146141
}
147142

@@ -151,6 +146,10 @@ static void zend_observer_fcall_install(zend_execute_data *execute_data)
151146
*end_handlers = *end_handlers_start;
152147
*end_handlers_start = tmp;
153148
}
149+
150+
if (!has_handlers) {
151+
*begin_handlers_start = ZEND_OBSERVER_NONE_OBSERVED;
152+
}
154153
}
155154

156155
/* We need to provide the ability to retrieve the handler which will move onto the position the current handler was.
@@ -182,8 +181,8 @@ static bool zend_observer_remove_handler(void **first_handler, void *old_handler
182181

183182
ZEND_API void zend_observer_add_begin_handler(zend_function *function, zend_observer_fcall_begin_handler begin) {
184183
size_t registered_observers = zend_observers_fcall_list.count;
185-
zend_observer_fcall_begin_handler *first_handler = (void *)&ZEND_OBSERVER_DATA(function), *last_handler = first_handler + registered_observers - 1;
186-
if (*first_handler == ZEND_OBSERVER_NOT_OBSERVED) {
184+
zend_observer_fcall_begin_handler *first_handler = ZEND_OBSERVER_DATA(function), *last_handler = first_handler + registered_observers - 1;
185+
if (*first_handler == ZEND_OBSERVER_NOT_OBSERVED || *first_handler == ZEND_OBSERVER_NONE_OBSERVED) {
187186
*first_handler = begin;
188187
} else {
189188
for (zend_observer_fcall_begin_handler *cur_handler = first_handler + 1; cur_handler <= last_handler; ++cur_handler) {
@@ -198,24 +197,47 @@ ZEND_API void zend_observer_add_begin_handler(zend_function *function, zend_obse
198197
}
199198

200199
ZEND_API bool zend_observer_remove_begin_handler(zend_function *function, zend_observer_fcall_begin_handler begin, zend_observer_fcall_begin_handler *next) {
201-
return zend_observer_remove_handler((void **)&ZEND_OBSERVER_DATA(function), begin, (void **)next);
200+
void **begin_handlers = (void **)ZEND_OBSERVER_DATA(function);
201+
if (zend_observer_remove_handler(begin_handlers, begin, (void**)next)) {
202+
// Ensure invariant: ZEND_OBSERVER_NONE_OBSERVED in begin_handlers if both are not observed
203+
if (*begin_handlers == ZEND_OBSERVER_NOT_OBSERVED) {
204+
size_t registered_observers = zend_observers_fcall_list.count;
205+
if (begin_handlers[registered_observers] /* first end handler */ == ZEND_OBSERVER_NOT_OBSERVED) {
206+
*begin_handlers = ZEND_OBSERVER_NONE_OBSERVED;
207+
}
208+
}
209+
return true;
210+
}
211+
return false;
202212
}
203213

204214
ZEND_API void zend_observer_add_end_handler(zend_function *function, zend_observer_fcall_end_handler end) {
205215
size_t registered_observers = zend_observers_fcall_list.count;
206-
zend_observer_fcall_end_handler *end_handler = (zend_observer_fcall_end_handler *)&ZEND_OBSERVER_DATA(function) + registered_observers;
216+
void **begin_handler = (void **)ZEND_OBSERVER_DATA(function);
217+
zend_observer_fcall_end_handler *end_handler = (zend_observer_fcall_end_handler *)begin_handler + registered_observers;
207218
// to allow to preserve the invariant that end handlers are in reverse order of begin handlers, push the new end handler in front
208219
if (*end_handler != ZEND_OBSERVER_NOT_OBSERVED) {
209220
// there's no space for new handlers, then it's forbidden to call this function
210221
ZEND_ASSERT(end_handler[registered_observers - 1] == NULL);
211222
memmove(end_handler + 1, end_handler, sizeof(end_handler) * (registered_observers - 1));
223+
} else if (*begin_handler == ZEND_OBSERVER_NONE_OBSERVED) {
224+
*begin_handler = ZEND_OBSERVER_NOT_OBSERVED;
212225
}
213226
*end_handler = end;
214227
}
215228

216229
ZEND_API bool zend_observer_remove_end_handler(zend_function *function, zend_observer_fcall_end_handler end, zend_observer_fcall_end_handler *next) {
217230
size_t registered_observers = zend_observers_fcall_list.count;
218-
return zend_observer_remove_handler((void **)&ZEND_OBSERVER_DATA(function) + registered_observers, end, (void **)next);
231+
void **begin_handlers = (void **)ZEND_OBSERVER_DATA(function);
232+
void **end_handlers = begin_handlers + registered_observers;
233+
if (zend_observer_remove_handler(end_handlers, end, (void**)next)) {
234+
// Ensure invariant: ZEND_OBSERVER_NONE_OBSERVED in begin_handlers if both are not observed
235+
if (*begin_handlers == ZEND_OBSERVER_NOT_OBSERVED && *end_handlers == ZEND_OBSERVER_NOT_OBSERVED) {
236+
*begin_handlers = ZEND_OBSERVER_NONE_OBSERVED;
237+
}
238+
return true;
239+
}
240+
return false;
219241
}
220242

221243
static inline zend_execute_data **prev_observed_frame(zend_execute_data *execute_data) {
@@ -224,33 +246,33 @@ static inline zend_execute_data **prev_observed_frame(zend_execute_data *execute
224246
return (zend_execute_data **)&Z_PTR_P(EX_VAR_NUM((ZEND_USER_CODE(func->type) ? func->op_array.last_var : ZEND_CALL_NUM_ARGS(execute_data)) + func->common.T - 1));
225247
}
226248

227-
static void ZEND_FASTCALL _zend_observe_fcall_begin(zend_execute_data *execute_data)
228-
{
249+
static void ZEND_FASTCALL _zend_observe_fcall_begin(zend_execute_data *execute_data) {
229250
if (!ZEND_OBSERVER_ENABLED) {
230251
return;
231252
}
232253

233-
zend_function *function = execute_data->func;
254+
zend_observer_fcall_begin_specialized(execute_data, true);
255+
}
234256

235-
if (!ZEND_OBSERVABLE_FN(function)) {
236-
return;
237-
}
257+
ZEND_API void ZEND_FASTCALL zend_observer_fcall_begin_prechecked(zend_execute_data *execute_data, zend_observer_fcall_begin_handler *handler)
258+
{
259+
zend_observer_fcall_begin_handler *possible_handlers_end = handler + zend_observers_fcall_list.count;
238260

239-
zend_observer_fcall_begin_handler *handler = (zend_observer_fcall_begin_handler *)&ZEND_OBSERVER_DATA(function);
240261
if (!*handler) {
241262
zend_observer_fcall_install(execute_data);
263+
if (zend_observer_handler_is_unobserved(handler)) {
264+
return;
265+
}
242266
}
243267

244-
zend_observer_fcall_begin_handler *possible_handlers_end = handler + zend_observers_fcall_list.count;
245-
246268
zend_observer_fcall_end_handler *end_handler = (zend_observer_fcall_end_handler *)possible_handlers_end;
247269
if (*end_handler != ZEND_OBSERVER_NOT_OBSERVED) {
248-
*prev_observed_frame(execute_data) = current_observed_frame;
249-
current_observed_frame = execute_data;
250-
}
270+
*prev_observed_frame(execute_data) = EG(current_observed_frame);
271+
EG(current_observed_frame) = execute_data;
251272

252-
if (*handler == ZEND_OBSERVER_NOT_OBSERVED) {
253-
return;
273+
if (*handler == ZEND_OBSERVER_NOT_OBSERVED) { // this function must not be called if ZEND_OBSERVER_NONE_OBSERVED, hence sufficient to check
274+
return;
275+
}
254276
}
255277

256278
do {
@@ -265,17 +287,17 @@ ZEND_API void ZEND_FASTCALL zend_observer_generator_resume(zend_execute_data *ex
265287

266288
ZEND_API void ZEND_FASTCALL zend_observer_fcall_begin(zend_execute_data *execute_data)
267289
{
268-
ZEND_ASSUME(execute_data->func);
269-
if (!(execute_data->func->common.fn_flags & ZEND_ACC_GENERATOR)) {
290+
ZEND_ASSUME(EX(func));
291+
if (!(EX(func)->common.fn_flags & ZEND_ACC_GENERATOR)) {
270292
_zend_observe_fcall_begin(execute_data);
271293
}
272294
}
273295

274296
static inline void call_end_observers(zend_execute_data *execute_data, zval *return_value) {
275-
zend_function *func = execute_data->func;
297+
zend_function *func = EX(func);
276298
ZEND_ASSERT(func);
277299

278-
zend_observer_fcall_end_handler *handler = (zend_observer_fcall_end_handler *)&ZEND_OBSERVER_DATA(func) + zend_observers_fcall_list.count;
300+
zend_observer_fcall_end_handler *handler = (zend_observer_fcall_end_handler *)ZEND_OBSERVER_DATA(func) + zend_observers_fcall_list.count;
279301
// TODO: Fix exceptions from generators
280302
// ZEND_ASSERT(fcall_data);
281303
if (!*handler || *handler == ZEND_OBSERVER_NOT_OBSERVED) {
@@ -288,19 +310,16 @@ static inline void call_end_observers(zend_execute_data *execute_data, zval *ret
288310
} while (++handler != possible_handlers_end && *handler != NULL);
289311
}
290312

291-
ZEND_API void ZEND_FASTCALL zend_observer_fcall_end(zend_execute_data *execute_data, zval *return_value)
313+
ZEND_API void ZEND_FASTCALL zend_observer_fcall_end_prechecked(zend_execute_data *execute_data, zval *return_value)
292314
{
293-
if (execute_data != current_observed_frame) {
294-
return;
295-
}
296315
call_end_observers(execute_data, return_value);
297-
current_observed_frame = *prev_observed_frame(execute_data);
316+
EG(current_observed_frame) = *prev_observed_frame(execute_data);
298317
}
299318

300319
ZEND_API void zend_observer_fcall_end_all(void)
301320
{
302-
zend_execute_data *execute_data = current_observed_frame, *original_execute_data = EG(current_execute_data);
303-
current_observed_frame = NULL;
321+
zend_execute_data *execute_data = EG(current_observed_frame), *original_execute_data = EG(current_execute_data);
322+
EG(current_observed_frame) = NULL;
304323
while (execute_data) {
305324
EG(current_execute_data) = execute_data;
306325
call_end_observers(execute_data, NULL);
@@ -401,8 +420,8 @@ ZEND_API void ZEND_FASTCALL zend_observer_fiber_switch_notify(zend_fiber_context
401420
callback(from, to);
402421
}
403422

404-
from->top_observed_frame = current_observed_frame;
405-
current_observed_frame = to->top_observed_frame;
423+
from->top_observed_frame = EG(current_observed_frame);
424+
EG(current_observed_frame) = to->top_observed_frame;
406425
}
407426

408427
ZEND_API void ZEND_FASTCALL zend_observer_fiber_destroy_notify(zend_fiber_context *destroying)

0 commit comments

Comments
 (0)