Cache is often necessarry to achieve good performance.
The two articles:
covers the more advanced scenarios. This article is about the more simple scenarios. The content in this article is rather trivial, but I have written it for completeness as the two articles listed above are missing some stuff.
The basic concept is easy to understand.
The topology described here is local in-application cache.
All examples below will try to cache a simulated database operation that takes 10 milliseconds.
All modern programming environments has a datastructure for fast lookup (hash table based, tree based or whatever).
Usually called map, dictionary or associative array.
It is actually sufficient to implement a simple cache.
package demo;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class TestPrimitive {
private static final int DT = 10;
private static final int SIZE = 1000;
private static String realLookup(int key) {
// simulate expensive DB operation
try {
Thread.sleep(DT);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Value#" + key;
}
private static Map<Integer, String> cache;
private static String cacheLookup(int key) {
String val = cache.get(key);
if(val == null) {
val = realLookup(key);
cache.put(key, val);
}
return val;
}
public static void test(String lbl, int startKey, int endKey, Function<Integer, String> lookup) {
long t1 = System.currentTimeMillis();
for(int i = startKey; i <= endKey; i++) {
String val = lookup.apply(i);
if(!val.equals("Value#" + i)) throw new RuntimeException("Ooops");
}
long t2 = System.currentTimeMillis();
System.out.printf("%s keys %d - %d : %d ms\n", lbl, startKey, endKey, t2 - t1);
}
public static void main(String[] args) throws Exception {
cache = new HashMap<Integer, String>();
test("No cache", 1, SIZE, key -> realLookup(key));
test("Cache (1st)", 1, SIZE, key -> cacheLookup(key));
test("Cache (2nd)", 1, SIZE, key -> cacheLookup(key));
test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key -> cacheLookup(key));
test("Cache (back)", 1, SIZE, key -> cacheLookup(key));
}
}
using System;
using System.Collections.Generic;
using System.Threading;
namespace LocalCache.TestPrimitive
{
public class Program
{
private const int DT = 10;
private const int SIZE = 1000;
private static string RealLookup(int key)
{
// simulate expensive DB operation
Thread.Sleep(DT);
return "Value#" + key;
}
private static IDictionary<int, string> cache;
private static string CacheLookup(int key)
{
string val;
if(!cache.ContainsKey(key))
{
val = RealLookup(key);
cache[key] = val;
}
else
{
val = cache[key];
}
return val;
}
public static void Test(string lbl, int startKey, int endKey, Func<int, string> lookup)
{
DateTime t1 = DateTime.Now;
for(int i = startKey; i <= endKey; i++)
{
string val = lookup(i);
if(val != "Value#" + i) throw new Exception("Ooops");
}
DateTime t2 = DateTime.Now;
Console.WriteLine("{0} keys {1} - {2} : {3} ms", lbl, startKey, endKey, (long)(t2 - t1).TotalMilliseconds);
}
public static void Main(string[] args)
{
cache = new Dictionary<int, string>();
Test("No cache", 1, SIZE, key => RealLookup(key));
Test("Cache (1st)", 1, SIZE, key => CacheLookup(key));
Test("Cache (2nd)", 1, SIZE, key => CacheLookup(key));
Test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key => CacheLookup(key));
Test("Cache (back)", 1, SIZE, key => CacheLookup(key));
Console.ReadKey();
}
}
}
from time import sleep, time
DT = 10
SIZE = 1000
def real_lookup(key):
sleep(DT / 1000.0)
return 'Value#' + str(key)
cache = {}
def cache_lookup(key):
if key not in cache:
val = real_lookup(key)
cache[key] = val
else:
val = cache[key]
return val
def test(lbl, start_key, end_key, lookup):
t1 = time()
for i in range(start_key, end_key + 1):
val = lookup(i)
if val != 'Value#' + str(i):
raise Exception('Ooops')
t2 = time()
print('%s keys %d - %d : %d ms' % (lbl, start_key, end_key, (t2 - t1) * 1000));
test('No cache', 1, SIZE, lambda key: real_lookup(key));
test('Cache (1st)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (2nd)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (half)', SIZE // 2 + 1, 3 * SIZE // 2, lambda key: cache_lookup(key));
test('Cache (back)', 1, SIZE, lambda key: cache_lookup(key));
<?php
define('DT', 10);
define('SIZE', 1000);
function real_lookup($key) {
usleep(DT * 1000);
return 'Value#' . $key;
}
$cache = [];
function cache_lookup($key) {
global $cache;
if(!isset($cache[$key])) {
$val = real_lookup($key);
$cache[$key] = $val;
} else {
$val = $cache[$key];
}
return $val;
}
function test($lbl, $start_key, $end_key, $lookup) {
$t1 = microtime(true);
for($i = $start_key; $i <= $end_key; $i++) {
$val = $lookup($i);
if($val != 'Value#' . $i) throw new Exception('Ooops');
}
$t2 = microtime(true);
printf("%s keys %d - %d : %d ms\n", $lbl, $start_key, $end_key, ($t2 - $t1) * 1000);
}
test('No cache', 1, SIZE, fn($key) => real_lookup($key));
test('Cache (1st)', 1, SIZE, fn($key) => cache_lookup($key));
test('Cache (2nd)', 1, SIZE, fn($key) => cache_lookup($key));
test('Cache (half)', SIZE / 2 + 1, 3 * SIZE / 2, fn($key) => cache_lookup($key));
test('Cache (back)', 1, SIZE, fn($key) => cache_lookup($key));
?>
There are some problems with the approach in the previous section though.
Biggest problem is that no entries get removed from the cache ever, so the size of the cache will keep growing over time. In many cases that will cause the application to run out of memory and crash. Not good.
Another potential problem is that in some cases the business logic considers it acceptable if the cache returns an obsolete value for X seconds after the value has been updated without the cache being updated (simple example: a DBA changed data in the database at an SQL prompt), but if no entries removed from the cache ever, then the obsolete value could be used for hours/days/months. Not good.
A cache library is basically something providing map / dictionary / associative array functionality but with mechanisms to get entries out of the cache again.
Typical mechanisms are:
An application specific policy for those can make cache usage much safer.
All examples below will try to implement a size limit of 1000 entries and expiration after 60 seconds.
Note that practically all cache libraries are a bit relaxed regarding purge of entries for performance reasons. Do not expect purge to start exactly when the size limit is reached. Do not expect entries to be purged exactly at expiration time. The cache library purges when considered convenient.
There are a ton of Cache libraries for Java.
Here we will cover:
package demo;
import java.util.function.Function;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.statistics.StatisticsGateway;
public class TestEHCache2 {
private static final int DT = 10;
private static final int SIZE = 1000;
private static final int TMO = 60;
private static String realLookup(int key) {
// simulate expensive DB operation
try {
Thread.sleep(DT);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Value#" + key;
}
private static Cache cache;
private static String cacheLookup(int key) {
Element elm = cache.get(key);
String val;
if(elm == null) {
val = realLookup(key);
cache.put(new Element(key, val));
} else {
val = (String)elm.getObjectValue();
}
return val;
}
public static void test(String lbl, int startKey, int endKey, Function<Integer, String> lookup) {
long t1 = System.currentTimeMillis();
for(int i = startKey; i <= endKey; i++) {
String val = lookup.apply(i);
if(!val.equals("Value#" + i)) throw new RuntimeException("Ooops");
}
long t2 = System.currentTimeMillis();
System.out.printf("%s keys %d - %d : %d ms\n", lbl, startKey, endKey, t2 - t1);
StatisticsGateway stat = cache.getStatistics();
System.out.printf("Statistics: %d = %d + %s (%.3f)\n", stat.cacheGetOperation().count().value(),
stat.cacheHitCount(),
stat.cacheMissCount(),
stat.cacheHitRatio());
}
public static void main(String[] args) throws Exception {
CacheManager cm = CacheManager.create();
cm.addCache("MyCache");
cache = cm.getCache("MyCache");
cache.getCacheConfiguration().setMaxEntriesLocalHeap(2 * SIZE);
cache.getCacheConfiguration().setTimeToIdleSeconds(TMO);
cache.getStatistics().cacheHitRatio(); // works as init
test("No cache", 1, SIZE, key -> realLookup(key));
test("Cache (1st)", 1, SIZE, key -> cacheLookup(key));
test("Cache (2nd)", 1, SIZE, key -> cacheLookup(key));
test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key -> cacheLookup(key));
test("Cache (back)", 1, SIZE, key -> cacheLookup(key));
cm.shutdown();
}
}
package demo;
import java.time.Duration;
import java.util.function.Function;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.core.internal.statistics.DefaultStatisticsService;
import org.ehcache.core.statistics.CacheStatistics;
public class TestEHCache3 {
private static final int DT = 10;
private static final int SIZE = 1000;
private static final int TMO = 60;
private static String realLookup(int key) {
// simulate expensive DB operation
try {
Thread.sleep(DT);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Value#" + key;
}
private static DefaultStatisticsService statserv;
private static Cache<Integer, String> cache;
private static String cacheLookup(int key) {
String val = cache.get(key);
if(val == null) {
val = realLookup(key);
cache.put(key, val);
}
return val;
}
public static void test(String lbl, int startKey, int endKey, Function<Integer, String> lookup) {
long t1 = System.currentTimeMillis();
for(int i = startKey; i <= endKey; i++) {
String val = lookup.apply(i);
if(!val.equals("Value#" + i)) throw new RuntimeException("Ooops");
}
long t2 = System.currentTimeMillis();
System.out.printf("%s keys %d - %d : %d ms\n", lbl, startKey, endKey, t2 - t1);
CacheStatistics stat = statserv.getCacheStatistics("MyCache");
System.out.printf("Statistics: %d = %d + %s (%.3f)\n", stat.getCacheGets(),
stat.getCacheHits(),
stat.getCacheMisses(),
stat.getCacheHitPercentage() / 100.0);
}
public static void main(String[] args) throws Exception {
statserv = new DefaultStatisticsService();
CacheConfiguration<Integer, String> cc = CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class, ResourcePoolsBuilder.heap(SIZE))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(TMO)))
.build();
CacheManager cm = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("MyCache", cc)
.using(statserv)
.build();
cm.init();
cache = cm.getCache("MyCache", Integer.class, String.class);
test("No cache", 1, SIZE, key -> realLookup(key));
test("Cache (1st)", 1, SIZE, key -> cacheLookup(key));
test("Cache (2nd)", 1, SIZE, key -> cacheLookup(key));
test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key -> cacheLookup(key));
test("Cache (back)", 1, SIZE, key -> cacheLookup(key));
}
}
package demo;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheStats;
public class TestGuava {
private static final int DT = 10;
private static final int SIZE = 1000;
private static final int TMO = 60;
private static String realLookup(int key) {
// simulate expensive DB operation
try {
Thread.sleep(DT);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Value#" + key;
}
private static Cache<Integer, String> cache;
private static String cacheLookup(int key) {
String val = cache.getIfPresent(key);
if(val == null) {
val = realLookup(key);
cache.put(key, val);
}
return val;
}
public static void test(String lbl, int startKey, int endKey, Function<Integer, String> lookup) {
long t1 = System.currentTimeMillis();
for(int i = startKey; i <= endKey; i++) {
String val = lookup.apply(i);
if(!val.equals("Value#" + i)) throw new RuntimeException("Ooops");
}
long t2 = System.currentTimeMillis();
System.out.printf("%s keys %d - %d : %d ms\n", lbl, startKey, endKey, t2 - t1);
CacheStats stat = cache.stats();
System.out.printf("Statistics: %d = %d + %s (%.3f)\n", stat.requestCount(),
stat.hitCount(),
stat.missCount(),
stat.hitRate());
}
public static void main(String[] args) throws Exception {
cache = CacheBuilder.newBuilder().maximumSize(SIZE).expireAfterAccess(TMO, TimeUnit.SECONDS).recordStats().build();
test("No cache", 1, SIZE, key -> realLookup(key));
test("Cache (1st)", 1, SIZE, key -> cacheLookup(key));
test("Cache (2nd)", 1, SIZE, key -> cacheLookup(key));
test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key -> cacheLookup(key));
test("Cache (back)", 1, SIZE, key -> cacheLookup(key));
}
}
package demo;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
public class TestCaffeine {
private static final int DT = 10;
private static final int SIZE = 1000;
private static final int TMO = 60;
private static String realLookup(int key) {
// simulate expensive DB operation
try {
Thread.sleep(DT);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Value#" + key;
}
private static Cache<Integer, String> cache;
private static String cacheLookup(int key) {
String val = cache.getIfPresent(key);
if(val == null) {
val = realLookup(key);
cache.put(key, val);
}
return val;
}
public static void test(String lbl, int startKey, int endKey, Function<Integer, String> lookup) {
long t1 = System.currentTimeMillis();
for(int i = startKey; i <= endKey; i++) {
String val = lookup.apply(i);
if(!val.equals("Value#" + i)) throw new RuntimeException("Ooops");
}
long t2 = System.currentTimeMillis();
System.out.printf("%s keys %d - %d : %d ms\n", lbl, startKey, endKey, t2 - t1);
CacheStats stat = cache.stats();
System.out.printf("Statistics: %d = %d + %s (%.3f)\n", stat.requestCount(),
stat.hitCount(),
stat.missCount(),
stat.hitRate());
}
public static void main(String[] args) throws Exception {
cache = Caffeine.newBuilder().maximumSize(2 * SIZE).expireAfterAccess(TMO, TimeUnit.SECONDS).recordStats().build();
test("No cache", 1, SIZE, key -> realLookup(key));
test("Cache (1st)", 1, SIZE, key -> cacheLookup(key));
test("Cache (2nd)", 1, SIZE, key -> cacheLookup(key));
test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key -> cacheLookup(key));
test("Cache (back)", 1, SIZE, key -> cacheLookup(key));
}
}
Different versions of .NET has different cache libraries:
using System;
using System.Runtime.Caching;
using System.Threading;
namespace LocalCache.TestBuiltin
{
public class Program
{
private const int DT = 10;
private const int SIZE = 1000;
private static string RealLookup(int key)
{
// simulate expensive DB operation
Thread.Sleep(DT);
return "Value#" + key;
}
private static ObjectCache cache;
private static string CacheLookup(int key)
{
string val = (string)cache[key.ToString()];
if(val == null)
{
val = RealLookup(key);
cache[key.ToString()] = val;
}
return val;
}
public static void Test(string lbl, int startKey, int endKey, Func<int, string> lookup)
{
DateTime t1 = DateTime.Now;
for(int i = startKey; i <= endKey; i++)
{
string val = lookup(i);
if(val != "Value#" + i) throw new Exception("Ooops");
}
DateTime t2 = DateTime.Now;
Console.WriteLine("{0} keys {1} - {2} : {3} ms", lbl, startKey, endKey, (long)(t2 - t1).TotalMilliseconds);
}
public static void Main(string[] args)
{
cache = MemoryCache.Default;
Test("No cache", 1, SIZE, key => RealLookup(key));
Test("Cache (1st)", 1, SIZE, key => CacheLookup(key));
Test("Cache (2nd)", 1, SIZE, key => CacheLookup(key));
Test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key => CacheLookup(key));
Test("Cache (back)", 1, SIZE, key => CacheLookup(key));
Console.ReadKey();
}
}
}
Note: no max entry limit and no max time to live limit.
It is possible to specify max size in config.app though:
<configuration>
...
<system.runtime.caching>
<memoryCache>
<namedCaches>
<add name="Default"
cacheMemoryLimitMegabytes="128"
physicalMemoryLimitPercentage="2" />
</namedCaches>
</memoryCache>
</system.runtime.caching>
...
</configuration>
Newer .NET with Microsoft Extension:
using System;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;
namespace LocalCache.TestCore
{
public class Program
{
private const int DT = 10;
private const int SIZE = 1000;
private static string RealLookup(int key)
{
// simulate expensive DB operation
Thread.Sleep(DT);
return "Value#" + key;
}
private static IMemoryCache cache;
private static string CacheLookup(int key)
{
string val = (string)cache.Get(key);
if (val == null)
{
val = RealLookup(key);
cache.Set(key, val);
}
return val;
}
public static void Test(string lbl, int startKey, int endKey, Func<int, string> lookup)
{
DateTime t1 = DateTime.Now;
for (int i = startKey; i <= endKey; i++)
{
string val = lookup(i);
if (val != "Value#" + i) throw new Exception("Ooops");
}
DateTime t2 = DateTime.Now;
Console.WriteLine("{0} keys {1} - {2} : {3} ms", lbl, startKey, endKey, (long)(t2 - t1).TotalMilliseconds);
}
public static void Main(string[] args)
{
cache = cache = new MemoryCache(new MemoryCacheOptions());
Test("No cache", 1, SIZE, key => RealLookup(key));
Test("Cache (1st)", 1, SIZE, key => CacheLookup(key));
Test("Cache (2nd)", 1, SIZE, key => CacheLookup(key));
Test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key => CacheLookup(key));
Test("Cache (back)", 1, SIZE, key => CacheLookup(key));
}
}
}
Note: this example does not impose any limits.
But it is possible to impose limits:
using System;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;
namespace LocalCache.TestCoreAdv
{
public class Program
{
private const int DT = 10;
private const int UNIT = 1;
private const int SIZE = 1000 * UNIT;
private const int TMO = 60;
private static string RealLookup(int key)
{
// simulate expensive DB operation
Thread.Sleep(DT);
return "Value#" + key;
}
private static IMemoryCache cache;
private static MemoryCacheEntryOptions entryoptions;
private static string CacheLookup(int key)
{
string val = (string)cache.Get(key);
if (val == null)
{
val = RealLookup(key);
cache.Set(key, val, entryoptions);
}
return val;
}
public static void Test(string lbl, int startKey, int endKey, Func<int, string> lookup)
{
DateTime t1 = DateTime.Now;
for (int i = startKey; i <= endKey; i++)
{
string val = lookup(i);
if (val != "Value#" + i) throw new Exception("Ooops");
}
DateTime t2 = DateTime.Now;
Console.WriteLine("{0} keys {1} - {2} : {3} ms", lbl, startKey, endKey, (long)(t2 - t1).TotalMilliseconds);
}
public static void Main(string[] args)
{
MemoryCacheOptions options = new MemoryCacheOptions();
options.SizeLimit = SIZE;
options.TrackStatistics = true;
cache = new MemoryCache(options);
entryoptions = new MemoryCacheEntryOptions();
entryoptions.SetSize(UNIT);
entryoptions.SetSlidingExpiration(TimeSpan.FromSeconds(TMO));
Test("No cache", 1, SIZE, key => RealLookup(key));
Test("Cache (1st)", 1, SIZE, key => CacheLookup(key));
Test("Cache (2nd)", 1, SIZE, key => CacheLookup(key));
Test("Cache (half)", SIZE / 2 + 1, 3 * SIZE / 2, key => CacheLookup(key));
Test("Cache (back)", 1, SIZE, key => CacheLookup(key));
}
}
}
Note: entry size is not 1 or number of bytes but an arbitrarily assigned unit.
Python has several cache libraries available:
It is worth noting that Python has a unique annotation/attribute based approach to caching the requires a lot less code than in other languages.
from time import sleep, time
from functools import lru_cache
DT = 10
SIZE = 1000
def real_lookup(key):
sleep(DT / 1000.0)
return 'Value#' + str(key)
@lru_cache(SIZE)
def cache_lookup(key):
return real_lookup(key)
def test(lbl, start_key, end_key, lookup):
t1 = time()
for i in range(start_key, end_key + 1):
val = lookup(i)
if val != 'Value#' + str(i):
raise Exception('Ooops')
t2 = time()
print('%s keys %d - %d : %d ms' % (lbl, start_key, end_key, (t2 - t1) * 1000));
test('No cache', 1, SIZE, lambda key: real_lookup(key));
test('Cache (1st)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (2nd)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (half)', SIZE // 2 + 1, 3 * SIZE // 2, lambda key: cache_lookup(key));
test('Cache (back)', 1, SIZE, lambda key: cache_lookup(key));
Note: no max time to live specified.
from time import sleep, time
from cachetools.func import ttl_cache
DT = 10
SIZE = 1000
TMO = 60
def real_lookup(key):
sleep(DT / 1000.0)
return 'Value#' + str(key)
@ttl_cache(SIZE, TMO)
def cache_lookup(key):
return real_lookup(key)
def test(lbl, start_key, end_key, lookup):
t1 = time()
for i in range(start_key, end_key + 1):
val = lookup(i)
if val != 'Value#' + str(i):
raise Exception('Ooops')
t2 = time()
print('%s keys %d - %d : %d ms' % (lbl, start_key, end_key, (t2 - t1) * 1000));
stat = cache_lookup.cache_info()
print('%d + %d' % (stat.hits, stat.misses))
test('No cache', 1, SIZE, lambda key: real_lookup(key));
test('Cache (1st)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (2nd)', 1, SIZE, lambda key: cache_lookup(key));
test('Cache (half)', SIZE // 2 + 1, 3 * SIZE // 2, lambda key: cache_lookup(key));
test('Cache (back)', 1, SIZE, lambda key: cache_lookup(key));
The variety of PHP execution models make even no-distributed cache in PHP tricky.
One solution is the APCu cache library.
<?php
define('DT', 10);
define('SIZE', 1000);
define('TMO', 60);
function real_lookup($key) {
usleep(DT * 1000);
return 'Value#' . $key;
}
function cache_lookup($key) {
$val = apcu_fetch((string)$key);
if(!$val) {
$val = real_lookup($key);
apcu_store((string)$key, $val, TMO);
}
return $val;
}
function test($lbl, $start_key, $end_key, $lookup) {
$t1 = microtime(true);
for($i = $start_key; $i <= $end_key; $i++) {
$val = $lookup($i);
if($val != 'Value#' . $i) throw new Exception('Ooops');
}
$t2 = microtime(true);
printf("%s keys %d - %d : %d ms\n", $lbl, $start_key, $end_key, ($t2 - $t1) * 1000);
$stat = apcu_cache_info();
printf("%d + %d\n", $stat['num_hits'], $stat['num_misses']);
}
test('No cache', 1, SIZE, fn($key) => real_lookup($key));
test('Cache (1st)', 1, SIZE, fn($key) => cache_lookup($key));
test('Cache (2nd)', 1, SIZE, fn($key) => cache_lookup($key));
test('Cache (half)', SIZE / 2 + 1, 3 * SIZE / 2, fn($key) => cache_lookup($key));
test('Cache (back)', 1, SIZE, fn($key) => cache_lookup($key));
?>
Note: no max number entries specified.
But size can be limited in php.ini:
...
[apcu]
apc.enable = 1
apc.enable_cli = 1
apc.shm_size = 128M
apc.ttl=60
apc.serializer=php
...
| Version | Date | Description |
|---|---|---|
| 1.0 | April 22nd 2026 | Initial version |
See list of all articles here
Please send comments to Arne Vajhøj