Skip to content

Runtime: every string copy is an eager memcpy — __string__separate ignores the refcount, no copy-on-write (lib/JIT/Builtin/Type/String_.php, lib/JIT/JitValueBox.php) #36192

Description

@PurHur

Category

Foundation: · runtime value model · child of #36188

Problem

%__string__ = <{ %__ref__ (refcount+flags), i64 len, i8 data… }> carries a refcount, but assignment does not use it. JitValueBox::copyBetweenPointers() (lib/JIT/JitValueBox.php:638-) lowers the string arm of every value copy as

%s = call @__value__readString(src)
%n = call @__string__separate(%s)      ; alloc + memcpy(len+1), unconditionally
call @__value__writeString(dst, %n)

and String_::implementSeparate() (lib/JIT/Builtin/Type/String_.php:748-790) is __string__alloc(len) + memcpy with no refcount check. So $b = $a;, passing a string argument, returning a string, storing into an array element, and every boxed temporary copies the bytes. A 1 MB string passed through three functions is copied three times; string-heavy code (templating, JSON, HTML building) pays O(len) per assignment where Zend pays an increment.

The generated code for this arm is also emitted inline at each of the 176 copy* call sites (see the codegen-bloat sibling issue).

php-src reference

  • Zend/zend_string.hzend_string_copy is GC_ADDREF; zend_string_separate/SEPARATE_STRING only copies when GC_REFCOUNT > 1 and a write is about to happen; interned strings (IS_STR_INTERNED) are never refcounted or copied.

PHP implementation target

  • JitValueBox::copyBetweenPointers string arm → __ref__addref only (immortal/interned strings skip even that; the __ref__ flags already carry an interned bit — __init__ clears bit 0 on constants).
  • Every in-place mutation site (.=, $s[$i] = 'x', str_* helpers that write into their argument, __string__realloc users) must call a __string__separate_for_write that copies only if refcount > 1.
  • __value__valueDelref for strings must free at refcount 0 (verify __ref__delref already does).
  • Run with --repeat 10 and under valgrind/ASan for one sweep: this changes ownership rules, and the repo's characteristic bug is intermittent heap corruption (AOT: script-scope ++/-- goes wrong once several statements or variables are involved (loop after statements, second variable) #23842).

Repro

./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && cat > build/scopy.php <<'"'"'EOP'"'"'
<?php $s = str_repeat("x", 1<<20); $t = 0; for ($i = 0; $i < 20000; $i++) { $u = $s; $t += strlen($u); } echo $t, "\n";
EOP
php bin/compile.php -o build/scopy build/scopy.php && time ./build/scopy && time php build/scopy.php'

Today AOT copies 20 GB; Zend does 20k increments.

Done when

  • The probe above runs within 2x of Zend under AOT
  • A test/differential/cases/ batch covering aliasing semantics: copy-then-mutate original, mutate copy, .= on a shared string, by-ref string params, string in array copied then modified, str_repeat/substr results
  • script/differential-sweep.sh --aot --repeat 10 green; no new leaks (PHP_COMPILER_VM_RSS_GUARD-style peak RSS check on the probe)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    IMPORTANTCritical patharea:compilerCompiler / CFG / JITbugSomething isn't workingimplementation-readySpec complete: repro, php-src ref, done-when — safe for workers to claimphase-3:aotPhase 3 – AOT deployment

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions