Skip to content

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

Description

@PurHur

Category

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

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

  • 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

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

    MOST IMPORTANTThis are the most important targetsarea: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