On Tue, Jun 2, 2026, at 8:11 AM, Go Kudo wrote:
> 2026年5月17日(日) 0:19 Go Kudo <[email protected]>:
>> Hi internals,
>> 
>> I'd like to start the discussion for a new RFC, OPcache Static Cache.
>> 
>> RFC: https://wiki.php.net/rfc/opcache_static_cache
>> Implementation: https://github.com/php/php-src/pull/22052
>> 
>> The proposal adds an OPcache-managed shared-memory cache for explicit 
>> userland values and for selected PHP static state. It introduces explicit 
>> functions under the OPcache namespace (volatile_* and persistent_*) and two 
>> attributes, #[OPcache\VolatileStatic] and #[OPcache\PersistentStatic], that 
>> let selected static properties and method static variables survive across 
>> requests. The feature is disabled by default and only activates once memory 
>> is allocated through the new INI directives.
>> 
>> The RFC covers the motivation, the deliberate split between the two 
>> backends, the trust model (one PHP runtime = one trust domain; this is not a 
>> tenant isolation boundary), and benchmarks against APCu on NTS php-fpm and 
>> ZTS FrankenPHP. The PR is the full implementation, with PHPT coverage 
>> summarized in the Validation section.
>> 
>> One thing to flag on the implementation status: the Windows build is 
>> currently broken. I don't have a Windows development environment available 
>> yet — one is being arranged through work, and I'll get the Windows side 
>> fixed once that's in place.
>> 
>> Feedback welcome.
>> 
>> Best Regards,
>> Go Kudo
>
>  Hi Nicolas, Jakub, Timo, Larry
>
> I update RFC and Implementation:
> RFC: https://wiki.php.net/rfc/opcache_static_cache
> PR: https://github.com/php/php-src/pull/22052

I'm only responding to bits here and there, because the LLM text here is just 
too much for me to bother reading.  (Frankly, your non-LLM follow up message 
was perfectly readable to me.  I don't think you need it.)

It also seems like you're rewriting the RFC every time someone posts a comment. 
 There are differences of opinion on the list, so it will be less work for you 
and everyone else to slow down and let more people comment before you start 
making radical changes.

> ## The resulting public API
>
> For reference, here is the shape the explicit API settled into, summarised
> from the stub:
>
> ```php
> namespace OPcache;
>
> // Explicit cache: two final classes, static methods only, no instances.
> final class VolatileCache
> {
>     public static function get(string $key, 
> null|bool|int|float|string|array|object $default = null): 
> null|bool|int|float|string|array|object;
>     public static function getMultiple(array $keys, ?array $default = 
> null): array|false;
>     public static function set(string $key, 
> null|bool|int|float|string|array|object $value, int $ttl = 0): bool;
>     public static function setMultiple(array $values, int $ttl = 0): 
> bool;
>     public static function has(string $key): bool;
>     public static function delete(string $key_or_class): bool;
>     public static function deleteMultiple(array $keys): bool;
>     public static function clear(): bool;
>     public static function lock(string $key, int $lease = 0): bool;
>     public static function unlock(string $key): bool;
>     public static function getCacheStoreType(string $key_or_property, 
> ?string $class_name = null): CacheStoreType;
>     public static function info(): StaticCacheInfo;
> }
>
> // PinnedCache is the same set, except set()/setMultiple() take no $ttl,
> // plus two atomic counters:
> final class PinnedCache
> {
>     // get/getMultiple/set/setMultiple/has/delete/deleteMultiple/clear/
>     // lock/unlock/getCacheStoreType/info  -- as above
>     public static function increment(string $key, int $step = 1): int|false;
>     public static function decrement(string $key, int $step = 1): int|false;

No int|false.  That's an anti-pattern.  If you must do "int or error", at the 
very least use null here.

> }
>
> // getCacheStoreType() reports how a value is stored, without decoding it:
> enum CacheStoreType
> {
>     case NotFound;          // no entry for the key/property
>     case Scalar;            // stored inline
>     case SharedGraph;       // zero-copy graph laid out in SHM (the fast path)
>     case OPcacheSerialized; // OPcache binary serializer (SHM-safe, no 
> userland)
>     case PHPSerialized;     // php_var_serialize() last resort
> }
>
> // Declarative static state, over the same storage:
> #[Attribute] final class VolatileStatic {
>     public function __construct(int $ttl = 0, CacheStrategy $strategy = 
> CacheStrategy::Immediate);
> }
> #[Attribute] final class PinnedStatic {}
> enum CacheStrategy: int { case Immediate = 0; case Tracking = 1; }
>
> // Status object and the single exception type:
> final readonly class StaticCacheInfo { /* enabled, available, 
> configured_memory, entry_count, ... */ }
> class StaticCacheException extends \Exception {}
> ```
>
> Two final classes with static methods, no instances and no shared
> interface. Misses and contention return the default or `false`; genuine
> backend failures return `false` (or `int|false` for the atomic counters);
> `Closure` and resource values are rejected with a `TypeError`; and
> `StaticCacheException` is reserved for strict `#[OPcache\PinnedStatic]`
> publication.

I want to be clear on this: I will absolutely vote against this proposal if it 
ships with static methods as the API, no matter what else it contains.  That is 
a horrible anti-pattern and it should not be brought anywhere close to PHP's 
stdlib.  No.  Absolutely not.

Nicolas' original proposal was for regular objects, with a factory method.  I 
also prefer a regular object, but I'd go a step further:

$volatile = new VolatileCache('some_key');
$volatile->set('key', $val);

Pass a "scoping key" (or namespace, or prefix, or whatever you want to call it) 
to the constructor of the cache objects.  In most cases, frameworks (like 
Symfony or Laravel) already have an app-key value that is unique to the app 
instance, and that can be used probably directly.  That provides a clear 
separation between different cache pools; even if you have a multi-tenant setup 
such as apache2, using different random strings for the scoping key will keep 
the values separate.  

That gives us a clear separation, and justification for shipping on-by-default.

(This is what I meant earlier when talking about "pools."  That's essentially 
what this is.)

Also: I really don't like the name "pinned."  The opposite of "Volatile" is 
usually "stable".  That's less misleading than "persistent" (the original 
name), but also less confusing than "pinned", which means nothing here.

> ## References and the silent fallback

I honestly didn't follow this section.  Probably because of the LLM.

> ## Scalars and arrays-of-scalars only
>
> This is where the discussion helped most. I argued before that scalars-only
> gave up a real win; you pushed back with measurements; so I built your setup
> and measured it properly, including the large nested workloads that are the
> actual case for a cache. You were right that native was losing. That sent me
> into the implementation, and I found the cause and fixed it. The path is
> worth setting out.
>
> Two of your framings I agree with up front:
>
> 1. For array-of-scalars config/metadata, an immutable interned array is
>    essentially free, and the cache should not claim to beat it.
> 2. The "Nx faster than APCu" headline is size-dependent; APCu is only a few
>    microseconds for small payloads.

I can see Nicolas' argument for scalar/arrays-only, but I also agree that does 
greatly limit its usefulness.  You would need to spend a great deal of effort 
building an object facade for that data in many cases.  That's going to eat up 
a large chunk of the benefit (in both dev time and run time) of this feature.

Put another way, if I can just build up a data structure on a property, stick 
an attribute on it, and then always use it like:

$data = self::$data ??= compute_data();

And move on with life, that's huge for DX, even if it may be slightly slower 
than taking the time to compile an array form of it, save it to disk (and worry 
about file permissions and writeability), reload it, and then rehydrate to 
objects, potentially.  Frankly, I'd take that tradeoff more often than not.

> ### (d) Not just performance
>
> This does not rest on performance alone. Object support is also useful for
> being built in and generic (no third-party extension, nothing to pre-generate)
> and for being one primitive: the store side and the runtime cross-worker
> sharing live in the same place, instead of "cache the array" plus "hydrate in
> userland" wired together by every library. And the safe-direct registry is not
> a userland protocol: a plain user object with no magic and no cycles or refs
> takes the fast path automatically via `can_restore_direct()`, and the C-only
> registry only covers a few internal classes whose state the generic path
> cannot read. Keeping objects imposes nothing on the ecosystem.

Right, that.  The simplicity of the userland code is the big win for me, even 
if it's single-digit-percent slower than manually materializing in some cases.

> ## Dropping pinned (and the attributes)
>
>> PinnedStatic on the Carbon shape is ~1.5 us [...] there's no preload
>> trick that reaches that number, because preload can't bake a live object
>> graph into an opcode literal
>
> Pinned is the one place a live-object representation still wins clearly, for a
> reason the volatile numbers above do not capture. Pinned (and
> `#[PinnedStatic]`) materialize the graph once per worker; after that it is a
> plain static read on every subsequent request in that worker, near zero per
> request. The hydration approach pays its hydrate cost on every request 
> instead.
> preload cannot reach this either: it can only intern scalar and array
> literals, not bake a live object graph into an opcode literal.
>
> The caveat is that this holds for read-only / immutable shared state, where
> keeping one live instance across requests is correct; a mutable shared 
> instance
> would leak between requests. But that is a real and common case: a compiled DI
> container, a routing table, config value objects. Your request-registry 
> counter
> rebuilds per request from the cache, so it does not reach the per-worker
> amortization, and for the read-only data where it would help, pinned already
> does it with less per-request cost.
>
> The attributes are the ergonomic surface over that same mechanism, so I would
> keep them in this RFC rather than split them out. They add no new storage
> model; they remove the explicit store/fetch boilerplate for the static-state
> case.

I would prefer to keep these in rather than remove them, but I wouldn't vote 
against the RFC if the consensus is eventually to remove them until later.

--Larry Garfield

Reply via email to