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.
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 inEG
, so there would be no
need to modifyzend_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
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
Hey Edmond,
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
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.