From 8ec44d68177c36f58f86ff5cfe9af356ee87a51c Mon Sep 17 00:00:00 2001 From: Xinchen Hui Date: Tue, 1 Sep 2026 19:05:28 +0800 Subject: [PATCH] ext/yac: Add a Memory Management chapter and improve the examples Add a Memory Management chapter that explains the two independent shared-memory pools: - the key pool: a fixed-size slot table; expired and deleted slots are reused for free, and only a probe path of four live entries triggers an eviction (the least recently used, then the least hit), counted in the kicks counter - the value pool: 4M segments with a wrap-around allocator whose recycles overwrite the oldest values, detected by a CRC integrity guard and turned into misses Document how to size both pools and how to read the Yac::info counters (interval hit rate, kicks, recycles, fails) so each number maps to a concrete action. Also improve the examples: - split the combined add/set/get/delete examples into one runnable example per parameter form (single key, TTL, array form, default value, delayed deletion), each self-contained - fix the Yac::dump paging example, which returned an empty array on an empty cache while claiming int(100), and correct the per-entry hits reset semantics (reset on overwrite, not on delete or expiry) Further fixes: - fill the empty constant descriptions (YAC_VERSION, YAC_MAX_VALUE_RAW_LEN, YAC_MAX_RAW_COMPRESSED_LEN) and fix typos - align the synopsis parameter names with the arginfo ($keys -> $key) - document compress_threshold clamping (1024..1M, -1 disables) and the 1 MB stored-size / 64 MB raw-size limits - drop the stale configure.xml template (duplicated the installation section of setup.xml) --- reference/yac/book.xml | 9 +- reference/yac/configure.xml | 35 ----- reference/yac/constants.xml | 22 +++- reference/yac/ini.xml | 64 +++++---- reference/yac/memory.xml | 245 +++++++++++++++++++++++++++++++++++ reference/yac/yac/add.xml | 41 +++--- reference/yac/yac/delete.xml | 30 ++++- reference/yac/yac/dump.xml | 10 +- reference/yac/yac/get.xml | 28 +++- reference/yac/yac/info.xml | 10 +- reference/yac/yac/set.xml | 26 +++- 11 files changed, 413 insertions(+), 107 deletions(-) delete mode 100644 reference/yac/configure.xml create mode 100644 reference/yac/memory.xml diff --git a/reference/yac/book.xml b/reference/yac/book.xml index c0aba1418ee6..eb137f716dcf 100644 --- a/reference/yac/book.xml +++ b/reference/yac/book.xml @@ -25,7 +25,8 @@ path, a read is essentially a hash lookup in shared memory. As a result, Yac is extremely fast, with microsecond-level read latency, and its throughput can scale with the number of - workers as long as writes are spread across keys. + workers as long as writes are spread across keys; see the + benchmarks. Because Yac trades correctness guarantees for speed and throughput, @@ -36,8 +37,9 @@ As of yac 2.4.0, small scalar values — - NULL, booleans, integers, short strings of up to - 7 bytes and empty arrays — are stored directly inside the hash slot + NULL, booleans, most integers (those that fit in + 60 signed bits on 64-bit builds), strings of up to 7 bytes and empty + arrays — are stored directly inside the hash slot instead of in a separate value block ("embedded values"), which removes the value-memory allocation and block copy on every access and significantly improves performance while reducing memory usage. @@ -54,6 +56,7 @@ &reference.yac.setup; + &reference.yac.memory; &reference.yac.constants; &reference.yac.yac; diff --git a/reference/yac/configure.xml b/reference/yac/configure.xml deleted file mode 100644 index d003246fda54..000000000000 --- a/reference/yac/configure.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - -
- &reftitle.install; - - - &pecl.info; - &url.pecl.package;yac - - - -
- - - diff --git a/reference/yac/constants.xml b/reference/yac/constants.xml index a6c06d0eefbe..279d23bcde75 100644 --- a/reference/yac/constants.xml +++ b/reference/yac/constants.xml @@ -13,6 +13,7 @@ + The version of the extension as a string. @@ -23,7 +24,8 @@ - Max length of a key could be, it is 48 bytes. + Maximum length of a key in bytes: 48. An instance prefix counts + against this limit. @@ -34,6 +36,9 @@ + Maximum length of a value before serialization, in bytes: + 67,108,863 ((1 << 26) - 1). Larger values + are rejected. @@ -44,6 +49,8 @@ + Maximum size of a stored entry, in bytes: 1,048,576 (1M). Values + that do not fit even after compression are rejected. @@ -54,7 +61,7 @@ - Use php serialize as serializer + Use the PHP serialize format as serializer (the default). @@ -65,7 +72,8 @@ - Use json as serializer(requrie --enable-json) + Use JSON as the serializer. Requires the extension to be built with + . @@ -76,7 +84,8 @@ - Use igbinary as serializer(require --enable-igbinary) + Use igbinary as the serializer. Requires the extension to be built + with . @@ -87,7 +96,8 @@ - Use msgpack as serializer(require --enable-msgpack) + Use msgpack as the serializer. Requires the extension to be built + with . @@ -98,7 +108,7 @@ - Which serialzier is yac used + The serializer currently used by the extension. diff --git a/reference/yac/ini.xml b/reference/yac/ini.xml index c9bf9f9551ba..cbf54bbd01d1 100644 --- a/reference/yac/ini.xml +++ b/reference/yac/ini.xml @@ -19,7 +19,7 @@ yac.compress_threshold - -1 + 4K INI_SYSTEM @@ -43,7 +43,7 @@ yac.keys_memory_size - 4M + 8M INI_SYSTEM @@ -76,10 +76,17 @@ Serialized values larger than this number of bytes are - compressed before being stored (currently with LZ4). Set it to - -1 (the default) to disable compression - entirely. Compressing large values saves shared memory at the cost - of some CPU on both store and retrieve. + compressed (with LZ4, since Yac 2.4.0) before being stored. + Values above the 1M stored entry limit + (YAC_MAX_RAW_COMPRESSED_LEN) are always + compressed regardless of this setting, because they cannot be + stored uncompressed. The default of 4K + enables compression; -1 disables it for + values below the stored entry limit, and other positive values + are clamped to the + 1024..1M range. + Compressing large values saves shared memory at the cost of + some CPU on both store and retrieve. @@ -128,12 +135,30 @@ - Amount of shared memory used for the hash slots that hold keys - and bookkeeping. Each slot is a fixed-size structure, so this value - determines how many items can be tracked at once. Defaults to - 4M. Yac splits this area into segments; the - segment size is 4M, so this value must be a multiple of - 4M. + Amount of shared memory for the table of keys. It caps how many + entries can exist at once — the default of + 8M gives around 65,536 entries. Raise this + when the hit rate drops and kicks climb; see + for how a full table + behaves and when that actually matters. + + + + + + yac.values_memory_size + string + + + + Amount of shared memory for storing values. Values are allocated + in a bump allocator over segments of 4M each; when no segment has + room for a new value, an allocator cursor wraps and the oldest + values are silently overwritten ("recycled"). Their reads then + degrade to misses, detected by an integrity guard. Enlarge this + if recycles climb while slots still have room, + or enable yac.compress_threshold + to shrink large payloads. See . @@ -155,21 +180,6 @@
- - - yac.values_memory_size - string - - - - Amount of shared memory used to store the actual values. - Defaults to 64M. Yac allocates this area in - segments of 4M each, so this value must be a multiple of - 4M. When the area is full, least recently used - entries are kicked to make room for new ones. - - - diff --git a/reference/yac/memory.xml b/reference/yac/memory.xml new file mode 100644 index 000000000000..fce411d067f6 --- /dev/null +++ b/reference/yac/memory.xml @@ -0,0 +1,245 @@ + + + + + Memory Management + + + Yac keeps its data in two independent shared-memory pools, configured + with + yac.keys_memory_size + and + yac.values_memory_size. + They fill up and free up in different ways, so it helps to know which + pool a symptom belongs to before touching either knob. + + +
+ What Each Pool Holds + + The two pools store different halves of an entry. The key pool holds + the keys: one slot per cached key, carrying the key itself (up to 48 + bytes) together with its hash, TTL, hit count and last-access time; + the value is only referenced through a pointer to the value pool. The + value pool holds the values: every stored value occupies one block of + serialized bytes — the compressed form when the entry went through + yac.compress_threshold. + + + The one exception is embedded values: tiny scalars — &null;, + &true;, &false;, most integers (those that fit in 60 signed bits on + 64-bit builds), strings of up to 7 bytes and empty arrays — are + stored directly inside the slot, in the pointer that would otherwise + reference a block. Such values occupy no space in the value pool at + all; only their key does. + + + As a rough sizing guide: + + + + the key pool holds around 8,000 keys per MB, so size + yac.keys_memory_size + as the number of distinct keys divided by 8,000 per MB, rounded up + — the default 8M holds around 64,000 keys; + + + + + the value pool must hold every value that may still be read, so + size + yac.values_memory_size + as the number of live values times their average serialized size + (after compression), and allow roughly twice that: the pool is a + ring, and a value only dies once the allocator cursor comes back + around to overwrite it. + + + + Embedded values occupy a slot like any other entry, but consume no + space in the value pool, so leave them out of the second calculation. + +
+ +
+ The key pool (slots) + + yac.keys_memory_size + holds a fixed-size table of slots — the default of + 8M gives around 65,536 slots. Each + stored key occupies exactly one slot, so this pool caps the number + of entries that can exist at once; unlike the value pool, slots are + never individually freed. An expired slot — one past its TTL, or the + tombstone left by Yac::delete — is recycled + for free when a new key needs it. Only when all four candidate slots + of a probe path hold live entries is one of them evicted to make + room — one kick (the + kicks counter of Yac::info). + + + The eviction picks among the four live candidates of the colliding + probe path only: + + + + the least recently used one (the oldest + atime) is evicted; + + + + + on a tie the least-hit entry, then the earliest probe position. + + + + + + A common point of confusion: slots_used reaching + slots_size is not an error + condition. A cache whose working set of keys is larger than the slot + table simply runs at 100% occupancy from then on, evicting and + re-inserting as needed. The only thing that says whether the key + pool is sized correctly is the hit rate + (hits / (hits + miss), computed over the deltas + between two Yac::info snapshots rather than + the lifetime average). A high kicks count on its + own means nothing is wrong — the key distribution is simply not + uniform and some probe paths collide more than others. Only when the + hit rate and kicks are both + bad is the table too small for the key set, and the remedy is a + bigger yac.keys_memory_size. + + + A second consequence of slots never being freed: entries with no TTL + (ttl = 0) that are never read again keep occupying + a slot until an eviction happens to pick them. If an application + stores large amounts of such one-shot data, give those entries a TTL + so they expire and can be recycled without displacing live entries, + or size the key pool for the full key set. + +
+ +
+ The value pool (segments) + + yac.values_memory_size + is split into segments of 4M each, managed as rings: writes advance a + per-segment cursor and space is never freed per entry. When an + allocation no longer fits, the cursor wraps back to the start of a + segment — one recycle (the + recycles counter of + Yac::info). A recycle does not invalidate + the segment at once: overwritten values stay readable until the + wrapped cursor actually overwrites them, at which point their reads + fail the integrity guard and turn into misses. + + + Two sizes matter for this pool: the total + yac.values_memory_size + must hold the working set of live values, and a single entry can + hold at most 1 MB as stored + (YAC_MAX_RAW_COMPRESSED_LEN). Values larger + than that are therefore always compressed before being stored; a + value that cannot shrink below 1 MB — most often because it is + random data — is rejected and bumps the + fails counter. The absolute size limit on the + value itself is much higher: serialized values above 64 MB + (YAC_MAX_VALUE_RAW_LEN, that is + (1 << 26) - 1 bytes) are rejected + outright. + +
+ +
+ Sizing and what to watch + + Start with the defaults and watch the counters of + Yac::info — they accumulate from + start_time, so compare two snapshots taken some + time apart: + + + + + hit rate healthy (say >= 90%): the cache is fine; nothing to do, + whatever the other counters show; + + + + + hit rate low and kicks climbing: the key pool is + too small for the key set — live entries get evicted before they are + re-read. Raise + yac.keys_memory_size; + + + + + recycles frequent: this is a real problem, not a + benign counter. A recycle means the value allocator has wrapped and is + about to overwrite entries — anything overwritten dies before it could + be re-read, so the bytes spent storing it were wasted and the hit rate + suffers. The value pool is too small for the volume of live data. In + order of impact: + + + + give entries a TTL. Values written with ttl = 0 + stay live forever, so they keep occupying the pool and force the + cursor to wrap sooner. A TTL bounds how long each entry may live, + shrinking the live working set the pool has to hold; + + + + + raise + yac.values_memory_size + so the pool holds the whole live value set (remember to budget + roughly twice the live footprint — a value only dies once the + cursor comes back around to overwrite it); + + + + + store less per entry: lower + yac.compress_threshold + if it is set above the 1024 minimum, so large + payloads are compressed, and trim values that do not need to be + cached in full; + + + + + + + + fails growing: values that could not be stored, + most often a single value larger than the 1 MB stored-size limit + even after compression — split the value. + + + +
+ +
+ + diff --git a/reference/yac/yac/add.xml b/reference/yac/yac/add.xml index 5025732e38fd..8da46466dc8e 100644 --- a/reference/yac/yac/add.xml +++ b/reference/yac/yac/add.xml @@ -11,7 +11,7 @@ &reftitle.description; public boolYac::add - stringarraykeys + stringarraykey mixedvalue intttl0 @@ -31,7 +31,7 @@ &reftitle.parameters; - keys + key A string key, or an array of @@ -45,7 +45,7 @@ The value to store. Every PHP type except resource can be stored. Only used in the single-key form; when - keys is an array, this argument is instead the + key is an array, this argument is instead the optional ttl. @@ -68,21 +68,6 @@ Returns &true; on success, &false; on failure. A store is also rejected (returning &false;) when the key already exists and has not expired. - - - Yac stores entries without locks. Under heavy contention a store can - fail transiently; if the value must eventually be stored, retry: - -add("key", "value")) { - // retry on transient failure -} -?> -]]> - - - @@ -96,11 +81,31 @@ $yac = new Yac(); var_dump($yac->add("foo", "bar")); // bool(true) var_dump($yac->add("foo", "baz")); // bool(false): "foo" already exists +?> +]]> + + + + Adding an entry with a TTL + +add("short-lived", "value", 5); sleep(6); var_dump($yac->get("short-lived")); // bool(false): expired +?> +]]> + + + + Adding multiple entries at once + + value pairs with one call, with a ttl $yac->add(array("a" => 1, "b" => 2), 60); diff --git a/reference/yac/yac/delete.xml b/reference/yac/yac/delete.xml index 68d3ad88a82f..524a958f4e1c 100644 --- a/reference/yac/yac/delete.xml +++ b/reference/yac/yac/delete.xml @@ -11,7 +11,7 @@ &reftitle.description; public boolYac::delete - stringarraykeys + stringarraykey intdelay0 @@ -38,7 +38,7 @@ &reftitle.parameters; - keys + key A string key, or an array of keys to be @@ -96,13 +96,33 @@ var_dump($yac->delete("never")); // bool(false): was never stored var_dump($yac->info()["slots_used"]); // int(1) print_r($yac->dump()); // "foo" is still listed; its ttl // is in the past +?> +]]> + + + + Delayed deletion + +set("tmp", "value"); var_dump($yac->delete("tmp", 60)); // bool(true) +?> +]]> + + + + Deleting multiple keys at once + +set("tmp", "value"); -// deleting several keys at once returns true only when every key -// was present +// returns true only when every key was present var_dump($yac->delete(array("tmp", "nope"))); // bool(false): "nope" missing ?> ]]> diff --git a/reference/yac/yac/dump.xml b/reference/yac/yac/dump.xml index afd8377297a9..68b3843b335f 100644 --- a/reference/yac/yac/dump.xml +++ b/reference/yac/yac/dump.xml @@ -134,8 +134,9 @@ hits A per-entry hit counter, bumped on each successful - Yac::get, reset when the entry is overwritten, - deleted or expires (as of yac 2.4.0). + Yac::get and reset when the entry is + overwritten by a new Yac::set or + Yac::add (as of yac 2.4.0). @@ -232,6 +233,11 @@ Array set("key$i", $i); +} + $page_size = 100; $page_num = 2; diff --git a/reference/yac/yac/get.xml b/reference/yac/yac/get.xml index 55ccf9cc72d4..fd8cd0548de4 100644 --- a/reference/yac/yac/get.xml +++ b/reference/yac/yac/get.xml @@ -11,7 +11,7 @@ &reftitle.description; public mixedYac::get - stringarraykeys + stringarraykey mixeddefault&null; @@ -23,7 +23,7 @@ &reftitle.parameters; - keys + key A string key, or an array of keys. @@ -80,6 +80,16 @@ $yac = new Yac(); $yac->set("foo", "bar"); var_dump($yac->get("foo")); // string(3) "bar" var_dump($yac->get("missing")); // bool(false): a miss +?> +]]> + + + + Telling a stored false apart from a miss + +get("flag")); // bool(false): the stored value var_dump($yac->get("missing", false)); // bool(false): a miss, same shape var_dump($yac->get("flag", "__NONE__")); // bool(false): the stored value var_dump($yac->get("missing", "__NONE__")); // string(8) "__NONE__": a miss +?> +]]> + + + + Getting multiple keys at once + +set("foo", "bar"); $yac->set("foo2", "bar2"); + +// with an array of keys, only the found keys are present in the result var_dump($yac->get(array("foo", "foo2", "missing"))); // array(2) { ["foo"]=> string(3) "bar" ["foo2"]=> string(4) "bar2" } ?> diff --git a/reference/yac/yac/info.xml b/reference/yac/yac/info.xml index 8c4e9c3978e3..572c5fc14f4d 100644 --- a/reference/yac/yac/info.xml +++ b/reference/yac/yac/info.xml @@ -134,18 +134,18 @@ print_r($yac->info()); 46137344 - [slots_memory_size] => 4194304 - [values_memory_size] => 41943040 + [memory_size] => 75497472 + [slots_memory_size] => 8388608 + [values_memory_size] => 67108864 [segment_size] => 4194304 - [segment_num] => 10 + [segment_num] => 16 [miss] => 0 [hits] => 0 [fails] => 0 [kicks] => 0 [recycles] => 0 [start_time] => 1725955200 - [slots_size] => 32768 + [slots_size] => 65536 [slots_used] => 1 ) ]]> diff --git a/reference/yac/yac/set.xml b/reference/yac/yac/set.xml index 5ab42a9e5822..c6d50eb6b02d 100644 --- a/reference/yac/yac/set.xml +++ b/reference/yac/yac/set.xml @@ -11,7 +11,7 @@ &reftitle.description; public boolYac::set - stringarraykeys + stringarraykey mixedvalue intttl0 @@ -30,7 +30,7 @@ &reftitle.parameters; - keys + key A string key, or an array of @@ -44,7 +44,7 @@ The value to store. Every PHP type except resource can be stored. Only used in the single-key form; when - keys is an array, this argument is instead the + key is an array, this argument is instead the optional ttl. @@ -79,11 +79,31 @@ $yac = new Yac(); $yac->set("foo", "bar"); // store a single value $yac->set("foo", "baz"); // overwrite the existing entry +?> +]]> + + + + Setting an entry with a TTL + +set("short-lived", "value", 5); sleep(6); var_dump($yac->get("short-lived")); // bool(false): expired +?> +]]> + + + + Setting multiple entries at once + + value pairs with one call $yac->set(array("a" => 1, "b" => 2));