Yac is a lock-free shared-memory user data cache for PHP, dramatically faster than APCu and local Memcached under a realistic multi-worker load (see Benchmarks).
It can be used to replace APC or local memcached.
Yac is a lockless, shared memory cache. It lives in the same process space as PHP (no network round-trip) and avoids coarse-grained locks — which means:
- Best for: single-node deployments chasing maximum read throughput — configuration, routing tables, precomputed data, HTML fragments, anything you read far more often than you write. The cache lives in shared memory inherited by every PHP worker on the machine: no network round-trip, no cache server to run, and throughput scales with the worker count (see Benchmarks).
- Watch out for: deployments that need to share one cache across nodes — multiple hosts behind a load balancer must see the same entries. Yac's shared memory is per-machine: workers on different hosts each hold their own copy and never see one another's writes. Use Redis or Memcached when the cache has to span machines.
It is built for raw speed: get() is essentially a hash lookup in shared memory — microsecond-level latency.
16 worker processes sharing one cache, mixed reads/writes at a 100:1 ratio, compression off, 160M shared memory for both Yac and APCu. Numbers are aggregate ops/s across all workers, one value size per run:
| Value size | Yac | APCu 5.1.28 | Memcached 3.4.0 | Yac / APCu | Yac / Memcached |
|---|---|---|---|---|---|
| 6 B | 77.1M | 1.10M | 0.11M | 69.8x | 726.7x |
| 256 B | 60.5M | 1.12M | 0.11M | 54.0x | 574.3x |
| 2048 B | 17.2M | 1.28M | 0.10M | 13.4x | 169.6x |
Yac built from the current master. Measures throughput, not
consistency.
Environment and reproduction: bench/README.md.
- PHP 7+
$ pecl install yacPIE (PHP Installer for Extensions) downloads, builds and installs the extension from Packagist:
$ pie install laruence/yacOptional serializers can be enabled at install time:
$ pie install laruence/yac --enable-jsonSee PIE usage for more options (e.g. installing a specific version).
$ /path/to/phpize
$ ./configure --enable-yac \
[--enable-msgpack] \
[--enable-igbinary] \
[--enable-json] \
--with-php-config=/path/to-php-config
$ make && make installThe optional --enable-msgpack, --enable-igbinary, and --enable-json flags enable the corresponding serializers. See Serializer below.
Yac is disabled in CLI mode by default. If you are testing or running scripts from the command line, add this to your php.ini:
yac.enable_cli = 1Otherwise new Yac() will throw an exception.
- Yac is a lockless cache, you should try to avoid or reduce the probability of multiple processes setting the same key simultaneously.
- Yac checks every stored value with a full CRC-32C, so a corrupted or overwritten entry is detected and reported as a miss rather than returned.
- Cache key cannot be longer than 48 (
YAC_MAX_KEY_LEN) bytes. - Cache value cannot be longer than 67108863 (
YAC_MAX_VALUE_RAW_LEN) bytes, i.e.(1 << 26) - 1. - Cache value after compression cannot be longer than 1M (
YAC_MAX_RAW_COMPRESSED_LEN) bytes.
yac.enable = 1
yac.debug = 0 ; enable debug mode (PHP_INI_ALL).
; Currently reserved for future use.
yac.keys_memory_size = 8M ; 8M can hold ~64K key slots, scaling roughly linearly
yac.values_memory_size = 64M
yac.compress_threshold = 4K ; -1 disables compression. A positive value N means
; values larger than N bytes will be compressed before storage.
; Values below 1024 are clamped to 1024,
; values above the 1M stored-entry limit are clamped to it.
yac.enable_cli = 0 ; whether to enable Yac in CLI mode, default 0
yac.serializer = php ; since Yac 2.2.0, specify which serializer Yac uses.
; Available: php (always available),
; json (requires --enable-json),
; msgpack (requires --enable-msgpack),
; igbinary (requires --enable-igbinary)YAC_VERSION
YAC_MAX_KEY_LEN = 48
; If your key is longer than 48 bytes, consider using md5() of the key instead.
YAC_MAX_VALUE_RAW_LEN = 67108863 ; (1 << 26) - 1
YAC_MAX_RAW_COMPRESSED_LEN = 1048576 ; 1M in bytes
YAC_SERIALIZER_PHP = 0 ; always available, since Yac 2.2.0
; The following are only defined when the corresponding serializer is compiled in:
YAC_SERIALIZER_JSON = 1 ; requires --enable-json
YAC_SERIALIZER_MSGPACK = 2 ; requires --enable-msgpack
YAC_SERIALIZER_IGBINARY = 3 ; requires --enable-igbinary
YAC_SERIALIZER ; the serializer in use, determined by yac.serializer.
; Default is YAC_SERIALIZER_PHP.Yac can store all PHP types except resources:
| Type | Notes |
|---|---|
null |
Stored as-is; it comes back as NULL, while a miss returns false (or the default) — see Missing keys |
bool |
Stored as-is; to tell a stored false apart from a miss, pass a default — see Missing keys |
int / long |
Stored directly |
float / double |
Stored directly |
string |
Stored directly; compressed if larger than yac.compress_threshold |
array |
Serialized (php/msgpack/igbinary/json), then compressed if needed |
object |
Serialized and compressed same as array |
resource |
Not supported — triggers a warning |
Without a second argument, get() returns false for a key that does
not exist (or that fails the integrity check) — the same value a stored
false returns, so the two cases are indistinguishable by return value
alone.
Since 2.4.0, get() accepts an optional default argument that is
returned when the key is missing:
<?php
$yac->set("f", false);
var_dump($yac->get("f")); // bool(false) — the stored value
var_dump($yac->get("missing")); // bool(false) — a miss, same shape
var_dump($yac->get("f", "__NONE__")); // bool(false) — the stored value
var_dump($yac->get("missing", "__NONE__")); // string(8) "__NONE__" — the default
?>With a sentinel default, a miss can be told apart from every stored
value, including stored false and stored NULL. The default is passed
by value, so any expression works: get("n", 0), get("n", []), etc.
Yac::__construct([string $prefix = ""])Constructor of Yac. You can specify a prefix which will be prepended to every key in subsequent set/get/delete calls.
The prefix is concatenated directly onto the key — no separator is added automatically. If you need a separator, include it in the prefix.
Throws an exception if:
- Yac is not enabled (
yac.enable = 0) - The prefix exceeds
YAC_MAX_KEY_LEN(48) bytes
<?php
$yac = new Yac("myproduct_");
?><?php
// Two instances sharing the same memory pool but with isolated key namespaces:
$userCache = new Yac("user_");
$sysCache = new Yac("sys_");
$userCache->set("123", $userData); // actual key: "user_123"
$sysCache->set("config", $config); // actual key: "sys_config"Yac also supports array-like property access, which maps directly to get/set/delete:
<?php
$yac = new Yac();
$yac->foo = "bar"; // equivalent to $yac->set("foo", "bar")
echo $yac->foo; // equivalent to $yac->get("foo")
unset($yac->foo); // equivalent to $yac->delete("foo")Note: Property access always uses set() semantics (overwrite), not add(). TTL is not supported through property access — if you need TTL or add semantics, call the method explicitly.
Yac::add(string $key, mixed $value[, int $ttl = 0]): bool
Yac::add(array $kvs[, int $ttl = 0]): boolSimilar to set, but only stores the value if the key does not already exist (memcached add semantics).
$ttl is the time-to-live in seconds. 0 means the value never expires.
Returns true on success, false if the key already exists or on error.
<?php
$yac = new Yac();
$yac->add("foo", "bar"); // true — key doesn't exist yet
$yac->add("foo", "new"); // false — key already existsYac::set(string $key, mixed $value[, int $ttl = 0]): bool
Yac::set(array $kvs[, int $ttl = 0]): boolStore a value into Yac cache. Keys are cache-unique, so storing a second value with the same key will overwrite the original value.
$ttl is the time-to-live in seconds. 0 means the value never expires.
Returns true on success, false on error (e.g. cannot obtain CAS write lock).
<?php
$yac = new Yac();
$yac->set("foo", "bar");
$yac->set([
"dummy" => "foo",
"dummy2" => "foo",
]);
?>Since Yac 2.1, set() may fail if CAS competition occurs. For critical values, retry until success:
while (!$yac->set("important", "value") && $retry++ < 100/* guard against persistent CAS failure */);Yac::get(string $key[, mixed $default]): mixed
Yac::get(array $keys[, mixed $default]): arrayFetches a stored variable from the cache.
For a single key, returns the cached value on success. On a miss (key
does not exist, or integrity check fails) it returns $default if one
was passed, otherwise false — see Missing keys.
For an array of keys, returns an array of the found key-value pairs. Missing keys are omitted from the result when no default is given, so the presence of a key in the returned array means it exists in the cache; when a default is given, each missing key is present in the result with the default as its value.
Note: Before 2.4.0,
get()with an array of keys inserted afalseplaceholder for every missing key. Since 2.4.0 they are omitted (or filled with the given default) instead — code that reads$result[$key]without checking existence may now raise an undefined-key warning.
<?php
$yac = new Yac();
$yac->set("dummy", "foo");
$yac->set("dummy2", "foo");
$yac->get("dummy"); // "foo"
$yac->get("missing"); // false — a miss
$yac->get("missing", "fallback"); // "fallback"
$yac->get(["dummy", "missing"]); // ["dummy" => "foo"] — "missing" omitted
$yac->get(["dummy", "missing"], "none"); // ["dummy" => "foo", "missing" => "none"]
?>Yac::delete(string|array $keys[, int $delay = 0]): boolRemoves a stored variable from the cache. If $delay is specified (in seconds), the value will be deleted after $delay seconds — a delayed deletion.
Note:
delete()is a logical deletion — it marks the entry as expired (sets its TTL to 1, or to a future timestamp for delayed deletion) but keeps the slot until the space is reclaimed on a future access. It does not immediately free memory.
Returns true on success, false on failure.
Yac::flush(): boolImmediately invalidates all existing items across all Yac instances. This does not actually free any resources — it only marks all items as invalid. The operation is global and affects the entire shared memory pool, regardless of which instance (or prefix) calls it.
Yac::info(): arrayGet cache info and statistics.
<?php
var_dump($yac->info());
/* will return an array like:
array(13) {
["memory_size"] => int(75497472)
["slots_memory_size"] => int(8388608)
["values_memory_size"] => int(67108864)
["segment_size"] => int(4194304)
["segment_num"] => int(16)
["miss"] => int(0)
["hits"] => int(955)
["fails"] => int(0)
["kicks"] => int(0)
["recycles"] => int(0)
["start_time"] => int(1787379043)
["slots_size"] => int(65536)
["slots_used"] => int(955)
}
*/Each field means:
memory_size— slots + values memory in total (bytes)slots_memory_size— memory reserved for the slot table (bytes)values_memory_size— memory reserved for values (bytes)segment_size— size of each value segment (bytes)segment_num— number of value segmentsmiss— failed lookups (not found or expired)hits— successful lookupsfails— failed writes (value too big to allocate, etc.)kicks— live entries evicted to make room for new ones (expired slots are recycled for free and do not count)recycles— value segments wrapped around and reusedstart_time— when the shared memory was created (last (re)start), not reset byflush()(since Yac 2.4.0)slots_size— total number of slotsslots_used— slots currently occupied
Yac maintains two independent pools:
- Keys memory (
yac.keys_memory_size) is a fixed-size hash table ofslots_sizeslots — it caps how many entries can exist at once. Each key occupies one slot; a lookup probes up to 4 candidate slots. Expired slots (past their TTL, or the tombstonesdelete()leaves behind) are recycled for free. Only when all 4 candidate slots of a new key hold live entries is one of them evicted to make room — the entry with the oldestatimeamong them, ties falling to the least read candidate — one kick. - Values memory (
yac.values_memory_size) is split intosegment_numsegments ofsegment_sizebytes each (4M or more), managed as a ring: 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. A recycle does not invalidate the segment at once: existing values stay readable until the wrapped cursor actually overwrites them; overwritten values fail the integrity guard and turn into misses.
The core metric is the hit rate: hits / (hits + miss). The counters
accumulate from start_time, so compute it over the deltas between two
info() snapshots to reflect the current window instead of the lifetime
average.
- Hit rate is healthy (say ≥ 90%) — the cache is fine. A high
kicksalone is not a problem: it just means the key distribution is not uniform, so some probe groups collide more than others. - Hit rate low and
kickshigh — the slot table is too small for the key set; live entries get evicted before they are re-read. Increaseyac.keys_memory_size(8M holds ~64K slots, scaling roughly linearly). - Hit rate low and
recyclesfrequent — values are being overwritten before they get re-read. Increaseyac.values_memory_size; values aboveyac.compress_threshold(4K by default) are compressed already, so lower the threshold to shrink more of them. recyclesfrequent while the slot table still has room (slots_usedbelowslots_size), regardless of hit rate — the value ring is wrapping fast while keys memory is not the constraint: values memory is undersized for the write volume. Increaseyac.values_memory_size.fails> 0 — writes that could not allocate space: most commonly a single value larger than one segment, or transient allocator contention under heavy concurrent writes. Loweryac.compress_thresholdso the value gets compressed, or shrink oversized values.
Yac::dump([int $limit = 100, [int $offset = 0]]): arrayDump cache entries for debugging. Returns an array of entries, each containing:
index— slot index in the hash tablehash— 64-bit hash of the key, used for slot probingcrc— CRC32 checksum of the value payload;0for embedded entriesttl— expiration timestamp (unix time);0means never expiresk_len— key lengthv_len— value length in bytes; for compressed entries this is the length of the original (uncompressed) valuec_len— length of the compressed payload actually stored in shared memory, in bytes; present only for compressed entries (since Yac 2.4.0)size— allocated size of the value block in shared memory (bytes);0for embedded entriesatime— last access time, updated on successfulget(); the entry with the oldestatimeamong the candidate slots is evicted first, and when several share the oldestatimethe one with the fewesthitsgoes (since Yac 2.4.0)hits— per-entry hit counter, bumped on every successfulget(); reset when the entry is overwritten, deleted or expires; among entries with equally oldatime, the least hit one is evicted first (since Yac 2.4.0)embedded— whether the value is stored directly inside the slot (see below) (since Yac 2.4.0)key— the cache key
Note: Before 2.4.0,
dump()did not reportatime,hits,embeddedorc_len— these fields do not exist in older versions — andv_lenwas the stored (compressed) length rather than the original value length.
Small values are embedded in the slot itself instead of allocating a value
block: NULL, booleans, small integers, strings up to 7 bytes and empty
arrays. Embedded entries allocate no value memory at all, so for them crc
and size are reported as 0, while atime and hits are kept in the slot
and remain meaningful.
$limit controls the maximum number of entries returned (default 100). Passing -1 dumps all entries — intended for debugging only: the whole result is materialized as a PHP array and can consume a lot of memory on a busy cache.
$offset (since Yac 2.4.0) skips the first $offset occupied entries, so
dump($limit, $offset) returns entries $offset + 1 through $offset + $limit. If fewer than $offset entries exist (including an empty cache),
an empty array is returned.
Note: Since
delete()only marks entries expired (see Yac::delete) anddump()is a raw scan of all occupied slots, deleted or expired entries may still show up in the output. To filter them out, check thettlfield:ttl == 0means never expires; a non-zerottlthat is less than the current time indicates an expired or deleted entry.
<?php
$entries = $yac->dump(10);
foreach ($entries as $entry) {
echo $entry["key"] . " => ttl=" . $entry["ttl"] . "\n";
}c_len makes it easy to see how much compression saves per entry:
<?php
foreach ($yac->dump() as $entry) {
if (isset($entry["c_len"])) {
printf("%s: %d -> %d bytes (%.0f%% saved)\n",
$entry["key"], $entry["v_len"], $entry["c_len"],
100 - $entry["c_len"] / $entry["v_len"] * 100);
}
}- Compression: values exceeding
yac.compress_threshold(orYAC_STORAGE_MAX_ENTRY_LEN) are compressed before storage — with LZ4 since 2.4.0 (FastLZ in earlier releases). If compression would make a value larger (e.g. random or already-compressed data),set()fails with a warning instead of storing it. - CRC32 acceleration: integrity is checked with CRC-32C. Yac detects hardware CRC instruction support at runtime (SSE4.2 on x86_64, ARMv8 CRC on aarch64) and uses it when available, so a binary built on a newer CPU still runs on older ones; otherwise it falls back to a slicing-by-8 software CRC.
- Shared memory: Yac tries
mmap(MAP_ANON)first, thenmmap(/dev/zero), then falls back to SysV IPCshmget. The chosen backend is determined at compile time.