[PHP-DEV] [RFC] [Discussion] OPcache Static Cache

Hi internals and All OPcache Static Cache RFC discussion members.

I’ve made significant changes to the API to bring it to a form that most people will find acceptable.
Currently the implementation stub is here
https://github.com/colopl/php-src/blob/982a5f4d6bffd033f9c5a02586327c86fcef1cda/ext/opcache/opcache.stub.php

And thank you Nicolas, Hi’s more optimization and simplification bring better performance and simplism.
This means you ever have to worry about how values are stored anymore. Serializer is completely gone.
https://github.com/colopl/php-src/pull/4

I sent this email because I wanted to hear thoughts on whether
I should decide to remove the proposed implementation of Attribute based cache from this RFC.

Attribute-based caching is highly efficient and consistently delivers the best performance.
But, it also requires the OPcache JIT to intervene.

This requires a review by Derick, the developer of JIT, and must be handled with caution.
Furthermore, the argument that FrankenPHP should be used if that level of performance is required certainly makes sense.

Originally, I drafted this RFC with the goal of “improving performance as an extension of the existing PHP SAPI,”
so this would deviate from my initial objective. However, now that I realize there is a demand for a “more faster integrated APCu,”
I feel that this goal might need to be deprioritized.

This RFC also sparked a discussion about how security boundaries should be handled.
I’ve chosen to implement it in a way that SAPI isn’t enabled unless explicitly opted into
Do you think this meets the requirements? (I’d especially like to ask Jakub about this.)

To explain the process step by step, here is how I would like to proceed:

  1. Narrow the scope of this RFC, reach consensus and hold a vote as soon as possible, and ensure it can be incorporated into 8.6.
  2. Create a new RFC on Attribute-based Caching to initiate a new discussion (though this may not be ready in time for 8.6.)

Please share your opinions.

Best regards,
Go Kudo

2026年5月17日(日) 0:19 Go Kudo <zeriyoshi@gmail.com>:

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,

On Thu, Jun 4, 2026 at 6:49 PM Go Kudo <zeriyoshi@gmail.com> wrote:

Hi internals and All OPcache Static Cache RFC discussion members.

I’ve made significant changes to the API to bring it to a form that most people will find acceptable.
Currently the implementation stub is here
https://github.com/colopl/php-src/blob/982a5f4d6bffd033f9c5a02586327c86fcef1cda/ext/opcache/opcache.stub.php

And thank you Nicolas, Hi’s more optimization and simplification bring better performance and simplism.
This means you ever have to worry about how values are stored anymore. Serializer is completely gone.
https://github.com/colopl/php-src/pull/4

I sent this email because I wanted to hear thoughts on whether
I should decide to remove the proposed implementation of Attribute based cache from this RFC.

Attribute-based caching is highly efficient and consistently delivers the best performance.
But, it also requires the OPcache JIT to intervene.

This requires a review by Derick, the developer of JIT, and must be handled with caution.

I think you mean Dmitry.

Furthermore, the argument that FrankenPHP should be used if that level of performance is required certainly makes sense.

Originally, I drafted this RFC with the goal of “improving performance as an extension of the existing PHP SAPI,”
so this would deviate from my initial objective. However, now that I realize there is a demand for a “more faster integrated APCu,”
I feel that this goal might need to be deprioritized.

This RFC also sparked a discussion about how security boundaries should be handled.
I’ve chosen to implement it in a way that SAPI isn’t enabled unless explicitly opted into
Do you think this meets the requirements? (I’d especially like to ask Jakub about this.)

Unfortunately I don’t currently have much time as I need to spend all my time on stream (we have got quite tight deadline for it). The soonest I can properly spend time on this would be October.

To explain the process step by step, here is how I would like to proceed:

  1. Narrow the scope of this RFC, reach consensus and hold a vote as soon as possible, and ensure it can be incorporated into 8.6.
  2. Create a new RFC on Attribute-based Caching to initiate a new discussion (though this may not be ready in time for 8.6.)

It depends on other reviewers as well but I think 8.6 might be a bit too optimistic for this considering the implementation size a potentially security impact if it doesn’t work correctly.

Kind regards,

Jakub

Konnichiwa

Am 2026-05-16 17:19, schrieb Go Kudo:

RFC: PHP: rfc:opcache_static_cache
Implementation: [RFC] OPcache Static Cache Implementation by zeriyoshi · Pull Request #22052 · php/php-src · GitHub

Thank you for your RFC. I am seeing there already was discussion and changes to the RFC. This seems to have resulted in some inconsistencies.

The "Proposal" section still mentions "the volatile cache, used by OPcache\volatile_* […]", but these functions have been renamed. The same applies to the next bullet point for the pinned cache and to the "Why two cache backends" section. Maybe there is even more places where the old function names are referenced.

I also noticed some problems with the "Coding Standards and Naming Policy" (policies/coding-standards-and-naming.rst at main · php/policies · GitHub). Namely:

1. The namespace should be called `Opcache` for proper PascalCase.
2. The `CacheStoreType` cases should follow PascalCase, so `OpcacheSerialized` and `PhpSerialized`.
3. There must be a `OpcacheException` base exception: `class OpcacheException extends Exception { }`.
4. `StaticCacheException` must extend from the `OpcacheException`.
5. The policy does not say anything about properties, but it is generally accepted that properties should also use camelCase instead of underscores. So it needs to be `$startupFailed` instead of `$startup_failed` and similar.

I'm still working through the full RFC and the discussion. From what I see the points above are still valid in the latest version of the RFC and the discussion. That is why I am already sending them now.

Best regards
Tim Düsterhus

2026年6月8日(月) 20:09 Tim Düsterhus <tim@bastelstu.be>:

Konnichiwa

Am 2026-05-16 17:19, schrieb Go Kudo:

RFC: https://wiki.php.net/rfc/opcache_static_cache
Implementation: https://github.com/php/php-src/pull/22052

Thank you for your RFC. I am seeing there already was discussion and
changes to the RFC. This seems to have resulted in some inconsistencies.

The “Proposal” section still mentions “the volatile cache, used by
OPcache\volatile_* […]”, but these functions have been renamed. The same
applies to the next bullet point for the pinned cache and to the “Why
two cache backends” section. Maybe there is even more places where the
old function names are referenced.

I also noticed some problems with the “Coding Standards and Naming
Policy”
(https://github.com/php/policies/blob/main/coding-standards-and-naming.rst).
Namely:

  1. The namespace should be called Opcache for proper PascalCase.
  2. The CacheStoreType cases should follow PascalCase, so
    OpcacheSerialized and PhpSerialized.
  3. There must be a OpcacheException base exception: class OpcacheException extends Exception { }.
  4. StaticCacheException must extend from the OpcacheException.
  5. The policy does not say anything about properties, but it is
    generally accepted that properties should also use camelCase instead of
    underscores. So it needs to be $startupFailed instead of
    $startup_failed and similar.

I’m still working through the full RFC and the discussion. From what I
see the points above are still valid in the latest version of the RFC
and the discussion. That is why I am already sending them now.

Best regards
Tim Düsterhus

こんにちは Tim, お世話になっています
I have recovered my health thanks to the medication. :slight_smile:

Currently implementation has 3.0.0, but RFC document version is 2.0.0, Document is stale status.

I’m appealing to the Remove Attribute function currently, Attribute implementation requires the change of JIT implementations.
But currently do not respond to ML members. I stopped updating RFC documents and implementations.
(Sorry for my bad English, I didn’t say this with the intention of criticizing the ML members.)

I’ve implemented this as a pure PHP extension for use at my company. but performance is too bad compared
to this RFC implementation.

I understand there are security concerns associated with SHM sharing, and I believe I addressed these through
an opt-in process on the SAPI side. I would appreciate your feedback on whether this is a sufficient solution.

If I were to implement Attribute, this RFC would provide a very fast and efficient cache, but do you think we could get Dmitry’s approval?
(Derick, I apologize for getting your name wrong,)

What is the best option I can offer right now? For now, I want to implement this RFC in a way that everyone desires.

Best regards,
Go Kudo

On Wed, Jun 3, 2026, at 5:53 PM, Nicolas Grekas wrote:

4. Attributes. You kept them as "ergonomic surface, no new storage
model", but that does not answer the cost I raised: the JIT paths, the
VM hooks, and the CacheStrategy::Tracking machinery exist only for the
attribute case, and the reserved-key leak in #1 is their concrete
footprint on the explicit API. None of that is needed by the explicit
cache. That is exactly why I would split them into a follow-up: the
explicit cache can land and be reviewed on its own, while the attribute
semantics (cross-request shared mutable state, mutation tracking) get
the separate scrutiny they deserve. Bundled, they couple the review of
a simple primitive to the riskiest part of the patch.

5. Pinned/non-volatile I remain unconvinced. You proved that
"materialize once per worker, then a near-zero static read per request"
is something per-request hydrate cannot match. That is real but it is
narrow. If one cares about perf that much, then moving to a
worker-based runtime model (aka FrankenPHP workers) provides way more
evident perf improvement and doesn't need pinning at all since there,
static properties are live for a worker-long duration.

FrankenPHP has been sitting in the back of my head for this whole discussion. :slight_smile: The problem is that we are looking at three different levels of potential caching.

This RFC - relatively easy to use, may or may not need to re-materialize objects, fairly fast.
Compile to disk - hard to implement well, need to re-materialize objects, even faster.
Persistent process (Franken, Swoole, etc.) - Super simple to use, fastest option available.

I like the idea of this RFC, but part of me wonders if we should just say "use Franken, really." But then the fallback if you aren't running Fraken is the hardest to implement option. So why would I do that, when I have an easier to use option?

So, potentially, would that mean the options are "use this RFC or Franken, and the compiled option just kinda fades away?" (At least for data; for generated classes we'd still need it.) I'm not sure.

Just me thinking aloud...

--Larry Garfield

2026年6月9日(火) 18:58 Larry Garfield <larry@garfieldtech.com>:

On Wed, Jun 3, 2026, at 5:53 PM, Nicolas Grekas wrote:

  1. Attributes. You kept them as “ergonomic surface, no new storage
    model”, but that does not answer the cost I raised: the JIT paths, the
    VM hooks, and the CacheStrategy::Tracking machinery exist only for the
    attribute case, and the reserved-key leak in #1 is their concrete
    footprint on the explicit API. None of that is needed by the explicit
    cache. That is exactly why I would split them into a follow-up: the
    explicit cache can land and be reviewed on its own, while the attribute
    semantics (cross-request shared mutable state, mutation tracking) get
    the separate scrutiny they deserve. Bundled, they couple the review of
    a simple primitive to the riskiest part of the patch.

  2. Pinned/non-volatile I remain unconvinced. You proved that
    “materialize once per worker, then a near-zero static read per request”
    is something per-request hydrate cannot match. That is real but it is
    narrow. If one cares about perf that much, then moving to a
    worker-based runtime model (aka FrankenPHP workers) provides way more
    evident perf improvement and doesn’t need pinning at all since there,
    static properties are live for a worker-long duration.

FrankenPHP has been sitting in the back of my head for this whole discussion. :slight_smile: The problem is that we are looking at three different levels of potential caching.

This RFC - relatively easy to use, may or may not need to re-materialize objects, fairly fast.
Compile to disk - hard to implement well, need to re-materialize objects, even faster.
Persistent process (Franken, Swoole, etc.) - Super simple to use, fastest option available.

I like the idea of this RFC, but part of me wonders if we should just say “use Franken, really.” But then the fallback if you aren’t running Fraken is the hardest to implement option. So why would I do that, when I have an easier to use option?

So, potentially, would that mean the options are “use this RFC or Franken, and the compiled option just kinda fades away?” (At least for data; for generated classes we’d still need it.) I’m not sure.

Just me thinking aloud…

–Larry Garfield

Hi Larry

So, potentially, would that mean the options are “use this RFC or Franken, and the compiled option just kinda fades away?” (At least for data; for generated classes we’d still need it.) I’m not sure.

Yes, that’s essentially what I have in mind.

However, adapting existing applications to support FrankenPHP (and other
memory-resident runners) requires a considerable amount of effort.

What I want to achieve with this RFC is to reach that intermediate point—
to make a memory-resident cache available on an opt-in basis,
even in the traditional execution model, where needed.

Of course, I’m aware that there are “workarounds” using preload today.

However, those only work with scalar types. Applying such optimizations to
existing applications at a “truly effective level” would likely require about
the same amount of effort as migrating to FrankenPHP.

This RFC fixes this problem at its root. It is entirely opt-in, so users can
adopt the feature only after verifying its safety and reliability.

Furthermore, I believe that implementing this via Attributes offers the best
cost-benefit ratio. An Attribute-based implementation is the only approach that
can rival a true memory-resident one.

To achieve this, I think an explanation of this complex background and
consensus building are necessary.

I considered postponing the Attribute-based implementation, but perhaps I should have shared this premise first.

I hope this helps move the discussion forward.

Best regards,
Go Kudo

Hey Go,

On 16.5.2026 17:19:31, Go Kudo wrote:

Hi internals,

I'd like to start the discussion for a new RFC, OPcache Static Cache.

RFC: PHP: rfc:opcache_static_cache
Implementation: [RFC] OPcache Static Cache Implementation by zeriyoshi · Pull Request #22052 · php/php-src · GitHub

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.

Feedback welcome.

Best Regards,
Go Kudo

I've been trying to digest the RFC and it's quite long-winded.

The one thing I don't grasp is the "Security and Trust Model". Sure, if you have the worker pools, like in fpm, it absolutely makes sense to use it.

But why is it fundamentally required to have this sort of separation? What's the point? It just means that the whole webserver is a single boundary rather than having the ability to split more precisely. Which in the end is a configuration / system administration setup issue, rather than a fundamental flaw.

To me it's a non-starter to exclude apache2handler SAPI from this feature.

Regarding the API:

I think it might make sense to make the caches non-static classes, with a constructor accepting an optional arbitrary namespace; further split the overloaded behaviour of delete() and getCacheStoreType() - let's not mix classes and arbitrary keys:

abstract class Cache {
public abstract function get(string $key, null|bool|int|float|string|array|object $default = null): null|bool|int|float|string|array|object;
public abstract function getMultiple(array $keys, ?array $default = null): array|false;
public abstract function set(string $key, null|bool|int|float|string|array|object $value): bool;
public abstract function setMultiple(array $values): bool;
public abstract function has(string $key): bool;
public abstract function delete(string $key): bool;
public abstract function deleteMultiple(array $keys): bool;
public abstract function clear(): bool;
public abstract function lock(string $key, int $lease = 0): bool;
public abstract function unlock(string $key): bool;
public static function clearClass(string $class_name): bool;
public static function getCacheStoreType(string $key): CacheStoreType;
public static function getPropertyCacheStoreType(string $class_name, string $property): CacheStoreType;
public abstract static function clearAll(): bool;
public abstract static function info(): StaticCacheInfo;
}

class VolatileCache extends Cache {
public function __construct(string $namespace = "");
// Note that set() and setMultiple() are redeclared with optional `int $ttl = 0` parameters
public function set(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool;
public function setMultiple(array $values, int $ttl = 0): bool;
public static function clearAll(): bool;
public static function info(): StaticCacheInfo;
}

class PinnedCache extends Cache {
public function __construct(string $namespace = "");
public function increment(string $key, int $step = 1): int|false;
public function decrement(string $key, int $step = 1): int|false;
public static function clearAll(): bool;
public static function info(): StaticCacheInfo;
}

Given that the API is nearly identical, we can simplify it for both cache types and any library which actually requires $ttl can explicitly require VolatileCache instead of PinnedCache - with just $ttl being different.

An *application* author who does not want to carry around an instance of the caches, can trivially write define('VOLATILE', new VolatileCache); once and write VOLATILE->get("mykey") everywhere, getting the same usability than Volatile::get("mykey"), essentially.

This will allow you to inject well-scoped caches (instead of relying on the libraries to prefix their keys), declare new cache impls (e.g. "class LocalCache extends Cache", which would just do request-local caching rather than actually storing it) and make it easy to select between PinnedCache and VolatileCache as a library user.

I'm pretty confident this is what a lot of people here want to actually have, API wise.

Regarding atomic increment/decrement: does it actually matter that volatile does not guarantee continuity?

The behaviour of missing key is well-defined. I consider it much better to provide it than have users create their own poor-mans counter of get() + set() combination.

Also, a counter on a VolatileCache may actually be useful - e.g. the counter dropping back to zero is an indicator that eviction started. I would not assume this useless. Less useful than a counter of a PinnedCache, yes, but omitting it from VolatileCache is too opinionated.

Regarding Storable values: why are Closures not storable? The RFC says:

"Closure objects are request-local executable state and cannot be represented as stable shared cache values"

But that's not quite true - all Closures, except for those from non-file inclusions, like eval(), are effectively available in opcaches shared memory. And those which are not, could technically be stored too - might be a bit more expensive, but it's seldom the case. Would need some custom serialization though. Not supporting them is a choice, but the reason ("cannot") is the wrong one.

Regarding Attributes: Ah, the big contentious topic?

I'm not sure what to make of them. I get the appeal of a nice attribute that makes it just work, but it's limited, in sort of subtle ways:
- Refreshing the values ... is impossible? The RFC text says manually going through the Volatile/PinnedCache APIs with the 'volatile_static_class:' prefix and such is disallowed?
- Do we actually need to reserve these (*_static[_class]:slight_smile: prefixes? Can't we keep this an implementation detail and make sure that there are dedicated API methods to properly handle it?
- Why do we need dedicated prefixes? (volatile_ and pinned_, that is) Can't we look the specific class / property up and derive the required cache internally?
- The tracking implementation works by adding a tracking callback to the hot path of every array modification. Minor, but noticeable cost if nothing uses these attributes. Significant cost if attributes are used. Even more significant if writes are alternating (i.e. subsequent writes access different arrays). That's a total non-starter. Maybe you could do some hackery with overloading RW access to static properties specifically to assume dirtied. But the current implementation is a no-no. I get that this also affects stored objects and such. But, as said, too invasive/expensive.
I really wonder whether you sat down at any point here and asked yourself "should I really do this?", given all its complexity here.
- Scalar values are also not auto-refreshed, which probably makes for surprising behavior when trying to increment some counter, and other requests increment in parallel. I'm not too happy about the developer experience around this.

Overall, the attributes probably need quite a bit more discussion and refinement. I think it might be worth splitting it fully off into its own RFC.
And the implementation will be probably also downsized by more than half, making it a bit more manageable to review and assess.

The general integration of APCu-like capabilities with separate pinned and volatile caches make a lot of sense to me.
With some minor refinements, I think I'd like to see this functionality in PHP!

Thanks for your effort on this RFC,

Bob