In PHP, when does passing or assigning a large array actually duplicate its memory, and which values are refcounted at all?
answer
- share until someone writes
- separation on the first write
- int, float, bool, null live inline
- interned strings and immutable arrays
- debug_zval_dump() shows refcount
basics
~20 sAssigning or passing an array only shares it; PHP duplicates the table when one holder writes while others still share it. Integers, floats, booleans and null live inside the zval uncounted; strings, arrays and objects are refcounted.
solid answer
~50 sEvery PHP variable is a **zval**. Integers, floats, booleans and `null` are stored inside the zval itself, so copying them is trivial and nothing is counted. Strings, arrays and objects point to a separate structure with a **refcount**. Assigning or passing an array just increments that count; `memory_get_usage()` barely moves. The cost comes at the **first write** through a holder while the count is above one: PHP separates, copying the array's table so the writer has its own. Nested arrays and strings inside it are shared again, not deep-copied. So a function that receives a 50 MB array and only reads it costs nothing extra, while one that appends a single element briefly holds two copies and raises the peak. Objects differ: the variable holds a handle, so passing one never copies the object. String literals and constant arrays compiled by OPcache are **interned** or immutable and are not refcounted at all.
code
php · 13 lines<?php
declare(strict_types=1);
$a = range(1, 1_000_000);
$base = memory_get_usage();
$b = $a; // shares the array
echo memory_get_usage() - $base, "\n"; // about 0
$b[] = 0; // first write: separation
echo memory_get_usage() - $base, "\n"; // roughly the size of the array
debug_zval_dump('literal'); // string(7) "literal" internedgo deeper
Know that assigning an array does not copy it until one side writes, and that objects are shared through handles.
Explain zvals, which types are refcounted, separation on first write, and why a function that modifies a received array raises the peak.
Spot hidden separations in hot code paths, read peaks rather than averages, and avoid reference tricks that force copies of their own.
Guide APIs towards read-only passing of large data and chunked transformations, so memory cost scales with batch size rather than dataset size.
## zvals and what they point to In PHP every variable, array element and property holds a **zval**: a small fixed-size value with a type tag. What happens on copy depends on the type: | Type | Stored | Refcounted | |---|---|---| | `int`, `float`, `bool`, `null` | inside the zval | no | | `string` | separate buffer | yes, unless interned | | `array` | separate hash table | yes, unless immutable | | `object` | separate object; the zval holds a handle | yes | Copying a scalar copies a few bytes. Copying a string, array or object copies only a pointer and increments the target's **refcount**. ## Copy-on-write for arrays and strings Arrays and strings have **value semantics**: after `$b = $a`, changing `$b` must not change `$a`. PHP implements that lazily: 1. `$b = $a;` — both point to the same array; its refcount becomes 2. No memory is allocated. 2. `$b[] = 1;` — the write sees a refcount above 1, so PHP **separates**: it duplicates the array's table for `$b`, then writes. Now memory really doubles for the table. 3. `$a` keeps the original, whose refcount drops back to 1. The duplicate is **shallow**. Elements that are themselves strings or arrays are not copied; their refcounts are incremented, and they are separated later only if written through the new array. ## Where the cost shows up in real code - **Passing to a read-only function**: free. `function total(array $rows): int` that only loops over `$rows` adds no memory. - **Modifying a parameter**: a function that sorts, appends to or `unset()`s elements of a received array separates it, so for a moment the caller's array and the function's copy coexist. With a 200 MB array, the peak becomes 400 MB. - **Returning a modified copy**: `$rows = addTotals($rows);` separates inside the function, then the caller's old array is freed when `$rows` is reassigned. The peak still includes both. - **Array functions**: most, like `array_map()` and `array_filter()`, build a new array, so input and output coexist until the input is released. - **foreach by value**: iterating does not copy the array; the loop shares it. Writing to the array inside the loop is what forces a separation. ## Values that are not counted at all - **Interned strings**: literals, identifiers and class names in compiled code are stored once and shared without counting, and with OPcache they live in shared memory across requests. - **Immutable arrays**: with OPcache, an array literal made only of constants, such as `['draft', 'sent', 'paid']`, is stored as an immutable array. Assigning it copies nothing; the first write creates a normal, refcounted copy. `debug_zval_dump()` makes this visible: it prints `refcount(n)` for counted values and `interned` for interned strings and immutable arrays. ## Objects are different An object variable holds a **handle**. Passing it to a function or assigning it to another variable shares the same object, and writes are visible to every holder; nothing is separated. Only `clone` creates a second object, and even then it is shallow: properties that hold arrays are shared copy-on-write, properties that hold objects keep pointing at the same objects. ## Strings follow the same rule Strings are also copy-on-write. Appending with `.=` to a string that only one variable holds extends it in place; appending to a string that is shared first copies it. Functions such as `substr()` or `str_replace()` return new strings, so a loop that repeatedly rewrites a large string allocates a fresh copy each time. For large text output, writing pieces to a stream is cheaper than building one huge string. ## Practical rules 1. Pass large arrays freely to functions that only read them. 2. When a function must transform a large array, build the result while dropping the input, or process it in chunks. 3. Measure with `memory_get_peak_usage()`: separation costs show up as peaks, not as steady usage. 4. Do not reach for references (`&`) just to avoid a copy; a reference to an array that is also shared by value forces a separation of its own.
- Does passing an object to a function copy it like an array?No. The variable holds a handle, so the function receives a handle to the same object, and property changes are visible to the caller. Only `clone` makes a second object, and that copy is shallow.
- Why can modifying a received array inside a function double peak memory?The caller still holds the array, so its refcount is above one. The first write inside the function separates it: PHP duplicates the table for the function's copy. Until the function returns and one copy is released, both exist, and the peak reflects both.
A shared photocopy on a desk: everyone reads the same sheet, and only when someone wants to write on it do they make their own copy first.
saying these in an interview costs you the question
- Assigning an array to another variable copies it immediately
- Passing an object to a function clones it
- Integers and booleans carry a refcount
- Separation deep-copies every nested array
- Using & everywhere is the standard way to save memory