Deferred backtrace generation algorithm.

php.internals

Edmond Dantes

1 year ago
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.

Rob Landers

1 year ago
On Fri, May 2, 2025, at 10:14, Edmond Dantes wrote:
> 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. >
Hey Ed,
> 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).
For what it is worth, the stack trace is not generated in the constructor (at least from the perspective of the developer). Using reflection to create the exception without calling the constructor still results in the stack trace being generated in the exception object. If it were created only in the constructor, then I suspect this could all be resolved in user-land libraries. — Rob

Edmond Dantes

1 year ago
Hello, Rob!
> For what it is worth, the stack trace is not generated in the constructor (at least from the perspective of the developer).
This is a design feature. In PHP, call frames for PHP functions are stored in a separate stack. However, they are still stored following the stack (LIFO) principle. When a function returns a result, the memory of its frame will be overwritten by the next one. If the backtrace hasn’t been built while the stack still exists, there might not be another chance. That’s the main technical challenge. For example, in Python (though this information might not be entirely accurate), frames are objects that can be reused.
> Using reflection to create the exception without calling the constructor still results in the stack trace being generated in the exception object.
That's because PHP objects actually have two constructors: * The internal constructor * The PHP-land constructor, called __construct The internal constructor must always be called. -- Ed

Bob Weinand

1 year ago
Hey Edmond, On 2.5.2025 10:14:37, Edmond Dantes wrote:
> 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).
It basically depends on the stack depth at that point. So it _can_ be more, or less than that.
> 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`.
What you actually want is, I think, a proper new call_info flag, which you can attach: "frame has associated exceptions to check" (you can stuff that into the same branch than ZEND_CALL_FREE_EXTRA_ARGS in leave_helper though). There can be many exceptions referencing the same (and different) stack frames in flight. So you effectively will have to manage a weakmap of next-stack-frame to exception objects. This certainly seems doable. Of note is also that $trace and $string are actually private properties. So access via reflection works there. And it might be changed too. It possibly should be converted to a property hook then, which materializes on first access. But otherwise, I think, this is mostly a matter of implementation. If you're interested in providing a patch, you're definitely welcome. Bob

Edmond Dantes

1 year ago
Hello Bob.
> So you effectively will have to manage a weakmap of next-stack-frame to exception objects.
Yes, that was exactly my first idea — to use a WeakMap from exceptions. Now it seems that copying part of the frame information will use less memory. But that's not certain :)
> Of note is also that $trace and $string are actually private properties.
Agree.
> If you're interested in providing a patch, you're definitely welcome.
Yes, I can give it a try. I'm just wondering if I'm missing something important in the logic.