forked from php/php-src
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRFC.txt
More file actions
489 lines (334 loc) · 23.9 KB
/
RFC.txt
File metadata and controls
489 lines (334 loc) · 23.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
====== PHP RFC: OPcache User Cache and RequestOverStatic ======
* Version: 1.0
* Date: 2026-04-27
* Author: Go Kudo <zeriyoshi@php.net> <g-kudo@colopl.co.jp>
* Status: Draft
* Implementation: TBD
* First Published at: TBD
===== Introduction =====
PHP applications commonly use APCu when they need to cache user-defined values across requests. APCu is a proven solution and is widely deployed, but its architecture necessarily stores values through serialization and restores them through deserialization. This cost is acceptable for small scalar values, but it becomes a visible performance and memory cost for large arrays and object graphs.
A different class of solutions has appeared in the PHP ecosystem: long-running runtimes such as FrankenPHP, Swoole, RoadRunner, and similar server models. These runtimes can keep objects alive in process memory across requests. However, moving an existing PHP application to such a runtime often requires a substantial review of request isolation assumptions, service lifetimes, object reuse, and extension compatibility.
This RFC proposes adding an OPcache-backed user cache and a controlled static-state persistence mechanism. The goal is to provide a faster request-crossing cache while keeping the traditional shared-nothing PHP request model available to existing applications.
Ironically, this means OPcache would regain part of what the historical APC extension provided before APCu and OPcache became separate concerns. However, in modern PHP, OPcache is the component that already owns persistent script metadata, immutable interned structures, preloading, and JIT integration. For this reason, OPcache is the appropriate implementation location for cache paths that need to cooperate with the engine and the JIT.
The proposal also includes shared-memory support in ZTS builds. This makes the mechanism useful not only for traditional multi-process SAPIs such as FPM, but also for threaded or always-running environments such as FrankenPHP when they are built with ZTS.
===== Problems =====
==== APCu requires serialization and deserialization ====
APCu stores user values outside the request heap. As a result, storing and fetching values requires a serialization boundary. This is simple and robust, but it means the following costs are paid repeatedly:
* serializing the value when storing it;
* deserializing the value when fetching it;
* allocating a fresh request-local copy of the whole value;
* recursively rebuilding large object graphs even if the request only reads them.
This is especially expensive for large objects, nested arrays, and data structures that are read many times and mutated rarely.
For example:
<code php>
$data = apcu_fetch('application.graph');
// The request may only read a small part of the graph, but the whole graph has
// already been deserialized and materialized into request-local memory.
return $data->configuration->features['checkout'];
</code>
==== Existing long-running runtimes require migration work ====
Long-running runtimes can keep PHP objects alive across requests, avoiding some of the serialization overhead described above. This can be very powerful, but applications originally written for the traditional request model often rely on request-local cleanup and implicit isolation.
Migrating such applications may require reviewing:
* service container lifetimes;
* mutable singletons;
* global state and static variables;
* extension state that was designed for request shutdown;
* framework middleware and bootstrap assumptions;
* deployment and process management.
This RFC is not intended to replace long-running runtimes. Instead, it provides a lower migration-cost option for applications that want faster cross-request caching while continuing to run as normal PHP requests.
==== Static state is request-local even when it is logically cache state ====
Class static properties and method static variables are sometimes used to cache expensive computations inside a request:
<code php>
final class Metadata
{
public static function forClass(string $class): array
{
static $cache = [];
return $cache[$class] ??= self::compute($class);
}
}
</code>
This pattern is efficient within one request, but the state is lost at request shutdown. Replacing such code with APCu usually requires manual cache keys, invalidation, error handling, and serialization behavior. It also changes the shape of the code.
===== Proposal =====
This RFC proposes three related additions to OPcache:
* an explicit OPcache user cache API;
* an <php>OPcache\RequestOverStatic</php> attribute for persistent static state;
* an <php>OPcache\SafeDirectCache</php> attribute for direct engine-managed cache paths.
The cache is stored in a separate OPcache shared-memory segment. It is disabled by default and is enabled by configuring a new <php>opcache.user_cache_memory_consumption</php> directive.
The design goals are:
* keep the traditional PHP request model;
* avoid unnecessary serialization and eager materialization where the engine can safely do so;
* make repeated same-request reads cheap through a request-local lookup cache;
* support NTS, process-based SAPIs, and ZTS shared-memory environments;
* avoid enabling any new behavior unless the administrator explicitly allocates memory for it.
==== Explicit OPcache user cache API ====
The following class and functions are added to the <php>OPcache</php> namespace:
<code php>
namespace OPcache;
class UserCacheException extends \Exception {}
function cache_store(string $key, mixed $value, int $ttl = 0): bool;
function cache_store_array(array $values, int $ttl = 0): bool;
function cache_fetch(string $key): mixed;
function cache_delete(string $key): void;
function cache_delete_array(array $keys): void;
function cache_clear(): void;
function cache_atomic_increment(string $key, int $step = 1): int;
function cache_atomic_decrement(string $key, int $step = 1): int;
function user_cache_info(): array;
</code>
The API is intentionally similar to existing user cache APIs, but it lives in OPcache and can use OPcache-specific storage and optimization paths.
Example:
<code php>
OPcache\cache_store('config', load_configuration(), ttl: 60);
try {
$config = OPcache\cache_fetch('config');
} catch (OPcache\UserCacheException) {
$config = load_configuration();
OPcache\cache_store('config', $config, ttl: 60);
}
</code>
<php>cache_store()</php> stores one value. <php>cache_store_array()</php> stores multiple key/value pairs with the same TTL. A TTL of zero means the entry does not expire by time. Negative TTL values are rejected.
<php>cache_fetch()</php> returns the stored value or throws <php>OPcache\UserCacheException</php> if the key is not present, expired, corrupted, or otherwise unavailable.
<php>cache_delete()</php>, <php>cache_delete_array()</php>, and <php>cache_clear()</php> remove entries immediately.
<php>cache_atomic_increment()</php> and <php>cache_atomic_decrement()</php> update an integer value under the user-cache lock and return the new value. They throw <php>OPcache\UserCacheException</php> if the key is missing or the stored value is not an integer.
<php>user_cache_info()</php> returns implementation status. The current implementation exposes at least the following keys:
* <php>enabled</php>
* <php>available</php>
* <php>startup_failed</php>
* <php>backend_initialized</php>
* <php>configured_memory</php>
* <php>shared_memory</php>
* <php>segment_count</php>
* <php>shared_model</php>
* <php>failure_reason</php>, when applicable
The existing <php>opcache_get_status()</php> result is also extended with user-cache status information.
==== Request-local lookup cache ====
A request-local lookup cache is used for repeated reads of the same key within one request. The first fetch resolves the shared entry. Later fetches can avoid repeated shared-memory lookup and conversion, as long as the shared mutation epoch still matches.
The lookup cache is invalidated when the same request stores, deletes, or clears entries. If another process, worker, or ZTS thread mutates the shared cache, the next fetch detects the shared mutation epoch change and discards stale request-local entries.
==== Read-mostly object graph fetch and copy-on-mutate ====
For object graphs that are safe to represent in OPcache-managed shared storage, the cache may fetch a shared graph in a read-mostly form instead of eagerly rebuilding a full request-local deep copy.
If the value is only read, this avoids unnecessary allocation and object graph reconstruction. If the request mutates the fetched value, OPcache materializes a request-local copy before applying the mutation. This preserves normal PHP value semantics while improving read-mostly workloads.
This is an implementation optimization, not a new user-visible reference model. User code continues to observe normal PHP values.
==== SafeDirectCache ====
The following attribute is added:
<code php>
namespace OPcache;
#[\Attribute(\Attribute::TARGET_CLASS)]
final class SafeDirectCache {}
</code>
<php>OPcache\SafeDirectCache</php> marks classes for which OPcache may use a direct engine-managed cache representation instead of a generic serialization fallback.
The initial implementation uses this for selected internal classes, including DateTime-related classes and selected SPL collection classes, where the engine can safely encode and restore state:
* <php>DateTime</php>
* <php>DateTimeImmutable</php>
* <php>DateTimeZone</php>
* <php>DateInterval</php>
* <php>SplFixedArray</php>
* <php>ArrayObject</php>
* <php>ArrayIterator</php>
* <php>RecursiveArrayIterator</php>
If a subclass overrides serialization-related magic methods such as <php>__serialize()</php>, <php>__unserialize()</php>, <php>__sleep()</php>, or <php>__wakeup()</php>, OPcache falls back to the safe generic path instead of bypassing userland-defined behavior.
This attribute is not intended to make unsafe objects magically shareable. It is an opt-in marker for classes whose state can be handled by OPcache without violating PHP object semantics.
==== RequestOverStatic ====
The following attribute is added:
<code php>
namespace OPcache;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class RequestOverStatic {}
</code>
<php>OPcache\RequestOverStatic</php> allows selected static state to persist across requests through the OPcache user cache.
When the attribute is applied to a class, OPcache persists the class static properties and method static variables of that class as class-wide state:
<code php>
#[OPcache\RequestOverStatic]
final class Metadata
{
public static array $classes = [];
public static function forClass(string $class): array
{
static $cache = [];
return $cache[$class] ??= self::compute($class);
}
}
</code>
When the attribute is applied to a static property, only that property is persisted:
<code php>
final class Counter
{
#[OPcache\RequestOverStatic]
public static int $value = 0;
}
++Counter::$value;
</code>
When the attribute is applied to a method, only the static variables declared inside that method are persisted:
<code php>
final class MethodCounter
{
#[OPcache\RequestOverStatic]
public static function next(): int
{
static $value = 0;
return ++$value;
}
}
</code>
Normal static properties and normal method static variables without the attribute are unchanged. They remain request-local.
The first request initializes static values normally. Later requests restore the selected static values from the OPcache user cache. Mutations are tracked by the engine and are published to the cache according to the RequestOverStatic persistence rules.
Class-level <php>RequestOverStatic</php> stores a class snapshot under one cache entry. This avoids creating many independent cache keys for a class with multiple static properties and method static variables, and gives OPcache a single point for class-wide restore and publish.
Property-level <php>RequestOverStatic</php> stores the selected property independently. Nested array and object mutations are tracked so that updates are not lost merely because the root static property was not directly assigned again.
Method-level <php>RequestOverStatic</php> stores the selected method static variables independently, using the same request-end publish rules as other method static state.
==== Enabling and disabling ====
The feature is disabled by default.
A new INI directive is added:
<code ini>
opcache.user_cache_memory_consumption=0
</code>
The value is a memory size in megabytes. A value of zero disables the OPcache user cache and disables <php>RequestOverStatic</php> persistence. Non-zero values below the OPcache minimum shared-memory size of 8MB are rejected with a warning and leave the user cache unavailable.
The directive is <php>PHP_INI_SYSTEM</php>. It must be configured before OPcache is set up. In particular, FPM pools must not attempt to change it per pool after OPcache shared memory has already been initialized.
If user-cache startup fails, OPcache itself remains available. The failure disables only the OPcache user cache and related features, and the failure reason can be inspected with <php>OPcache\user_cache_info()</php> or <php>opcache_get_status()</php>.
===== Shared Memory and ZTS =====
The OPcache user cache uses a shared-memory segment separate from the existing script cache segment. This is necessary because the normal OPcache shared allocator is designed around persistent script structures, while a user cache needs mutable allocation, deletion, TTL expiry, and reuse of freed blocks.
The user cache therefore has its own storage header, hash table, allocator/free list, and locking. The implementation supports the same broad family of shared-memory backends used by OPcache, subject to platform availability.
In ZTS builds, the cache is shared through SHM and protected by process/thread-safe locking. This means multiple PHP threads in the same process can observe the same cache state, and multiple processes can still share the same backing storage when the SAPI model supports it.
This is useful for classic SAPIs, but it is also important for long-running ZTS runtimes. Even if such runtimes can keep per-process memory alive, a shared OPcache cache can provide a common cache mechanism across workers and threads without requiring application code to abandon the request model.
===== Examples =====
==== Explicit cache ====
<code php>
function get_permissions(int $userId): array
{
$key = "permissions:$userId";
try {
return OPcache\cache_fetch($key);
} catch (OPcache\UserCacheException) {
$permissions = load_permissions_from_database($userId);
OPcache\cache_store($key, $permissions, ttl: 300);
return $permissions;
}
}
</code>
==== Persistent static cache ====
<code php>
#[OPcache\RequestOverStatic]
final class RouteMetadata
{
public static function get(string $controller): array
{
static $cache = [];
return $cache[$controller] ??= compute_route_metadata($controller);
}
}
</code>
This preserves the familiar method-static cache style, but the selected state can survive request shutdown.
==== Property-scoped persistence ====
<code php>
final class FeatureFlags
{
#[OPcache\RequestOverStatic]
public static array $resolved = [];
}
FeatureFlags::$resolved['checkout'] ??= load_flag('checkout');
</code>
Only <php>FeatureFlags::$resolved</php> is persisted. Other static state in the class is unaffected.
===== Performance =====
The implementation was benchmarked with <php>Zend/bench.php</php> on NTS and ZTS CLI builds. The baseline was commit <php>360ec2a38cf</php>. The current implementation was built from the working tree containing this proposal. Each configuration was executed 30 times.
To reduce systematic drift across long benchmark runs, the runs were interleaved: each iteration cycles through all four builds (baseline NTS, current NTS, baseline ZTS, current ZTS) and all five modes once before moving on to the next iteration.
The comparison used the following modes:
* OPcache disabled
* OPcache enabled
* OPcache enabled with <php>opcache.user_cache_memory_consumption=128</php>
* OPcache + JIT
* OPcache + JIT with <php>opcache.user_cache_memory_consumption=128</php>
The Total mean deltas were:
| ^ OPcache disabled ^ OPcache enabled ^ OPcache enabled + user cache 128MB ^ OPcache + JIT ^ OPcache + JIT + user cache 128MB ^
^ NTS CLI | +3.43% | +1.54% | +1.21% | +6.09% | +0.62% |
^ ZTS CLI | +5.64% | +2.62% | +1.22% | -0.30% | +0.08% |
Negative values mean the current implementation was faster than the baseline. Positive values mean the current implementation was slower.
Small individual benchmark items can show large percentage swings because <php>Zend/bench.php</php> reports some values close to 0.001 seconds. When both baseline and current values round to the same 0.001-second bucket, including the case where both round to 0.000s and the percentage formula divides by zero, the per-item delta is reported as +0.00%. For the Total result, NTS CLI stayed within +0.62% to +6.09% and ZTS CLI stayed within -0.30% to +5.64%. In the OPcache + JIT configuration, the NTS CLI Total mean was +6.09%, while the median stayed at 0.0430s for both baseline and current. Enabling a 128MB user cache stayed close to the corresponding OPcache configuration in this benchmark.
These results are not intended to claim that every workload becomes faster. The expected wins are workloads that repeatedly read large cached arrays, objects, or metadata graphs, especially when they are read more often than they are mutated.
An additional HTTP benchmark suite focused on cache-heavy application workloads was added in <php>OPcache_UserCache_Benchmark</php>. It was re-executed on an NTS php-fpm 1-worker setup and on a ZTS FrankenPHP 1-thread setup, with APCu enabled in both environments.
| Workload | Backend | NTS php-fpm mean worker_us | ZTS FrankenPHP mean worker_us |
|-----------------------|-------------|----------------------------:|-------------------------------:|
| shared_graph | build | 1491.63 | 1649.97 |
| shared_graph | serialize | 3423.57 | 3640.87 |
| shared_graph | apcu | 2802.33 | 3172.98 |
| shared_graph | user_cache | 1969.12 | 2066.07 |
| repeat_fetch | serialize | 104457.15 | 94200.90 |
| repeat_fetch | apcu | 95774.78 | 85251.33 |
| repeat_fetch | user_cache | 49822.87 | 54593.40 |
| shared_graph_mutate | serialize | 2896.00 | 3067.93 |
| shared_graph_mutate | apcu | 3567.10 | 2832.62 |
| shared_graph_mutate | user_cache | 2051.27 | 1938.53 |
| request_over_static | local | 42046.28 | 40759.22 |
| request_over_static | apcu | 8620.87 | 8187.08 |
| request_over_static | persisted | 2776.18 | 2782.18 |
In these HTTP results, the OPcache user cache beat APCu on the shared-graph workloads by 29.73%, 47.98%, and 42.49% on NTS php-fpm, and by 34.89%, 35.96%, and 31.56% on ZTS FrankenPHP for <php>shared_graph</php>, <php>repeat_fetch</php>, and <php>shared_graph_mutate</php> respectively. For <php>request_over_static</php>, the persisted path beat the explicit APCu path by 67.80% on NTS php-fpm and 66.02% on ZTS FrankenPHP, and beat local reconstruction by 93.40% and 93.17% respectively.
===== Backward Incompatible Changes =====
There are no intended backward incompatible changes for existing PHP code.
The OPcache user cache is disabled by default. If <php>opcache.user_cache_memory_consumption</php> remains zero, the new cache storage is not created and <php>RequestOverStatic</php> persistence remains inactive.
The following new names are introduced and therefore become unavailable for userland declarations when OPcache is enabled:
* <php>OPcache\UserCacheException</php>
* <php>OPcache\RequestOverStatic</php>
* <php>OPcache\SafeDirectCache</php>
* <php>OPcache\cache_store()</php>
* <php>OPcache\cache_store_array()</php>
* <php>OPcache\cache_fetch()</php>
* <php>OPcache\cache_delete()</php>
* <php>OPcache\cache_delete_array()</php>
* <php>OPcache\cache_clear()</php>
* <php>OPcache\cache_atomic_increment()</php>
* <php>OPcache\cache_atomic_decrement()</php>
* <php>OPcache\user_cache_info()</php>
===== Proposed PHP Version(s) =====
PHP 8.6.
===== RFC Impact =====
==== To SAPIs ====
No SAPI API changes are required.
SAPIs that already use OPcache may use the feature when <php>opcache.user_cache_memory_consumption</php> is configured. Process-based SAPIs can share the cache through OPcache SHM. ZTS SAPIs can share it across threads using the ZTS-safe locking path.
==== To Existing Extensions ====
No existing extension API is removed.
Extensions that inspect OPcache configuration or status may observe the new <php>opcache.user_cache_memory_consumption</php> directive and the new user-cache status array.
==== To Opcache ====
OPcache gains:
* a separate user-cache SHM segment;
* a mutable allocator/free-list for user values;
* request-local lookup cache support;
* native serialization and shared-graph storage paths;
* selected direct cache paths for safe internal classes;
* static access and mutation tracking hooks for <php>RequestOverStatic</php>;
* ZTS-safe shared-memory locking for the user cache.
==== New Classes ====
* <php>OPcache\UserCacheException</php>
* <php>OPcache\RequestOverStatic</php>
* <php>OPcache\SafeDirectCache</php>
==== New Functions ====
* <php>OPcache\cache_store()</php>
* <php>OPcache\cache_store_array()</php>
* <php>OPcache\cache_fetch()</php>
* <php>OPcache\cache_delete()</php>
* <php>OPcache\cache_delete_array()</php>
* <php>OPcache\cache_clear()</php>
* <php>OPcache\cache_atomic_increment()</php>
* <php>OPcache\cache_atomic_decrement()</php>
* <php>OPcache\user_cache_info()</php>
==== New Constants ====
None.
==== php.ini Defaults ====
A new INI directive is added:
| ^ Default ^ Changeable ^ Meaning ^
^ opcache.user_cache_memory_consumption | 0 | PHP_INI_SYSTEM | OPcache user cache memory size in megabytes |
A value of zero disables the feature. Non-zero values must be at least 8MB.
===== Open Issues =====
* Confirm the final implementation URL.
* Decide whether the function names should remain <php>cache_*</php> or use a more explicit <php>user_cache_*</php> prefix.
* Decide whether <php>OPcache\SafeDirectCache</php> should be documented as a user-facing opt-in attribute or treated primarily as an engine/internal marker exposed for reflection.
===== Future Scope =====
The following ideas are intentionally outside the initial proposal:
* eviction policies beyond explicit delete, clear, TTL expiry, and allocator failure behavior;
* a PSR-compatible cache adapter in core;
* distributed cache support;
* automatic persistence of arbitrary globals or arbitrary object instances;
* changing APCu behavior;
* changing the default value of <php>opcache.user_cache_memory_consumption</php> from zero.
===== Vote =====
TBD. A 2/3 majority is required because this RFC adds new functionality to PHP.
===== Patches and Tests =====
* Implementation: TBD
* Tests: OPcache user cache, SafeDirectCache, RequestOverStatic, FPM, and ZTS focused tests are included with the implementation branch.