From 0be5bb0b2c45d9eb9c7873f08978758b9a531758 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Sun, 27 Apr 2025 14:05:10 +0200
Subject: [PATCH] Improve performance of instantiating exceptions/errors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The class structure is fixed, so it makes no sense to go through all the
logic of looking up property info etc.
This patch introduces a local function `zend_obj_prop_num_checked()` to
help with that.

For this benchmark:
```php
for ($i = 0; $i < 1000000; $i++)
   new Error;
```

On an i7-4790:
```
Benchmark 1: ./sapi/cli/php  x.php
  Time (mean ± σ):     141.6 ms ±   9.3 ms    [User: 138.7 ms, System: 2.0 ms]
  Range (min … max):   135.4 ms … 177.7 ms    20 runs

Benchmark 2: ../RELx64_old/sapi/cli/php x.php
  Time (mean ± σ):     214.1 ms ±   7.0 ms    [User: 207.6 ms, System: 5.0 ms]
  Range (min … max):   206.6 ms … 230.9 ms    13 runs

Summary
  ./sapi/cli/php  x.php ran
    1.51 ± 0.11 times faster than ../RELx64_old/sapi/cli/php x.php
```

For this benchmark:
```php
for ($i = 0; $i < 1000000; $i++)
    new Exception("message", 0, null);
```

On an i7-4790:
```
Benchmark 1: ./sapi/cli/php  x.php
  Time (mean ± σ):     184.3 ms ±   9.5 ms    [User: 181.2 ms, System: 1.8 ms]
  Range (min … max):   173.8 ms … 205.1 ms    15 runs

Benchmark 2: ../RELx64_old/sapi/cli/php x.php
  Time (mean ± σ):     253.7 ms ±   7.0 ms    [User: 247.6 ms, System: 4.6 ms]
  Range (min … max):   245.7 ms … 263.7 ms    11 runs

Summary
  ./sapi/cli/php  x.php ran
    1.38 ± 0.08 times faster than ../RELx64_old/sapi/cli/php x.php
```

For this benchmark:
```php
for ($i = 0; $i < 1000000; $i++)
    new ErrorException("message", 0, 0, "xyz", 0, null);
```

On an i7-4790:
```
Benchmark 1: ./sapi/cli/php  x.php
  Time (mean ± σ):     223.6 ms ±   7.7 ms    [User: 220.1 ms, System: 2.4 ms]
  Range (min … max):   216.9 ms … 242.5 ms    12 runs

Benchmark 2: ../RELx64_old/sapi/cli/php x.php
  Time (mean ± σ):     343.5 ms ±   8.1 ms    [User: 337.1 ms, System: 4.6 ms]
  Range (min … max):   337.3 ms … 362.8 ms    10 runs

Summary
  ./sapi/cli/php  x.php ran
    1.54 ± 0.06 times faster than ../RELx64_old/sapi/cli/php x.php
```
---
 UPGRADING              |  3 ++
 Zend/zend_exceptions.c | 96 ++++++++++++++++++++++++------------------
 2 files changed, 57 insertions(+), 42 deletions(-)

diff --git a/UPGRADING b/UPGRADING
index 6fd01ab0e370f..75bd65ffe1c07 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -469,6 +469,9 @@ PHP 8.5 UPGRADE NOTES
 14. Performance Improvements
 ========================================
 
+- Core:
+  . Creating exceptions objects is now up to 50% faster.
+
 - ReflectionProperty:
   . Improved performance of the following methods: getValue(), getRawValue(),
     isInitialized(), setValue(), setRawValue().
diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c
index f9d0ae8ea8173..38145aca215b4 100644
--- a/Zend/zend_exceptions.c
+++ b/Zend/zend_exceptions.c
@@ -254,41 +254,45 @@ ZEND_API void zend_clear_exception(void) /* {{{ */
 }
 /* }}} */
 
+/* Same as OBJ_PROP_NUM(), but checks the offset is correct when Zend is built in debug mode. */
+static zend_always_inline zval *zend_obj_prop_num_checked(zend_object *object, uint32_t prop_num, zend_string *str)
+{
+#if ZEND_DEBUG
+	zend_class_entry *old_scope = EG(fake_scope);
+	EG(fake_scope) = i_get_exception_base(object);
+	const zend_property_info *prop_info = zend_get_property_info(object->ce, str, true);
+	ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == prop_num);
+	EG(fake_scope) = old_scope;
+#else
+	ZEND_IGNORE_VALUE(str);
+#endif
+	return OBJ_PROP_NUM(object, prop_num);
+}
+
 static zend_object *zend_default_exception_new(zend_class_entry *class_type) /* {{{ */
 {
-	zval tmp;
-	zval trace;
-	zend_class_entry *base_ce;
 	zend_string *filename;
 
 	zend_object *object = zend_objects_new(class_type);
 	object_properties_init(object, class_type);
+	zval *trace = zend_obj_prop_num_checked(object, 5, ZSTR_KNOWN(ZEND_STR_TRACE));
 
 	if (EG(current_execute_data)) {
-		zend_fetch_debug_backtrace(&trace,
+		zend_fetch_debug_backtrace(trace,
 			0,
 			EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0, 0);
 	} else {
-		array_init(&trace);
+		ZVAL_EMPTY_ARRAY(trace);
 	}
-	Z_SET_REFCOUNT(trace, 0);
-
-	base_ce = i_get_exception_base(object);
 
 	if (EXPECTED((class_type != zend_ce_parse_error && class_type != zend_ce_compile_error)
 			|| !(filename = zend_get_compiled_filename()))) {
-		ZVAL_STRING(&tmp, zend_get_executed_filename());
-		zend_update_property_ex(base_ce, object, ZSTR_KNOWN(ZEND_STR_FILE), &tmp);
-		zval_ptr_dtor(&tmp);
-		ZVAL_LONG(&tmp, zend_get_executed_lineno());
-		zend_update_property_ex(base_ce, object, ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
+		ZVAL_STRING(zend_obj_prop_num_checked(object, 3, ZSTR_KNOWN(ZEND_STR_FILE)), zend_get_executed_filename());
+		ZVAL_LONG(zend_obj_prop_num_checked(object, 4, ZSTR_KNOWN(ZEND_STR_LINE)), zend_get_executed_lineno());
 	} else {
-		ZVAL_STR(&tmp, filename);
-		zend_update_property_ex(base_ce, object, ZSTR_KNOWN(ZEND_STR_FILE), &tmp);
-		ZVAL_LONG(&tmp, zend_get_compiled_lineno());
-		zend_update_property_ex(base_ce, object, ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
+		ZVAL_STR_COPY(zend_obj_prop_num_checked(object, 3, ZSTR_KNOWN(ZEND_STR_FILE)), filename);
+		ZVAL_LONG(zend_obj_prop_num_checked(object, 4, ZSTR_KNOWN(ZEND_STR_LINE)), zend_get_compiled_lineno());
 	}
-	zend_update_property_ex(base_ce, object, ZSTR_KNOWN(ZEND_STR_TRACE), &trace);
 
 	return object;
 }
@@ -307,28 +311,30 @@ ZEND_METHOD(Exception, __construct)
 {
 	zend_string *message = NULL;
 	zend_long   code = 0;
-	zval  tmp, *object, *previous = NULL;
-	zend_class_entry *base_ce;
+	zval  *object, *previous = NULL;
 
 	object = ZEND_THIS;
-	base_ce = i_get_exception_base(Z_OBJ_P(object));
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "|SlO!", &message, &code, &previous, zend_ce_throwable) == FAILURE) {
 		RETURN_THROWS();
 	}
 
 	if (message) {
-		ZVAL_STR(&tmp, message);
-		zend_update_property_ex(base_ce, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_MESSAGE), &tmp);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 0, ZSTR_KNOWN(ZEND_STR_MESSAGE));
+		zval_ptr_dtor(tmp);
+		ZVAL_STR_COPY(tmp, message);
 	}
 
 	if (code) {
-		ZVAL_LONG(&tmp, code);
-		zend_update_property_ex(base_ce, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_CODE), &tmp);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 2, ZSTR_KNOWN(ZEND_STR_CODE));
+		zval_ptr_dtor(tmp);
+		ZVAL_LONG(tmp, code);
 	}
 
 	if (previous) {
-		zend_update_property_ex(base_ce, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_PREVIOUS), previous);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 6, ZSTR_KNOWN(ZEND_STR_PREVIOUS));
+		zval_ptr_dtor(tmp);
+		ZVAL_COPY(tmp, previous);
 	}
 }
 /* }}} */
@@ -358,7 +364,7 @@ ZEND_METHOD(ErrorException, __construct)
 	zend_string *message = NULL, *filename = NULL;
 	zend_long   code = 0, severity = E_ERROR, lineno;
 	bool lineno_is_null = 1;
-	zval   tmp, *object, *previous = NULL;
+	zval   *object, *previous = NULL;
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "|SllS!l!O!", &message, &code, &severity, &filename, &lineno, &lineno_is_null, &previous, zend_ce_throwable) == FAILURE) {
 		RETURN_THROWS();
@@ -367,35 +373,41 @@ ZEND_METHOD(ErrorException, __construct)
 	object = ZEND_THIS;
 
 	if (message) {
-		ZVAL_STR_COPY(&tmp, message);
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_MESSAGE), &tmp);
-		zval_ptr_dtor(&tmp);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 0, ZSTR_KNOWN(ZEND_STR_MESSAGE));
+		zval_ptr_dtor(tmp);
+		ZVAL_STR_COPY(tmp, message);
 	}
 
 	if (code) {
-		ZVAL_LONG(&tmp, code);
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_CODE), &tmp);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 2, ZSTR_KNOWN(ZEND_STR_CODE));
+		zval_ptr_dtor(tmp);
+		ZVAL_LONG(tmp, code);
 	}
 
 	if (previous) {
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_PREVIOUS), previous);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 6, ZSTR_KNOWN(ZEND_STR_PREVIOUS));
+		zval_ptr_dtor(tmp);
+		ZVAL_COPY(tmp, previous);
 	}
 
-	ZVAL_LONG(&tmp, severity);
-	zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_SEVERITY), &tmp);
+	{
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 7, ZSTR_KNOWN(ZEND_STR_SEVERITY));
+		zval_ptr_dtor(tmp);
+		ZVAL_LONG(tmp, severity);
+	}
 
 	if (filename) {
-		ZVAL_STR_COPY(&tmp, filename);
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_FILE), &tmp);
-		zval_ptr_dtor(&tmp);
+		zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 3, ZSTR_KNOWN(ZEND_STR_FILE));
+		zval_ptr_dtor(tmp);
+		ZVAL_STR_COPY(tmp, filename);
 	}
 
+	zval *tmp = zend_obj_prop_num_checked(Z_OBJ_P(object), 4, ZSTR_KNOWN(ZEND_STR_LINE));
+	zval_ptr_dtor(tmp);
 	if (!lineno_is_null) {
-		ZVAL_LONG(&tmp, lineno);
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
+		ZVAL_LONG(tmp, lineno);
 	} else if (filename) {
-		ZVAL_LONG(&tmp, 0);
-		zend_update_property_ex(zend_ce_exception, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
+		ZVAL_LONG(tmp, 0);
 	}
 }
 /* }}} */