[RFC] OPcache Static Cache Implementation#22052
Open
zeriyoshi wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RFC: https://wiki.php.net/rfc/opcache_static_cache
OPcache Static Cache
Summary
This PR adds OPcache Static Cache, an OPcache-managed shared-memory cache facility with two separately configured backends:
opcache.static_cache.volatile_size_mb, for recoverable process-shared cache entries and#[OPcache\VolatileStatic].opcache.static_cache.persistent_size_mb, for strict process-shared entries and#[OPcache\PersistentStatic].Both backends are disabled by default. A non-zero size reserves a dedicated OPcache SHM segment for that backend. The storage header and entry table are initialized eagerly, while payload pages are touched lazily on first allocation to avoid paying startup cost proportional to the full configured cache size.
For the RFC and more detailed design notes for this implementation, please see:
At a high level, this change includes:
OPcache\namespace.User-visible API
The explicit volatile API is:
OPcache\volatile_store()OPcache\volatile_store_array()OPcache\volatile_fetch()OPcache\volatile_fetch_array()OPcache\volatile_exists()OPcache\volatile_delete()OPcache\volatile_delete_array()OPcache\volatile_clear()OPcache\volatile_lock()OPcache\volatile_cache_info()The explicit persistent API is:
OPcache\persistent_store()OPcache\persistent_store_array()OPcache\persistent_fetch()OPcache\persistent_fetch_array()OPcache\persistent_exists()OPcache\persistent_delete()OPcache\persistent_delete_array()OPcache\persistent_clear()OPcache\persistent_lock()OPcache\persistent_atomic_increment()OPcache\persistent_atomic_decrement()OPcache\persistent_cache_info()The attribute API is:
#[OPcache\VolatileStatic(ttl: 0, strategy: OPcache\CacheStrategy::Immediate)]#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)]#[OPcache\PersistentStatic]OPcache\CacheStrategyOPcache\StaticCacheExceptionThe
OPcache\__DirectCacheSafemarker is internal-only. It is used to mark engine-vetted internal classes that OPcache can restore directly from its own encoded representation and copy through registered per-class handlers.API Contracts
Single-key APIs require non-empty string keys. Batch fetch/delete APIs accept arrays containing only non-empty strings or integers, and integer keys are converted without invoking userland code. Batch store APIs require non-empty string array keys. Invalid or empty keys throw
ValueError.Stored values may be
null,bool,int,float,string,array, orobject. Resources andClosureinstances are rejected during API validation or store preparation, including when they are reached through arrays, object properties,__serialize()result arrays,__sleep()selected properties, values published by*_store_array(), or static attributes.The error policy follows the intended usage of each backend:
bool, because pressure or temporary inability to cache is recoverable.PersistentStaticpublications throwOPcache\StaticCacheException, because failure means the strict persistent contract could not be satisfied.Single-key
*_fetch()calls keep request-local fetch state keyed by cache context, cache key, and mutation epoch. Successful fetches attempt to memoize a prototype zval slot reconstructed from the stored payload, and same-request hits copy from that slot when the value is supported by the request-local clone path. Object-free arrays keep PHP's ordinary copy-on-write behavior and avoid repeated PHP value graph reconstruction. Object-bearing values return a fresh object graph cloned from the request-local prototype by an internal path that does not invoke userland__clone, so object handles are not shared with values returned by earlier or later fetches. Ordinary PHP objects use OPcache's std-object clone helper, and engine-vettedOPcache\__DirectCacheSafeinternal objects use per-class copy handlers registered by their owning extension. Mutating a fetched object graph therefore does not mutate another fetched value, the request-local prototype, or the stored cache entry.The attribute API is intentionally more than syntactic sugar over
*_fetch(). Explicit key/value fetches must return an independent PHP value for each object-bearing fetch, so repeated object reads either reconstruct the PHP value graph from storage or clone from a request-local prototype using OPcache-controlled ordinary-object andOPcache\__DirectCacheSafecopy handlers. Attribute-backed static properties and method statics restore into the request's static slot once and ordinary reads use that slot directly. This meansOPcache\__DirectCacheSafeinternal state pays either the restore or prototype-copy cost at explicit-fetch time, but only the static-slot initialization cost for attribute-backed static reads.Storage Model
Each backend owns a separate storage context with its own SHM segment, lock file, entry table, allocator state, lookup cache, and status surface.
0disables the backend. Non-zero sizes are validated as system INI settings and cannot be changed after OPcache startup.Entries are stored in an open-addressed table. Payload storage uses a compact SHM allocator with free-list reuse, tail trimming, and compaction. The allocator can relocate ordinary key/string/serialized payload blocks, but shared-graph payloads are treated as immovable because decoded request values may hold pinned references to their graph representation.
A 64-bit mutation epoch is bumped by operations that can invalidate request-local observations: store, delete, clear, invalidation, compaction, and expiration cleanup. Mutation epochs are stored as
uint64_t. Epoch0is the initial state and is also used as the sentinel for uninitialized request-local lookup-cache entries. If incrementing the counter would wrap it back to0, OPcache advances it to1instead, so a freshly bumped epoch cannot be confused with the uninitialized state. Request-local lookup-cache entries and single-key fetch prototype slots are only reused while their epoch matches the current SHM header epoch.Locking and Fork Safety
The default process lock on Unix is a byte-range
fcntl()lock over a cache-specific lock file. The implementation uses blockingF_SETLKWfor read/write cache locks andF_SETLK/F_SETLKWfor entry reservation stripes where non-blocking behavior is required.In ZTS builds, process-local heap locks are layered on top of process locks so threads in the same worker serialize correctly without placing pthread mutex state in the shared mapping. The entry-lock state records the owning PID. After
fork(), a child drops inherited request-local reservation state, reinitializes the process-local ZTS entry locks, and does not release the parent's reservations during child shutdown.OPcache\*_lock($key)provides a request-retained reservation lock for single-builder patterns. Public store and persistent atomic mutators wait on the matching reservation before committing changes. Delete, clear, andopcache_reset()bypass reservation locks to avoid stripe deadlocks, so they are not barriers against later publishes by already-reserved builders. Referenced shared-graph payloads are retired and freed only after the last request releases them.Store and Fetch Safety
Store operations separate value preparation from the final SHM publish step. Snapshotting, shared-graph construction, and serialization preparation happen outside the cache write lock. The write lock is held only while the prepared payload is committed to SHM and the entry table is updated.
Fetch operations avoid PHP value graph reconstruction while holding the cache read lock:
Retired shared graphs are not freed while any request still holds a pin. A graph whose entry is overwritten or deleted is retired first, and the underlying SHM block is released only after the last request reference is gone.
Static State Integration
#[OPcache\VolatileStatic]and#[OPcache\PersistentStatic]can be applied to classes, methods, and properties. The implementation installs hooks for class static initialization, function static initialization, class static access, and class static update so attribute-backed static slots can be restored and published at the same points the engine creates or accesses the corresponding static storage.VolatileStaticsupports two strategies:Immediatepublishes the static root value when the root is assigned.Trackingtracks reachable arrays/objects and publishes the final dirty state at request shutdown.PersistentStaticuses the persistent backend and strict failure semantics. Capacity, encoding, and unsupported-value failures throwOPcache\StaticCacheExceptionat the assignment, mutation, or publication site. For array roots, the engine mutation hook observes mutations before copy-on-write separation and the static-cache code rechecks root identity before publishing, so mutations to local copies do not publish unrelated values.Class-level attributes use a class-blob path so static properties and dynamic method statics can be restored together when the class is accessed. Script invalidation and
opcache_reset()invalidate the associated static-cache keys.VM and JIT Integration
The VM gains a guarded mutation-hook mechanism:
EG(tracked_mutation_hooks_active)keeps the normal VM path cheap when no tracking is active.SEPARATE_ARRAY()where the original root identity is still observable.The JIT static-property fast path calls
zend_jit_static_prop_access_helper()even when the runtime cache already resolved the property slot. This keeps class-blob state refreshes consistent withzend_fetch_static_property_address(). The JIT path checks for exceptions after the helper call before continuing with the inlined static-property access.Serialization and Direct Restore
The OPcache serializer encodes scalar, array, object, and shared-reference structures into SHM-safe payloads. Decode paths copy header/state data into aligned local storage before reading fields, avoiding unaligned access to SHM bytes.
Some internal classes can be restored through direct OPcache-controlled handlers instead of userland serialization. This is limited to engine-vetted internal classes marked with the internal
OPcache\__DirectCacheSafeattribute and registered in OPcache's safe-direct handler table. ext-date and ext-spl expose small C-only getter functions that return const handler tables; the copy, unstorable-state detection, state serialization, and state unserialization callbacks remain private to the owning extension. This representation is tied to the current PHP build and is not an external persistence or interchange format.Test Coverage
The commit adds PHPT coverage for:
__clone, registered-handler copying forOPcache\__DirectCacheSafeobjects, lookup-cache behavior, mutation epochs, allocator reuse, fragmentation, relocation, and compaction.clear(),opcache_reset(), fork, and ZTS helper programs.VolatileStaticimmediate/tracking behavior,PersistentStatic,PersistentStaticfailure exceptions, class-level blobs, method statics, inherited attributes, readonly properties, preload, JIT, and script invalidation.Zend/tests.The test suite also includes C helper programs under
ext/opcache/tests/helpers/for fork/ZTS scenarios that are difficult to cover from a single PHPT process alone.Areas Where Focused Review Would Be Especially Helpful
I would especially appreciate review of the following correctness boundaries:
__clone, whileOPcache\__DirectCacheSafeobjects use registered extension-owned copy handlers for their internal state.