When opcache is enabled, error handling is altered in the following ways:
* Errors emitted during compilation bypass the user-defined error handler
* Exceptions emitted during class linking are turned into fatal errors
Changes here make the behavior consistent regardless of opcache being enabled or
not:
* Errors emitted during compilation and class linking are always delayed and
handled after compilation or class linking. During handling, user-defined
error handlers are not bypassed. Fatal errors emitted during compilation or
class linking cause any delayed errors to be handled immediately (without
calling user-defined error handlers, as it would be unsafe).
* Exceptions thrown by user-defined error handlers when handling class linking
error are not promoted to fatal errors anymore and do not prevent linking.
Fixes GH-17422.
Closes GH-18541.
Closes GH-17627.
Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
This reduces the chances of confusion between opcode handlers used by the
VM, and opcode handler functions used for tracing or debugging. Depending
on the VM, zend_vm_opcode_handler_t may not be a function. For instance in
the HYBRID VM this is a label pointer.
Closes GH-19006
* PHP-8.4:
Update NEWS for GH-19068
ext/gd: Drop useless and doubtful MSVC specific code (libgd/libgd@f1480ab)
Zend: fix undefined symbol 'execute_ex' on Windows ARM64 #19064; ext/gd: fix emmintrin.h not found on Windows ARM64
Registering the constant may happen under another name due to
lowercasing. This will cause the lookup to the constant to fail.
Instead of looking it up, just change the Zend API to return a pointer
instead.
Checking the non-exception path without arguments first, this avoids a redundant check in the case without arguments. The exception path may become more expensive, but we don't optimize for exception flow, rather we optimize for the happy flow. The other paths are unaffected.
Add recursion protection when emitting deprecation warnings for class
constants, since the deprecation message can come from an attribute that is
using the same constant for the message, or otherwise result in recursion.
But, internal constants are persisted, and thus cannot have recursion
protection. Otherwise, if a user error handler triggers bailout before the
recursion flag is removed then a subsequent request (e.g. with `--repeat 2`)
would start with that flag already applied. Internal constants can presumably
be trusted not to use deprecation messages that come from recursive attributes.
Fixes GH-18463
Fixes GH-17711
This causes problems if an allocation profiler decides to walk the
stack, or if the engine itself OOMs on this opcode, and it tries to
print file and line information.
* zend_vm: Add OPcode specialization for `=== []`
Checking whether an array is empty with a strict comparison against the empty
array is a common pattern in PHP. A GitHub search for `"=== []" language:PHP`
reveals 44k hits. From the set of `!$a`, `count($a) === 0`, `empty($a)` and
`$a === []` it however is also the slowest option.
A test script:
<?php
$variable = array_fill(0, 10, random_int(1, 2));
$f = true;
for ($i = 0; $i < 50_000_000; $i++) {
$isEmpty = $variable === [];
$f = $f && $isEmpty;
}
var_dump($f);
with the `$isEmpty = …;` statement appropriately replaced results in:
Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
Time (mean ± σ): 467.6 ms ± 2.3 ms [User: 463.3 ms, System: 3.4 ms]
Range (min … max): 464.6 ms … 473.4 ms 10 runs
Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
Time (mean ± σ): 305.3 ms ± 0.3 ms [User: 302.0 ms, System: 3.1 ms]
Range (min … max): 304.9 ms … 305.7 ms 10 runs
Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
Time (mean ± σ): 630.3 ms ± 3.9 ms [User: 624.8 ms, System: 3.8 ms]
Range (min … max): 627.4 ms … 637.6 ms 10 runs
Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
Time (mean ± σ): 311.8 ms ± 3.4 ms [User: 307.9 ms, System: 3.6 ms]
Range (min … max): 308.7 ms … 320.7 ms 10 runs
Summary
sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php ran
1.02 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
1.53 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
2.06 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
This patch adds another OPcode specialization for `ZEND_IS_IDENTICAL` that
specifically matches a comparison against the empty array. With this
specialization the `=== []` check becomes the fastest of them all, which is not
surprising given how specific it is:
Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
Time (mean ± σ): 384.1 ms ± 2.3 ms [User: 379.3 ms, System: 3.8 ms]
Range (min … max): 382.2 ms … 389.8 ms 10 runs
Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
Time (mean ± σ): 305.8 ms ± 3.2 ms [User: 301.7 ms, System: 3.8 ms]
Range (min … max): 304.4 ms … 314.9 ms 10 runs
Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.
Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
Time (mean ± σ): 293.9 ms ± 2.9 ms [User: 289.7 ms, System: 3.3 ms]
Range (min … max): 291.5 ms … 299.4 ms 10 runs
Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
Time (mean ± σ): 306.8 ms ± 0.4 ms [User: 303.8 ms, System: 2.7 ms]
Range (min … max): 306.3 ms … 307.3 ms 10 runs
Summary
sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php ran
1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
1.31 ± 0.02 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
As a follow-up optimization it might be possible to transform the other
emptiness checks, such as `count($arr) === 0` into `$arr === []` if `$arr` is
known to be `MAY_BE_ARRAY` only.
* zend_vm: Add OPcode specialization for `!== []`
* UPGRADING
On win64, xmm6-xmm15 are preserved registers, but the prologues and
epilogues of JITted code don't handle these. The issue occurs when
calling into the JIT code again via an internal handler
(like call_user_func). Therefore, we want to save/restore xmm registers
upon entering/leaving execute_ex. Since MSVC x64 does not support inline
assembly, we create an assembly wrapper around the real execute_ex
function.
The alternative is to always save/restore these xmm registers into the
fixed call frame, but this causes unnecessary overhead.
The same issue occurs for ARM64 platforms for floating point register
8 to 15. However, there we can use inline asm to fix this.
Closes GH-18352.
This changes the signature of opcode handlers in the CALL VM so that the opline
is passed directly via arguments. This reduces the number of memory operations
on EX(opline), and makes the CALL VM considerably faster.
Additionally, this unifies the CALL and HYBRID VMs a bit, as EX(opline) is now
handled in the same way in both VMs.
This is a part of GH-17849.
Currently we have two VMs:
* HYBRID: Used when compiling with GCC. execute_data and opline are global
register variables
* CALL: Used when compiling with something else. execute_data is passed as
opcode handler arg, but opline is passed via execute_data->opline
(EX(opline)).
The Call VM looks like this:
while (1) {
ret = execute_data->opline->handler(execute_data);
if (UNEXPECTED(ret != 0)) {
if (ret > 0) { // returned by ZEND_VM_ENTER() / ZEND_VM_LEAVE()
execute_data = EG(current_execute_data);
} else { // returned by ZEND_VM_RETURN()
return;
}
}
}
// example op handler
int ZEND_INIT_FCALL_SPEC_CONST_HANDLER(zend_execute_data *execute_data) {
// load opline
const zend_op *opline = execute_data->opline;
// instruction execution
// dispatch
// ZEND_VM_NEXT_OPCODE():
execute_data->opline++;
return 0; // ZEND_VM_CONTINUE()
}
Opcode handlers return a positive value to signal that the loop must load a
new execute_data from EG(current_execute_data), typically when entering
or leaving a function.
Here I make the following changes:
* Pass opline as opcode handler argument
* Return next opline from opcode handlers
* ZEND_VM_ENTER / ZEND_VM_LEAVE return opline|(1<<0) to signal that
execute_data must be reloaded from EG(current_execute_data)
This gives us:
while (1) {
opline = opline->handler(execute_data, opline);
if (UNEXPECTED((uintptr_t) opline & ZEND_VM_ENTER_BIT) {
opline = opline & ~ZEND_VM_ENTER_BIT;
if (opline != 0) { // ZEND_VM_ENTER() / ZEND_VM_LEAVE()
execute_data = EG(current_execute_data);
} else { // ZEND_VM_RETURN()
return;
}
}
}
// example op handler
const zend_op * ZEND_INIT_FCALL_SPEC_CONST_HANDLER(zend_execute_data *execute_data, const zend_op *opline) {
// opline already loaded
// instruction execution
// dispatch
// ZEND_VM_NEXT_OPCODE():
return ++opline;
}
bench.php is 23% faster on Linux / x86_64, 18% faster on MacOS / M1.
Symfony Demo is 2.8% faster.
When using the HYBRID VM, JIT'ed code stores execute_data/opline in two fixed
callee-saved registers and rarely touches EX(opline), just like the VM.
Since the registers are callee-saved, the JIT'ed code doesn't have to
save them before calling other functions, and can assume they always
contain execute_data/opline. The code also avoids saving/restoring them in
prologue/epilogue, as execute_ex takes care of that (JIT'ed code is called
exclusively from there).
The CALL VM can now use a fixed register for execute_data/opline as well, but
we can't rely on execute_ex to save the registers for us as it may use these
registers itself. So we have to save/restore the two registers in JIT'ed code
prologue/epilogue.
Closes GH-17952
https://wiki.php.net/rfc/static-aviz
Optimally, this would be moved to zend_fetch_static_property_address(). However,
this isn't currently effective for opcache, because R and RW/W/UNSET cache slots
are merged. This will circumvent the visibility check if the cache is primed by
a R instruction.
Closes GH-16486
zend_is_callable_ex() can unfortunately emit a deprecation, and then
a user error handler can throw an exception. This causes an assert
failure at ZEND_VM_NEXT_OPCODE(). We fix this by checking if there's an
exception after zend_is_callable_ex().
Closes GH-16803.
The `get_method` object handler may change the object pointer. SPL does
this in its iterator implementations. This causes the error message
to change to another class which is confusing to the user. JIT handles
this correctly. This patch aligns behaviour with JIT.
Closes GH-16576.
* PHP-8.4:
[ci skip] NEWS for GH-15960
Deny resetting an object as lazy during property iteration
Ensure to initialize lazy object in foreach
Do not null out obj->properties when resetting object
Fix handling of undef property during foreach by ref on hooked class
* Implement JIT for ZEND_FETCH_STATIC_PROP_* and improve interpretation
* Revert incorrect change
* Use FASTCALL calling convention
* Use EMPTY_SWITCH_DEFAULT_CASE
* Move the loading of the property info into zend_jit_uninit_static_prop()
zend_get_property_info_for_slot(obj, slot) assumes that 'slot' belongs to 'obj', but that may not be the case for lazy proxies.
Fortunately, the property info is often already available in path when it is needed.
For other cases, I make zend_get_property_info_for_slot() aware of lazy objects, and add zend_get_property_info_for_slot_self() for cases where the 'slot' is known to belong to the object itself.
Fixes oss-fuzz #71446
* Check VM interrupt while internal frame is on top
* Use tab instead of spaces
* fix frame used in interrupt and refactor
* remove unused failures for zend_jit_check_timeout
* Fix JIT support
Co-authored-by: Bob Weinand <bobwei9@hotmail.com>
* Fix the missing store to vm_interrupt
* Rename new functions
* Special case zend_interrupt_function in JIT code
* refactor to use ZEND_VM_SET_OPCODE_NO_INTERRUPT
* Split atomic exchange into load + store
It is difficult to determine performance of atomics sometimes. In this
case, the separate load+store is still correct, and a load does not
cause a modification, and might be faster for some platforms than an
exchange. A load+store is slower than an exchange, but we're fine
trading the penalty to the slow path and keeping the happy path faster.
---------
Co-authored-by: Bob Weinand <bobwei9@hotmail.com>
YIELD and YIELD_FROM increment opline before returning, but in most places
we need the opline to point to the YIELD and YIELD_FROM.
Here I change YIELD / YIELD_FROM to not increment opline. This simplifies the
code and fixes GH-15275 in a better way.
Closes GH-15328
This flag was never necessary. We know a static variable is uninitialized (i.e.
the initializer has never been called) iff the zval in the static variable array
does not contain a reference.
Prompted by a related issue in ext-uopz reported by Christoph.