Warum Kopieren fast nichts kostet, bis es doch etwas kostet
Eine Zuweisung wie $b = $a bei einem großen PHP-Array wirkt auf den ersten Blick wie eine teure Kopieroperation, kostet intern aber zunächst nur das Erhöhen eines Referenzzählers. Copy-on-Write verschiebt die eigentliche Kopie auf den Moment, in dem eine der beiden Variablen tatsächlich verändert wird, und genau dieser Mechanismus entscheidet über Speicherverbrauch und Performance bei jeder Array-Übergabe in PHP.
Inhaltsverzeichnis
- 1. Was Copy-on-Write bei Arrays eigentlich bedeutet
- 2. Der interne Aufbau von zend_array
- 3. Referenzzählung und der Moment der echten Kopie
- 4. Funktionsaufrufe und Copy-on-Write im Zusammenspiel
- 5. Explizite Referenzen und wie sie Copy-on-Write aushebeln
- 6. Verschachtelte Arrays und partielle Kopien
- 7. Copy-on-Write praktisch sichtbar machen
- 8. Typische Fallstricke in der Praxis
- 9. Copy-on-Write im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Copy-on-Write bei Arrays eigentlich bedeutet
PHP-Arrays gelten offiziell als Wertetypen: Eine Zuweisung wie $b = $a soll sich so verhalten, als würde eine unabhängige Kopie von $a in $b entstehen, sodass eine spätere Änderung an $b das Original in $a nicht beeinflusst. Würde PHP diese Kopie bei jeder Zuweisung tatsächlich sofort physisch anlegen, wäre das bei großen Arrays mit tausenden Elementen spürbar langsam und speicherintensiv, selbst wenn die Kopie am Ende nie verändert wird.
Copy-on-Write löst genau dieses Problem, indem die physische Kopie auf den letztmöglichen Moment verschoben wird: den Zeitpunkt, an dem eine der beiden Variablen tatsächlich modifiziert wird. Bis dahin teilen sich $a und $b intern dieselbe Speicherstruktur, PHP zählt lediglich mit, wie viele Variablen aktuell auf diese Struktur verweisen. Aus Sicht des PHP-Codes verhält sich das Array trotzdem korrekt als Wertetyp, weil die geteilte Struktur für beide Variablen unverändert bleibt, solange keine Schreiboperation stattfindet.
Dieser Mechanismus betrifft nicht nur einfache Zuweisungen, sondern auch die Übergabe von Arrays als Funktionsparameter, das Speichern in weiteren Arrays und Rückgabewerte von Funktionen. Copy-on-Write ist damit einer der zentralen Performance-Mechanismen von PHP, der im Alltag meist unsichtbar bleibt, dessen genaues Verständnis aber entscheidend wird, sobald große Datenmengen mehrfach durch Funktionsaufrufe gereicht werden.
2. Der interne Aufbau von zend_array
Intern wird jedes PHP-Array durch eine Struktur namens zend_array repräsentiert, die sowohl geordnete Werte als auch, für assoziative Arrays, eine Hashtabelle für die Schlüssel verwaltet. Zentral für Copy-on-Write ist das eingebettete zend_refcounted_h-Feld, das einen Referenzzähler enthält. Dieser Zähler gibt an, wie viele PHP-Variablen aktuell auf genau diese eine zend_array-Instanz verweisen, unabhängig davon, unter welchem Variablennamen sie im Code erscheinen.
Wird ein Array einer neuen Variable zugewiesen, etwa durch $b = $a, erzeugt PHP keine neue zend_array-Struktur. Stattdessen erhält $b lediglich einen Zeiger auf dieselbe Struktur, auf die auch $a zeigt, und der Referenzzähler wird von eins auf zwei erhöht. Diese Operation kostet nur wenige Instruktionen und ist unabhängig von der tatsächlichen Größe des Arrays, ob es zehn oder zehn Millionen Elemente enthält, macht für die Zuweisung selbst keinen Unterschied.
Das gilt sowohl für einfache Zuweisungen als auch für das Ablegen eines Arrays als Wert in einem anderen Array oder als Property eines Objekts. In allen Fällen erhöht PHP zunächst nur den Referenzzähler der zugrundeliegenden zend_array-Struktur, statt die enthaltenen Elemente zu duplizieren. Erst eine tatsächliche Änderung an einer der beteiligten Variablen bricht diese Freigabe auf.
3. Referenzzählung und der Moment der echten Kopie
Sobald eine der beiden Variablen tatsächlich verändert wird, etwa durch $b[] = 'neu' oder $b['key'] = 'wert', prüft PHP vor der eigentlichen Schreiboperation den Referenzzähler der zugrundeliegenden Struktur. Steht der Zähler auf einem Wert größer als eins, bedeutet das, mindestens eine weitere Variable teilt sich diese Struktur, und PHP erzeugt an dieser Stelle eine vollständige, unabhängige physische Kopie der gesamten zend_array-Struktur, bevor die Änderung angewendet wird. Erst diese Kopie wird dann tatsächlich modifiziert, das Original bleibt für alle anderen Variablen unverändert erhalten.
Dieser Moment der Kopie ist der eigentlich teure Vorgang von Copy-on-Write, und seine Kosten skalieren linear mit der Größe des Arrays: Ein Array mit einer Million Elementen zu kopieren, kostet spürbar mehr Zeit und Speicher als ein Array mit zehn Elementen. Solange aber nur gelesen wird, etwa durch foreach-Iteration oder einfache Wertzugriffe, bleibt der Referenzzähler unverändert und keine Kopie entsteht, unabhängig davon, wie oft gelesen wird.
declare(strict_types=1);
$original = range(1, 1_000_000);
$before = memory_get_usage();
// No copy yet: only the refcount of the underlying zend_array increases
$shared = $original;
$afterAssignment = memory_get_usage();
printf("After assignment: %d bytes extra\n", $afterAssignment - $before);
// Typically close to zero — no physical copy has happened yet
// Now trigger an actual copy: modifying $shared forces PHP to
// duplicate the whole zend_array structure before applying the change
$shared[] = 1_000_001;
$afterMutation = memory_get_usage();
printf("After mutation: %d bytes extra\n", $afterMutation - $before);
// Now the full array size is duplicated in memory
Der Unterschied zwischen den beiden Messpunkten in diesem Beispiel zeigt Copy-on-Write direkt: Nach der reinen Zuweisung ist kaum zusätzlicher Speicher belegt, nach der ersten Schreiboperation auf $shared verdoppelt sich der Speicherverbrauch annähernd, weil jetzt zwei vollständig unabhängige Kopien des Millionen-Elemente-Arrays existieren.
4. Funktionsaufrufe und Copy-on-Write im Zusammenspiel
PHP übergibt Arrays standardmäßig by value an Funktionen, was ohne Copy-on-Write bei jedem Funktionsaufruf eine vollständige Kopie erzwingen würde, selbst wenn die Funktion das übergebene Array gar nicht verändert. Mit Copy-on-Write passiert genau das nicht: Der Parameter innerhalb der Funktion verweist zunächst auf dieselbe zend_array-Struktur wie das Argument im Aufrufer-Kontext, der Referenzzähler wird erhöht, keine physische Kopie entsteht.
Erst wenn die Funktion den Parameter tatsächlich modifiziert, etwa durch Hinzufügen oder Entfernen eines Elements, greift derselbe Mechanismus wie bei Variablenzuweisungen: PHP erzeugt eine private Kopie innerhalb des Funktions-Scopes, das Original im Aufrufer bleibt unangetastet. Diese Eigenschaft macht reine, lesende Funktionen mit Array-Parametern praktisch kostenlos in Bezug auf Speicher und Kopieraufwand, unabhängig von der Größe des übergebenen Arrays.
declare(strict_types=1);
/**
* Read-only function: no copy happens regardless of array size,
* because the refcount only increases, nothing is ever written.
*/
function sumValues(array $values): int
{
$total = 0;
foreach ($values as $value) {
$total += $value;
}
return $total;
}
/**
* Mutating function: forces a real copy inside the function scope
* as soon as the first write happens, caller's array stays untouched.
*/
function withAppendedTotal(array $values): array
{
$values[] = array_sum($values); // triggers copy-on-write here
return $values;
}
$data = range(1, 500_000);
$sum = sumValues($data); // no copy — pure read
$extended = withAppendedTotal($data); // copy happens inside the function
// $data itself remains fully unchanged and unaffected by withAppendedTotal
Diese Eigenschaft ist ein zentraler Grund, warum Type-Hints wie array $values ohne zusätzliche Performance-Sorge in Funktionssignaturen verwendet werden können, selbst wenn potenziell sehr große Arrays übergeben werden. Der Type-Hint selbst löst keine Kopie aus, ausschließlich das tatsächliche Schreiben innerhalb der Funktion tut das.
5. Explizite Referenzen und wie sie Copy-on-Write aushebeln
Sobald eine Referenz mit & ins Spiel kommt, etwa $b = &$a oder ein Funktionsparameter mit function foo(array &$values), ändert sich das Verhalten grundlegend. Eine Referenz bindet zwei Variablennamen dauerhaft an dieselbe zugrundeliegende Struktur, und Copy-on-Write greift für diese Bindung nicht mehr: Eine Änderung über eine der beiden referenzierten Variablen wird sofort auch über die andere sichtbar, es entsteht zu keinem Zeitpunkt eine unabhängige Kopie.
Praktisch bedeutet das: Referenzen sind die richtige Wahl, wenn eine Funktion ein großes Array tatsächlich in-place modifizieren soll, ohne dass PHP dafür eine vollständige Kopie anlegt, etwa bei einer Sortierfunktion, die das übergebene Array direkt umordnet, statt eine sortierte Kopie zurückzugeben. Der Nachteil: Referenzen sind schwerer nachzuvollziehen, weil eine Funktion mit Referenzparameter unsichtbar im Aufrufer-Kontext Zustand verändert, was den Kontrollfluss weniger offensichtlich macht als ein expliziter Rückgabewert.
Ein oft übersehener Nebeneffekt: Sobald eine Variable irgendwo im Code per Referenz gebunden wurde, verliert sie dauerhaft die Fähigkeit, an Copy-on-Write teilzunehmen, selbst nachdem die Referenzbindung durch unset() aufgelöst wurde. PHP markiert intern die zugrundeliegende Struktur als referenzierbar (IS_REF), und diese Markierung bleibt bestehen, bis die Struktur vollständig neu erzeugt wird. Das kann bei intensiver Verwendung von Referenzen zu unerwarteten, dauerhaften Kopien führen, wo eigentlich Copy-on-Write greifen sollte.
6. Verschachtelte Arrays und partielle Kopien
Bei mehrdimensionalen Arrays greift Copy-on-Write auf jeder Verschachtelungsebene unabhängig. Wird nur ein verschachteltes Unterarray verändert, kopiert PHP nicht zwangsläufig das komplette äußere Array vollständig, sondern kann in bestimmten Fällen nur den betroffenen Zweig der Struktur duplizieren, während unveränderte Geschwister-Elemente weiterhin die geteilte Struktur referenzieren. Dieses Verhalten ist jedoch stark von der genauen Zugriffsart abhängig und sollte nicht als Garantie behandelt werden, ohne es für den konkreten Anwendungsfall zu messen.
Ein praktisches Beispiel: Ein großes, verschachteltes Konfigurationsarray, aus dem eine Funktion nur einen einzelnen, tief verschachtelten Wert liest, verursacht keine Kopie, weil ausschließlich gelesen wird. Wird dagegen ein einzelner Schlüssel in einem tief verschachtelten Unterarray geändert, muss PHP mindestens den Pfad von der Wurzel bis zu diesem Schlüssel kopieren, weil jede Ebene auf dem Weg dorthin als eigene zend_array-Struktur mit eigenem Referenzzähler existiert.
Für die Praxis bedeutet das: Bei sehr großen, tief verschachtelten Konfigurationsstrukturen, wie sie in Magento häufig für Attribut-Sets oder Layout-Konfiguration vorkommen, lohnt es sich, gezielt zu prüfen, an welcher Stelle tatsächlich geschrieben wird, statt pauschal anzunehmen, dass jede Modifikation die komplette Struktur kopiert. Ein gezieltes Redesign, das häufig geänderte Werte in ein separates, kleineres Array auslagert, kann die Kopierkosten spürbar senken.
7. Copy-on-Write praktisch sichtbar machen
Um Copy-on-Write im eigenen Code sichtbar zu machen, eignet sich die Xdebug-Funktion xdebug_debug_zval(), die für eine gegebene Variable unter anderem den aktuellen Referenzzähler ausgibt. Ein Wert von refcount=1 zeigt an, dass die Variable die einzige Referenz auf ihre Struktur ist, ein Wert größer als eins zeigt, dass eine Kopie bei der nächsten Schreiboperation ausgelöst würde. Diese Funktion ist ausschließlich für die Entwicklungsumgebung mit installiertem Xdebug gedacht und sollte nicht in Produktionscode verwendet werden.
Alternativ liefert ein einfacher Vorher-Nachher-Vergleich mit memory_get_usage(), wie im Codebeispiel aus Abschnitt drei gezeigt, ein verlässliches Bild davon, ob und wann eine echte Kopie stattfindet, ganz ohne Xdebug-Abhängigkeit. Für die Analyse in Produktionsumgebungen sind Profiler wie Blackfire oder XHProf besser geeignet, weil sie den Speicherverbrauch nach Funktionsaufruf aufschlüsseln und so zeigen, an welcher Stelle im Code unerwartete Array-Kopien tatsächlich entstehen.
Ein praktischer Test-Ansatz für Codebasen mit vielen Funktionsaufrufen: Ein großes Testarray erzeugen, es durch die relevante Funktionskette schicken, und den Speicherverbrauch vor und nach dem gesamten Durchlauf vergleichen. Bleibt der Speicherverbrauch stabil, arbeitet die Kette ausschließlich lesend. Ein sprunghafter Anstieg an einer bestimmten Stelle deutet zuverlässig auf eine Schreiboperation und damit eine ausgelöste Kopie hin.
8. Typische Fallstricke in der Praxis
Ein häufiger Fallstrick ist die scheinbar harmlose Verwendung von foreach ($array as &$value) mit einer Referenz auf das Iterationselement. Nach Abschluss dieser Schleife bleibt $value als Referenz auf das letzte Element des Arrays bestehen, was bei einer nachfolgenden zweiten foreach-Schleife ohne erneute Referenzdeklaration zu subtilen Bugs führt, bei denen das letzte Element versehentlich überschrieben wird. Ein unset($value) direkt nach der referenzierten Schleife verhindert dieses klassische Problem.
Ein zweiter Fallstrick betrifft Funktionen, die ein großes Array unnötig durch mehrere Zwischenschritte reichen, wobei jeder Schritt eine kleine, aber tatsächliche Modifikation vornimmt. Jede dieser Modifikationen löst eine vollständige Kopie des gesamten Arrays aus, selbst wenn nur ein einzelnes Element verändert wird. Bei einer Verarbeitungskette mit fünf solcher Schritte entstehen dadurch fünf vollständige Kopien eines möglicherweise sehr großen Arrays, wo eine einzige, gebündelte Modifikation genügt hätte.
declare(strict_types=1);
// WRONG: five separate mutations trigger five full array copies
function processInefficient(array $orders): array
{
$orders = addTaxField($orders); // copy #1
$orders = addDiscountField($orders); // copy #2
$orders = addShippingField($orders); // copy #3
$orders = addStatusField($orders); // copy #4
$orders = addTimestampField($orders); // copy #5
return $orders;
}
// RIGHT: bundle all mutations into a single pass, one copy total
function processEfficient(array $orders): array
{
foreach ($orders as $key => $order) {
$orders[$key]['tax'] = calculateTax($order);
$orders[$key]['discount'] = calculateDiscount($order);
$orders[$key]['shipping'] = calculateShipping($order);
$orders[$key]['status'] = 'processed';
$orders[$key]['processed_at'] = time();
}
return $orders; // single copy-on-write trigger for the whole batch
}
Der entscheidende Unterschied im zweiten Beispiel: Alle Modifikationen finden innerhalb einer einzigen Schleife statt, die den Referenzzähler nur einmal aufbricht, statt fünfmal hintereinander eine vollständige Kopie eines potenziell großen Arrays zu erzeugen.
9. Copy-on-Write im direkten Vergleich
Ein direkter Vergleich zeigt, welche Operationen Copy-on-Write auslösen und welche nicht, und was das für die Praxis bedeutet.
| Operation | Löst Kopie aus? | Kosten | Empfehlung |
|---|---|---|---|
| $b = $a (Zuweisung) | Nein | Nur refcount erhöht | Bedenkenlos verwenden |
| Array als Funktionsparameter | Nein, außer bei Schreiben | refcount, keine Kopie beim Lesen | Große Arrays ohne Sorge übergeben |
| $b[] = 'x' nach Zuweisung | Ja | Vollständige Kopie, linear zur Größe | Modifikationen bündeln |
| $b = &$a (Referenz) | Nie, aber teilt Zustand dauerhaft | Kein Kopieraufwand, aber Nebenwirkungen | Nur bei bewusstem In-Place-Update |
| foreach ($a as $v) (Lesen) | Nein | Kein Overhead | Iteration ist immer günstig |
Der Vergleich verdeutlicht das zentrale Muster: Lesen ist bei Copy-on-Write praktisch immer kostenlos, Schreiben löst eine Kopie aus, deren Kosten linear mit der Arraygröße wachsen. Referenzen umgehen dieses Verhalten vollständig, erkaufen sich das aber mit weniger vorhersagbarem Code.
10. Zusammenfassung
Copy-on-Write bei PHP-Arrays sorgt dafür, dass Zuweisungen, Funktionsübergaben und das Speichern in weiteren Strukturen praktisch kostenlos bleiben, solange ausschließlich gelesen wird. Der interne Referenzzähler der zend_array-Struktur entscheidet, ob eine physische Kopie nötig ist. Erst die erste tatsächliche Schreiboperation auf einer geteilten Struktur löst die vollständige, linear mit der Größe skalierende Kopie aus.
Wer diesen Mechanismus versteht, kann große Arrays bedenkenlos an lesende Funktionen übergeben, sollte aber bei Modifikationsketten mit mehreren aufeinanderfolgenden Schreiboperationen gezielt bündeln, um wiederholte, unnötige Kopien zu vermeiden. Referenzen mit & sind das richtige Werkzeug für echtes In-Place-Update, heben Copy-on-Write für die betroffenen Variablen aber dauerhaft auf und sollten deshalb gezielt und sparsam eingesetzt werden.
Copy-on-Write bei PHP-Arrays, das Wichtigste auf einen Blick
Zuweisung ist praktisch kostenlos
$b = $a erhöht nur den refcount der zend_array-Struktur, unabhängig von der Arraygröße.
Schreiben löst die echte Kopie aus
Sobald refcount größer eins ist, erzeugt die erste Schreiboperation eine vollständige, unabhängige Kopie.
Referenzen heben Copy-on-Write auf
$b = &$a bindet Variablen dauerhaft aneinander, kein Kopieraufwand, aber gemeinsamer Zustand.
Modifikationen bündeln
Mehrere aufeinanderfolgende Schreiboperationen in einer Schleife zusammenfassen statt mehrfach zu kopieren.
11. FAQ: Copy-on-Write bei PHP-Arrays
1Was ist Copy-on-Write bei PHP-Arrays?
2Kostet $b = $a Speicher bei großen Arrays?
3Löst eine Funktionsübergabe eine Kopie aus?
4Wie hebt eine Referenz Copy-on-Write auf?
5Warum kostet Schreiben mehr als Lesen?
6Was passiert bei mehreren Schreiboperationen?
7Warum foreach mit Referenz vorsichtig einsetzen?
8Betrifft das auch verschachtelte Arrays?
9Wie mache ich Copy-on-Write sichtbar?
10Wie vermeide ich unnötige Kopien?
Mironsoft
PHP Performance-Analyse, Array-Verarbeitung und Speicher-Optimierung
Unerwartete Kopien in eurer Array-Verarbeitung finden?
Wir analysieren Verarbeitungsketten mit großen Arrays, identifizieren wiederholte Copy-on-Write-Trigger und bündeln Modifikationen für spürbar geringeren Speicherverbrauch.
Array-Performance-Audit
Identifikation unnötiger Kopien in Verarbeitungsketten mit memory_get_usage-Analysen
Refactoring von Modifikationsketten
Bündelung mehrfacher Schreiboperationen zu einem einzigen Copy-on-Write-Trigger
Referenz-Strategie
Gezielter Einsatz von Referenzen für In-Place-Updates ohne unvorhersehbare Nebeneffekte