From 7e9c280104818c13acdd0de89e541e14be52e5f1 Mon Sep 17 00:00:00 2001 From: PurHur Date: Wed, 2 Sep 2026 03:53:37 +0000 Subject: [PATCH] AOT: string assignment uses addref instead of eager memcpy (#36192) Value-box string copies now share refcounted __string__ like Zend zend_string_copy, fixing O(n) memcpy on every $u = $s assignment. Mutation paths still use __string__separate / __ref__separate for writes. Co-authored-by: Cursor --- lib/JIT/JitValueBox.php | 64 ++++++++----------- .../cases/aot_string_copy_addref_36192.phpt | 16 +++++ test/repro/string_copy_addref_36192.php | 9 +++ test/unit/StringCopyOnWriteAotTest.php | 23 +++++++ 4 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 test/fixtures/aot/cases/aot_string_copy_addref_36192.phpt create mode 100644 test/repro/string_copy_addref_36192.php create mode 100644 test/unit/StringCopyOnWriteAotTest.php diff --git a/lib/JIT/JitValueBox.php b/lib/JIT/JitValueBox.php index 43bed84d067..8a773e642a7 100644 --- a/lib/JIT/JitValueBox.php +++ b/lib/JIT/JitValueBox.php @@ -484,15 +484,7 @@ public static function assignToPointer(Context $context, Value $destPtr, Variabl // Function-static / alloca slots are __string__**; writeString wants * (#31966). $strPtr = $context->builder->load($strPtr); } - $owned = $context->builder->call( - $context->lookupFunction('__string__separate'), - $strPtr - ); - $context->builder->call( - $context->lookupFunction('__value__writeString'), - $destPtr, - $owned - ); + self::writeStringToValuePtrByAddref($context, $destPtr, $strPtr); return; case Variable::TYPE_OBJECT: @@ -568,14 +560,10 @@ public static function promoteNativeLvalueToValueBox(Context $context, Variable $context->builder->call($context->lookupFunction('__value__writeNull'), $ptr); break; case Variable::TYPE_STRING: - $owned = $context->builder->call( - $context->lookupFunction('__string__separate'), - $context->helper->loadValue($var) - ); - $context->builder->call( - $context->lookupFunction('__value__writeString'), + self::writeStringToValuePtrByAddref( + $context, $ptr, - $owned + $context->helper->loadValue($var) ); break; case Variable::TYPE_OBJECT: @@ -688,15 +676,7 @@ private static function copyBetweenPointers(Context $context, Value $destPtr, Va $context->lookupFunction('__value__readString'), $srcPtr ); - $owned = $context->builder->call( - $context->lookupFunction('__string__separate'), - $str - ); - $context->builder->call( - $context->lookupFunction('__value__writeString'), - $destPtr, - $owned - ); + self::writeStringToValuePtrByAddref($context, $destPtr, $str); $context->builder->branch($done); $context->builder->positionAtEnd($afterString); @@ -790,6 +770,20 @@ private static function copyBetweenPointers(Context $context, Value $destPtr, Va BasicBlockHelper::branchToFreshContinue($context, 'after_value_copy_'.$tag); } + /** + * Share a refcounted {@see __string__} into a value box (Zend zend_string_copy semantics). + * {@see __string__separate} is for mutation / hashtable-key ownership, not assignment copy. + */ + private static function writeStringToValuePtrByAddref(Context $context, Value $destPtr, Value $strPtr): void + { + $context->refcount->addref($strPtr); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $destPtr, + $strPtr + ); + } + /** * Read boxed bool payload (writeBool stores int8 at value[0]). * Do not use {@see __value__readLong} — no NATIVE_BOOL arm (#21892). @@ -888,14 +882,10 @@ public static function valuePtrFromNativeVariable(Context $context, Variable $va ); break; case Variable::TYPE_STRING: - $owned = $context->builder->call( - $context->lookupFunction('__string__separate'), - $native - ); - $context->builder->call( - $context->lookupFunction('__value__writeString'), + self::writeStringToValuePtrByAddref( + $context, self::pointer($context, $slot), - $owned + $native ); break; case Variable::TYPE_OBJECT: @@ -976,14 +966,10 @@ public static function coerceToValuePtrForStore(Context $context, Value $raw): V } if ('__string__*' === $tyName) { $slot = self::alloc($context); - $owned = $context->builder->call( - $context->lookupFunction('__string__separate'), - $raw - ); - $context->builder->call( - $context->lookupFunction('__value__writeString'), + self::writeStringToValuePtrByAddref( + $context, self::pointer($context, $slot), - $owned + $raw ); return self::pointer($context, $slot); diff --git a/test/fixtures/aot/cases/aot_string_copy_addref_36192.phpt b/test/fixtures/aot/cases/aot_string_copy_addref_36192.phpt new file mode 100644 index 00000000000..d060c7df45d --- /dev/null +++ b/test/fixtures/aot/cases/aot_string_copy_addref_36192.phpt @@ -0,0 +1,16 @@ +--TEST-- +Language: AOT string assignment shares refcount (copy-on-write) — not eager memcpy (#36192) +--FILE-- +assertStringContainsString('writeStringToValuePtrByAddref', $src); + $this->assertStringContainsString('$context->refcount->addref($strPtr)', $src); + $this->assertStringNotContainsString('lookupFunction(\'__string__separate\')', $src); + } +}