Good day, everyone.

In the neighboring thread Concept: Lightweight error channels, the
topic of exception performance was discussed.

The main issue is the immediate backtrace generation in the
constructor, which can cause up to a 50% performance loss compared to
return (as I understood from the discussion — I may be mistaken).

I have a small idea on how to improve the situation in cases where
exceptions are caught and a backtrace isn't needed.

Let’s assume we delay backtrace generation. In PHP, you can’t just
keep a reference to a stack frame, since it may be destroyed. You
could copy it, of course (which is relatively inexpensive compared to
generating the full backtrace).

Based on that, there are two possible implementations:

* Generate the backtrace at the moment the stack is freed

* Clone the stack frame when the stack is freed (this is roughly what
happens in Python)

This would require changes to the functions
`zend_vm_stack_free_extra_args_ex` and
`zend_vm_stack_free_extra_args`.

These functions could be used to either clone the stack frame or
generate the full/partial backtrace at the right moment.

The information about whether something is referencing a stack frame
could be stored in a separate structure in `EG`, so there would be no
need to modify `zend_execute_data`.
Any code referencing the frame would, of course, need to correctly
decrement the reference counter.

For example, when an exception is created, it increments the reference
count on the current call frame, which guarantees that the frame stays
available until either the exception is destroyed or the backtrace is
generated.

Nuances:

* It’s more efficient to copy only the parts of the stack frame needed
for the backtrace, not the entire frame. This also applies to
parameter slots — they should be converted immediately if the
DEBUG_BACKTRACE_IGNORE_ARGS flag is not set.

* When cloning the stack frame, the reference to the previous frame
should automatically be incremented.

At first glance, this algorithm doesn’t break backward compatibility
in any way and spreads out memory/CPU costs.

Even if multiple exceptions are created within a function but no
backtrace is generated, overall memory usage decreases, since Zend
only retains the data structures currently needed, rather than
duplicating them in each backtrace.

Cloned call frames exist only once and can be shared across multiple exceptions.

**What are the benefits?**

* The cost of throwing an exception that is caught and not used at the
top level is nearly equivalent to a return operation.

* Lower memory usage when no backtrace is generated.

* It becomes possible to reuse a single backtrace generation for
multiple exceptions that share common frames (needs further
consideration).

**Drawbacks:**

* Exiting a PHP function consumes CPU to copy ~100 bytes of memory.

* Increased code complexity in the exception/stack frame modules.

These conclusions were made mentally without writing actual code.
But perhaps someone will have something to add — either in favor or against.

---
Ed.

Reply via email to