You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AOT: every array or string mutation in {main} scope is O(n²) in time and memory — refcounts never decrement in main, so separateContainerForWrite copies the whole table on every store (16k appends: 6.1 s in main vs 0.011 s inside a function) (lib/JIT/Builtin/Refcount.php phpc_destruct_delref_allowed, lib/JIT/HashTableWriteLlvm.php:1694) #36252
Foundation: · runtime memory model, root cause of the array/string blow-ups measured in #36188 · child of #36188 · sharpens #36215
Problem
Same loop, master 4eed6a2785, pinned image (build/audit/v_arr.php vs v_arr_fn.php):
$a = []; for (...) { $a[] = $i; }
n = 8,000
16,000
64,000
256,000
inside a function
0.012 s
0.011 s
0.014 s
0.022 s
at top level ({main})
1.54 s
6.1 s
> 60 s / OOM-killed at 12 GB (n = 50k–100k)
—
Zend (either)
~0.03 s
Peak RSS at top level: 6.7 MB → 47 MB → 175 MB → 702 MB for n = 1k/2k/4k/8k — quadratic memory, not just time. The same shape explains the other top-level probes in the audit: 5,000 string-key inserts = 171 s (#36191), the 300k .= loop killed at 12 GB (#36216/#36244), 2,000 copies of a 1 MB string = 2 GB memcpy (#36192).
Mechanism: HashTableWriteLlvm::separateContainerForWrite() (lib/JIT/HashTableWriteLlvm.php:1694) is correct copy-on-write — it duplicates only when refcount > 1. But in {main}phpc_destruct_delref_allowed() returns 0 so __value__valueDelref returns early (lib/JIT/Builtin/Refcount.php:458-480, #4013 "nothing is freed in main until shutdown"). Every temporary that ever addref'd the array (the assignment's value copy, the loop's boxed temps) keeps its count, so every store sees refcount > 1, calls HashTableDuplicateRuntime::duplicate, and the previous copy is never freed. Inside a function, delref works and the array stays refcount 1.
Most scripts, tests and benchmarks mutate arrays at top level — this is why benchmarks/README.md's array_access.php is .disabled, and why the audit's top-level probes looked catastrophically slower than the function-scoped ones.
php-src reference
Zend has no special case for the main op_array: zend_execute_scripts frees CVs and temporaries exactly like any other frame; SEPARATE_ARRAY copies only when GC_REFCOUNT(ht) > 1, which for a fresh array being appended to in a loop is never.
PHP implementation target
Make {main} release temporaries and dead locals like a function frame: delref boxed temps at the end of each statement / on overwrite, keep the "no destructor invocation in main until shutdown" semantics (Language: User __destruct() — JIT/AOT invoke on object release (phase 2 of #3144) #4013) only where it is actually required (globals whose destructors must run at shutdown in order), i.e. gate destruct invocation, not refcount decrement.
Top-level 16k-append loop ≤ 0.05 s and n = 256k within 2x of the function-scoped version; peak RSS linear in n
script/differential-sweep.sh --aot --repeat 10 unchanged (destructor order cases in test/compliance/cases/language/*destruct* by name); examples-web-smoke green
docs/runtime-semantics.md states the {main} ownership rule
Category
Foundation:· runtime memory model, root cause of the array/string blow-ups measured in #36188 · child of #36188 · sharpens #36215Problem
Same loop, master
4eed6a2785, pinned image (build/audit/v_arr.phpvsv_arr_fn.php):$a = []; for (...) { $a[] = $i; }{main})Peak RSS at top level: 6.7 MB → 47 MB → 175 MB → 702 MB for n = 1k/2k/4k/8k — quadratic memory, not just time. The same shape explains the other top-level probes in the audit: 5,000 string-key inserts = 171 s (#36191), the 300k
.=loop killed at 12 GB (#36216/#36244), 2,000 copies of a 1 MB string = 2 GB memcpy (#36192).Mechanism:
HashTableWriteLlvm::separateContainerForWrite()(lib/JIT/HashTableWriteLlvm.php:1694) is correct copy-on-write — it duplicates only whenrefcount > 1. But in{main}phpc_destruct_delref_allowed()returns 0 so__value__valueDelrefreturns early (lib/JIT/Builtin/Refcount.php:458-480, #4013 "nothing is freed in main until shutdown"). Every temporary that ever addref'd the array (the assignment's value copy, the loop's boxed temps) keeps its count, so every store seesrefcount > 1, callsHashTableDuplicateRuntime::duplicate, and the previous copy is never freed. Inside a function, delref works and the array stays refcount 1.Most scripts, tests and benchmarks mutate arrays at top level — this is why
benchmarks/README.md'sarray_access.phpis.disabled, and why the audit's top-level probes looked catastrophically slower than the function-scoped ones.php-src reference
zend_execute_scriptsfrees CVs and temporaries exactly like any other frame;SEPARATE_ARRAYcopies only whenGC_REFCOUNT(ht) > 1, which for a fresh array being appended to in a loop is never.PHP implementation target
{main}release temporaries and dead locals like a function frame: delref boxed temps at the end of each statement / on overwrite, keep the "no destructor invocation in main until shutdown" semantics (Language: User __destruct() — JIT/AOT invoke on object release (phase 2 of #3144) #4013) only where it is actually required (globals whose destructors must run at shutdown in order), i.e. gate destruct invocation, not refcount decrement.test/differential/cases/with a wall-clock bound, and to the bench gate (Foundation: no gate watches generated-code performance — an 8x regression of the headline benchmark shipped unnoticed for five weeks (script/bench.php, .github/workflows/compiler-gate.yml) #36196):{main}and function scope must be within 2x of each other for append/assoc/.=/string-copy loops.build/audit/micro3.sh) after the fix and update Runtime: __hashtable__ string keys are a singly linked list with no hash function — O(n) lookup, O(n²) build for every associative array (lib/JIT/Builtin/Type/HashTable.php) #36191/Runtime: every string copy is an eager memcpy —__string__separateignores the refcount, no copy-on-write (lib/JIT/Builtin/Type/String_.php, lib/JIT/JitValueBox.php) #36192/Runtime:.=is O(n²) — every append reallocates and memcpys both sides; no capacity field, no runtime interning (lib/JIT/Builtin/Type/String_.pre concat, lib/JIT/Builtin/Type/String_.php:928) #36216 with the residual costs — the hash index (Runtime: __hashtable__ string keys are a singly linked list with no hash function — O(n) lookup, O(n²) build for every associative array (lib/JIT/Builtin/Type/HashTable.php) #36191) and string COW (Runtime: every string copy is an eager memcpy —__string__separateignores the refcount, no copy-on-write (lib/JIT/Builtin/Type/String_.php, lib/JIT/JitValueBox.php) #36192) remain necessary but are second-order until this lands.Repro
./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && printf "<?php \$a=[]; for(\$i=0;\$i<16000;\$i++){ \$a[]=\$i; } echo count(\$a),\"\\n\";" > build/m.php && printf "<?php function f(){ \$a=[]; for(\$i=0;\$i<16000;\$i++){ \$a[]=\$i; } return count(\$a); } echo f(),\"\\n\";" > build/f.php && php bin/compile.php -o build/m build/m.php && php bin/compile.php -o build/f build/f.php && time ./build/m && time ./build/f'Done when
script/differential-sweep.sh --aot --repeat 10unchanged (destructor order cases intest/compliance/cases/language/*destruct*by name);examples-web-smokegreendocs/runtime-semantics.mdstates the{main}ownership rule