From 133b195d290cdd1197122cffda458761bf78109a Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 22:15:05 +0000 Subject: [PATCH] Share the observed keys of whole-variable writes and walk the smaller set Every write replacing a whole variable observes and kills the read keys of all its slots, and compileAccesses() copied them into each such write: quadratic in the writes and reads of a variable. They are now built once per variable and shared. Removing them, the markers of a variable and the observed keys from the live set walks the smaller of the two sets, like the per-slot markers already did. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ad6c-dec2-72ff-bbe8-5b3b62746c94 --- src/Analyser/VariableLivenessResolver.php | 104 ++++++---- turbo-ext/src/VariableLivenessResolver.cpp | 187 ++++++++++++------ .../src/generated/VariableLivenessResolver.h | 34 ++-- 3 files changed, 217 insertions(+), 108 deletions(-) diff --git a/src/Analyser/VariableLivenessResolver.php b/src/Analyser/VariableLivenessResolver.php index ee119a3c98c..24225d45efa 100644 --- a/src/Analyser/VariableLivenessResolver.php +++ b/src/Analyser/VariableLivenessResolver.php @@ -12,6 +12,9 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use Throwable; +use function array_diff_key; +use function array_fill_keys; +use function array_key_exists; use function array_keys; use function array_pop; use function array_reverse; @@ -89,6 +92,9 @@ final class VariableLivenessResolver /** @var array>> variable name => replaced slot => marker => id of the write leaving it */ private array $markersBySlot = []; + /** @var array> variable name => marker => id of the write leaving it */ + private array $markersByName = []; + /** @var array */ private array $overwrittenIds = []; @@ -264,9 +270,7 @@ private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContex $this->observeOverwrite($item, $next); } } - foreach (array_keys($this->killedKeys[$id] ?? []) as $key) { - unset($next[$key]); - } + $next = self::withoutKeys($next, $this->killedKeys[$id] ?? []); $next = $this->killOverwriteMarkers($flow->write, $next); if (isset($this->overwriteMarkers[$id])) { $next[$this->overwriteMarkers[$id]] = true; @@ -532,6 +536,14 @@ private function compileAccesses(): void $this->nameKeys[$name][$key] = true; } } + // the keys of every slot, shared by all writes of the whole variable + // instead of copied into each of them + $allKeys = []; + foreach ($keysBySlot as $keys) { + $allKeys += $keys; + } + $offsetKeys = array_diff_key($allKeys, $keysBySlot['container'] ?? []); + $allKills = array_fill_keys(array_keys($allKeys), true); foreach ($accesses as $access) { $write = $access->write; if ($write === null) { @@ -540,25 +552,21 @@ private function compileAccesses(): void $id = $write->getId(); $offset = $write->getOffset(); if ($write->isOffsetWrite() && $offset !== null) { - $slot = self::offsetKey($offset); - $selectedKeys = [$slot => $keysBySlot[$slot] ?? []]; + $keys = $keysBySlot[self::offsetKey($offset)] ?? []; + $kills = $write->replacesOffset(); } else { - $selectedKeys = $keysBySlot; - if ($write->isOffsetWrite()) { - unset($selectedKeys['container']); - } + $keys = $write->isOffsetWrite() ? $offsetKeys : $allKeys; + $kills = !$write->isOffsetWrite(); } - $kills = !$write->isOffsetWrite() || ($offset !== null && $write->replacesOffset()); - foreach ($selectedKeys as $keys) { - foreach ($keys as $key => $targetId) { - $this->observedKeys[$id][$key] = $targetId; - if (!$kills) { - continue; - } - - $this->killedKeys[$id][$key] = true; - } + if ($keys === []) { + continue; } + $this->observedKeys[$id] = isset($this->observedKeys[$id]) ? $this->observedKeys[$id] + $keys : $keys; + if (!$kills) { + continue; + } + $killedKeys = $keys === $allKeys ? $allKills : array_fill_keys(array_keys($keys), true); + $this->killedKeys[$id] = isset($this->killedKeys[$id]) ? $this->killedKeys[$id] + $killedKeys : $killedKeys; } $this->compileOverwrites($name, $accesses); } @@ -597,6 +605,11 @@ private function compileOverwrites(string $name, array $accesses): void } $this->markersBySlot[$name] = $markersBySlot; + $markersOfName = []; + foreach ($markersBySlot as $markers) { + $markersOfName += $markers; + } + $this->markersByName[$name] = $markersOfName; } /** The slot a write replaces: '' for the whole variable, null when it keeps the old value. */ @@ -616,7 +629,19 @@ private static function replacedSlot(VariableWrite $write): ?string /** @param array $next */ private function observeWrite(int $id, array $next): void { - foreach ($this->observedKeys[$id] ?? [] as $key => $targetId) { + $observedKeys = $this->observedKeys[$id] ?? []; + // walk the smaller of the two sets + if (count($observedKeys) > count($next)) { + $live = []; + foreach (array_keys($next) as $key) { + if (!array_key_exists($key, $observedKeys)) { + continue; + } + $live[$key] = $observedKeys[$key]; + } + $observedKeys = $live; + } + foreach ($observedKeys as $key => $targetId) { if (!isset($next[$key])) { continue; } @@ -689,23 +714,34 @@ private function killOverwriteMarkers(VariableWrite $write, array $next): array if ($slot === null) { return $next; } - foreach ($markersBySlot as $markerSlot => $markers) { - if ($slot !== '' && $markerSlot !== $slot) { - continue; + if ($slot === '') { + return self::withoutKeys($next, $this->markersByName[$write->getVariableName()]); + } + + return self::withoutKeys($next, $markersBySlot[$slot] ?? []); + } + + /** + * Walks the smaller of the two sets. + * + * @param array $next + * @param array $keys + * @return array + */ + private static function withoutKeys(array $next, array $keys): array + { + if (count($keys) <= count($next)) { + foreach (array_keys($keys) as $key) { + unset($next[$key]); } - // walk the smaller of the two sets - if (count($markers) <= count($next)) { - foreach (array_keys($markers) as $marker) { - unset($next[$marker]); - } + + return $next; + } + foreach (array_keys($next) as $key) { + if (!array_key_exists($key, $keys)) { continue; } - foreach (array_keys($next) as $key) { - if (!isset($markers[$key])) { - continue; - } - unset($next[$key]); - } + unset($next[$key]); } return $next; diff --git a/turbo-ext/src/VariableLivenessResolver.cpp b/turbo-ext/src/VariableLivenessResolver.cpp index 4f55fcede0a..2366ed3857f 100644 --- a/turbo-ext/src/VariableLivenessResolver.cpp +++ b/turbo-ext/src/VariableLivenessResolver.cpp @@ -358,6 +358,58 @@ zv::Val unionOf(const zv::Val &a, const zv::Val &b) return unionOf(a, tableOf(b)); } +/* Mirrors withoutKeys(): unset($next[$key]) for every key of $keys, + * walking the smaller of the two sets */ +void withoutKeys(zv::Val &next, HashTable *keys) +{ + if (keys == NULL || zend_hash_num_elements(keys) == 0) return; + if (zend_hash_num_elements(keys) <= countOf(next)) { + for (auto entry : zv::TableRef(keys)) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(next, key); + } + } + return; + } + /* a snapshot to walk while the live set loses keys */ + zv::Val live = shareArray(next); + for (auto entry : zv::TableRef(tableOf(live))) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL || !zend_hash_exists(keys, key)) continue; + unsetKey(next, key); + } +} + +/* $array[$index] = isset($array[$index]) ? $array[$index] + $keys : $keys + * ($keys shared, not copied, when the entry is new) */ +void addKeysAt(zv::Val &array, zend_long index, const zv::Val &keys) +{ + SEPARATE_ARRAY(array.raw()); + zval *slot = zend_hash_index_find(Z_ARRVAL_P(array.raw()), (zend_ulong) index); + if (slot == NULL) { + zval shared; + ZVAL_COPY(&shared, const_cast(keys).raw()); + zend_hash_index_add_new(Z_ARRVAL_P(array.raw()), (zend_ulong) index, &shared); + return; + } + SEPARATE_ARRAY(slot); + zend_hash_merge(Z_ARRVAL_P(slot), tableOf(keys), zval_add_ref, 0); +} + +/* array_fill_keys(array_keys($keys), true) */ +zv::Val trueForKeys(const zv::Val &keys) +{ + zv::Val result = emptyArray(); + for (auto entry : zv::TableRef(tableOf(keys))) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + setTrue(result, key); + } + } + return result; +} + bool isTrueAt(const zv::Val &array, zend_string *key) { return zend_hash_exists(tableOf(array), key); @@ -549,6 +601,7 @@ class VariableLivenessResolver zv::Val coveredIds = emptyArray(); zv::Val overwriteMarkers = emptyArray(); zv::Val markersBySlot = emptyArray(); /* name => slot => marker => writer id */ + zv::Val markersByName = emptyArray(); /* name => marker => writer id */ zv::Val overwrittenIds = emptyArray(); zv::Val allReadKeys = emptyArray(); zv::Val loopStatements = emptyArray(); @@ -827,15 +880,7 @@ class VariableLivenessResolver } } } - HashTable *killed = innerTable(killedKeys, id); - if (killed != NULL) { - for (auto entry : zv::TableRef(killed)) { - zend_string *key = entry.stringKeyOrNull(); - if (key != NULL) { - unsetKey(next, key); - } - } - } + withoutKeys(next, innerTable(killedKeys, id)); killOverwriteMarkers(view, next); zval *marker = zend_hash_index_find(tableOf(overwriteMarkers), (zend_ulong) id); if (marker != NULL) { @@ -1366,6 +1411,26 @@ class VariableLivenessResolver zend_hash_update(Z_ARRVAL_P(byName), key.get(), &trueValue); } } + /* the keys of every slot, shared by all writes of the whole + * variable instead of copied into each of them */ + zv::Val allKeys = emptyArray(); + for (auto slotEntry : zv::TableRef(tableOf(keysBySlot))) { + zv::Ref keys = slotEntry.value().deref(); + if (!keys.isArray()) continue; + SEPARATE_ARRAY(allKeys.raw()); + zend_hash_merge(Z_ARRVAL_P(allKeys.raw()), keys.asArrayTable(), zval_add_ref, 0); + } + zv::Val offsetKeys = shareArray(allKeys); + HashTable *containerKeys = innerTableByKey(keysBySlot, containerSlot); + if (containerKeys != NULL) { + for (auto keyEntry : zv::TableRef(containerKeys)) { + zend_string *key = keyEntry.stringKeyOrNull(); + if (key != NULL) { + unsetKey(offsetKeys, key); + } + } + } + zv::Val allKills = trueForKeys(allKeys); for (auto entry : zv::TableRef(accessTable)) { zend_object *access = Z_OBJ_P(entry.value().deref().raw()); zval *write = slot(access, slots.accessWrite, "write"); @@ -1374,38 +1439,28 @@ class VariableLivenessResolver WriteView view; if (UNEXPECTED(!view.load(write))) return false; zend_long id = view.id; - bool offsetIsNull = view.offsetIsNull; - zv::Val selectedKeys; - if (view.offsetWrite && !offsetIsNull) { + zv::Val keys; + bool kills; + bool whole = false; + if (view.offsetWrite && !view.offsetIsNull) { zv::Str slotName = offsetKey(view.offset.raw()); - selectedKeys = emptyArray(); - HashTable *keys = innerTableByKey(keysBySlot, slotName.get()); - zval keysValue = (keys != NULL ? zv::Arr::copyOfTable(keys) : zv::Arr::empty()).take(); - SEPARATE_ARRAY(selectedKeys.raw()); - zend_hash_update(Z_ARRVAL_P(selectedKeys.raw()), slotName.get(), &keysValue); + HashTable *slotKeys = innerTableByKey(keysBySlot, slotName.get()); + zval keysValue = (slotKeys != NULL ? zv::Arr::copyOfTable(slotKeys) : zv::Arr::empty()).take(); + keys = zv::Val::adopt(keysValue); + kills = view.replacesOffset; } else { - selectedKeys = shareArray(keysBySlot); - if (view.offsetWrite) { - unsetKey(selectedKeys, containerSlot); - } + whole = !view.offsetWrite; + keys = shareArray(whole ? allKeys : offsetKeys); + kills = whole; } - bool kills = !view.offsetWrite || (!offsetIsNull && view.replacesOffset); - for (auto slotEntry : zv::TableRef(tableOf(selectedKeys))) { - zv::Ref keys = slotEntry.value().deref(); - if (!keys.isArray()) continue; - for (auto keyEntry : zv::TableRef(keys.asArrayTable())) { - zend_string *key = keyEntry.stringKeyOrNull(); - if (key == NULL) continue; - zval *observed = innerSlot(observedKeys, id); - Z_TRY_ADDREF_P(keyEntry.value().raw()); - zend_hash_update(Z_ARRVAL_P(observed), key, keyEntry.value().raw()); - if (!kills) continue; - - zval *killed = innerSlot(killedKeys, id); - zval trueValue; - ZVAL_TRUE(&trueValue); - zend_hash_update(Z_ARRVAL_P(killed), key, &trueValue); - } + if (countOf(keys) == 0) continue; + addKeysAt(observedKeys, id, keys); + if (!kills) continue; + + if (whole) { + addKeysAt(killedKeys, id, allKills); + } else { + addKeysAt(killedKeys, id, trueForKeys(keys)); } } if (UNEXPECTED(!compileOverwrites(name, accessTable))) return false; @@ -1452,6 +1507,16 @@ class VariableLivenessResolver zval *stored = innerSlotByKey(markersBySlot, name); zval_ptr_dtor(stored); ZVAL_COPY(stored, slotMarkers.raw()); + zv::Val markersOfName = emptyArray(); + for (auto slotEntry : zv::TableRef(tableOf(slotMarkers))) { + zv::Ref markers = slotEntry.value().deref(); + if (!markers.isArray()) continue; + SEPARATE_ARRAY(markersOfName.raw()); + zend_hash_merge(Z_ARRVAL_P(markersOfName.raw()), markers.asArrayTable(), zval_add_ref, 0); + } + zval *storedOfName = innerSlotByKey(markersByName, name); + zval_ptr_dtor(storedOfName); + ZVAL_COPY(storedOfName, markersOfName.raw()); return true; } @@ -1521,30 +1586,15 @@ class VariableLivenessResolver if (bySlot == NULL) return; zv::Str slotName; if (!replacedSlot(write, slotName)) return; - bool whole = ZSTR_LEN(slotName.get()) == 0; - for (auto slotEntry : zv::TableRef(bySlot)) { - zend_string *markerSlot = slotEntry.stringKeyOrNull(); - if (markerSlot == NULL) continue; - if (!whole && !zend_string_equals(markerSlot, slotName.get())) continue; - zv::Ref markersRef = slotEntry.value().deref(); - if (!markersRef.isArray()) continue; - HashTable *markers = markersRef.asArrayTable(); - if (zend_hash_num_elements(markers) <= countOf(next)) { - for (auto markerEntry : zv::TableRef(markers)) { - zend_string *marker = markerEntry.stringKeyOrNull(); - if (marker == NULL) continue; - unsetKey(next, marker); - } - continue; - } - /* a snapshot to walk while the live set loses keys */ - zv::Val live = shareArray(next); - for (auto entry : zv::TableRef(tableOf(live))) { - zend_string *key = entry.stringKeyOrNull(); - if (key == NULL || !zend_hash_exists(markers, key)) continue; - unsetKey(next, key); - } + if (ZSTR_LEN(slotName.get()) == 0) { + withoutKeys(next, innerTableByKey(markersByName, write.nameStr)); + return; } + zval *markers = zend_hash_find(bySlot, slotName.get()); + if (markers == NULL) return; + ZVAL_DEREF(markers); + if (Z_TYPE_P(markers) != IS_ARRAY) return; + withoutKeys(next, Z_ARRVAL_P(markers)); } /* Mirrors observeWrite(). */ @@ -1552,6 +1602,21 @@ class VariableLivenessResolver { HashTable *observed = innerTable(observedKeys, id); if (observed == NULL) return; + /* walk the smaller of the two sets */ + zv::Val live; + if (zend_hash_num_elements(observed) > countOf(next)) { + live = emptyArray(); + for (auto entry : zv::TableRef(tableOf(next))) { + zend_string *key = entry.stringKeyOrNull(); + if (key == NULL) continue; + zval *targetId = zend_hash_find(observed, key); + if (targetId == NULL) continue; + SEPARATE_ARRAY(live.raw()); + Z_TRY_ADDREF_P(targetId); + zend_hash_update(Z_ARRVAL_P(live.raw()), key, targetId); + } + observed = tableOf(live); + } for (auto entry : zv::TableRef(observed)) { zend_string *key = entry.stringKeyOrNull(); if (key == NULL || !isTrueAt(next, key)) continue; diff --git a/turbo-ext/src/generated/VariableLivenessResolver.h b/turbo-ext/src/generated/VariableLivenessResolver.h index 9c62ee75255..421254b07fd 100644 --- a/turbo-ext/src/generated/VariableLivenessResolver.h +++ b/turbo-ext/src/generated/VariableLivenessResolver.h @@ -29,15 +29,16 @@ inline constexpr uint32_t literalItems = 15; inline constexpr uint32_t coveredIds = 16; inline constexpr uint32_t overwriteMarkers = 17; inline constexpr uint32_t markersBySlot = 18; -inline constexpr uint32_t overwrittenIds = 19; -inline constexpr uint32_t allReadKeys = 20; -inline constexpr uint32_t loopStatements = 21; -inline constexpr uint32_t ownWriteIds = 22; -inline constexpr uint32_t variableOverwritingLoops = 23; -inline constexpr uint32_t opaque = 24; -inline constexpr uint32_t readsAllVariables = 25; -inline constexpr uint32_t allNamesMentioned = 26; -inline constexpr uint32_t returnsByReference = 27; +inline constexpr uint32_t markersByName = 19; +inline constexpr uint32_t overwrittenIds = 20; +inline constexpr uint32_t allReadKeys = 21; +inline constexpr uint32_t loopStatements = 22; +inline constexpr uint32_t ownWriteIds = 23; +inline constexpr uint32_t variableOverwritingLoops = 24; +inline constexpr uint32_t opaque = 25; +inline constexpr uint32_t readsAllVariables = 26; +inline constexpr uint32_t allNamesMentioned = 27; +inline constexpr uint32_t returnsByReference = 28; } // namespace slot inline void declareClass(reg::Class &cls) @@ -67,6 +68,7 @@ inline void declareProperties(reg::Class &cls) cls.property("coveredIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("overwriteMarkers", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("markersBySlot", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); + cls.property("markersByName", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("overwrittenIds", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("allReadKeys", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("loopStatements", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); @@ -113,8 +115,10 @@ inline constexpr char strings[] = "observeWrite\0" /* 376 */ "observeOverwrite\0" /* 389 */ "killOverwriteMarkers\0" /* 406 */ - "resolveDependencies\0" /* 427 */ - "resolveCoverage"; /* 447 */ + "keys\0" /* 427 */ + "withoutKeys\0" /* 432 */ + "resolveDependencies\0" /* 444 */ + "resolveCoverage"; /* 464 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 21), /* resolve $function */ reg::packed(49, MAY_BE_NULL, 54), /* resolve $flow */ @@ -150,6 +154,9 @@ inline constexpr reg::PackedArg args[] = { reg::packed(262, 0, 202), /* killOverwriteMarkers $write */ reg::packed(133, MAY_BE_ARRAY), /* killOverwriteMarkers $next */ reg::packed(84, MAY_BE_ARRAY), /* killOverwriteMarkers return */ + reg::packed(133, MAY_BE_ARRAY), /* withoutKeys $next */ + reg::packed(427, MAY_BE_ARRAY), /* withoutKeys $keys */ + reg::packed(84, MAY_BE_ARRAY), /* withoutKeys return */ reg::packed(84, MAY_BE_VOID), /* resolveDependencies return */ reg::packed(84, MAY_BE_VOID), /* resolveCoverage return */ }; @@ -171,8 +178,9 @@ inline constexpr sigtab::Sig replacedSlot = { { 360 /* replacedSlot */, 1, 23, 1 inline constexpr sigtab::Sig observeWrite = { { 376 /* observeWrite */, 2, 25, 2, 27, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig observeOverwrite = { { 389 /* observeOverwrite */, 2, 28, 2, 30, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig killOverwriteMarkers = { { 406 /* killOverwriteMarkers */, 2, 31, 2, 33, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveDependencies = { { 427 /* resolveDependencies */, 0, 34, 0, 34, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveCoverage = { { 447 /* resolveCoverage */, 0, 35, 0, 35, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig withoutKeys = { { 432 /* withoutKeys */, 2, 34, 2, 36, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolveDependencies = { { 444 /* resolveDependencies */, 0, 37, 0, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveCoverage = { { 464 /* resolveCoverage */, 0, 38, 0, 38, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::VariableLivenessResolver