La memoization è una tecnica di ottimizzazione che consiste nel memorizzare i risultati di una funzione in base ai suoi argomenti, in modo da evitare di ricalcolarli quando la funzione viene chiamata di nuovo con gli stessi parametri.
Perché usare la memoization in PHP
In PHP la memoization è utile soprattutto quando:
- hai funzioni costose (ad esempio accessi al database, chiamate HTTP, elaborazioni numeriche complesse);
- la funzione è deterministica (a parità di input restituisce sempre lo stesso risultato);
- gli input possibili non sono infiniti o troppo variabili;
- vuoi ottimizzare il tempo di risposta all’interno della stessa richiesta PHP.
È importante ricordare che, per la natura di PHP (soprattutto in contesti web come Apache o FPM), lo stato in memoria vive solo per la durata della richiesta. La memoization quindi, nella sua forma più semplice, agisce a livello di richiesta e non è un cache globale condivisa tra più richieste.
Esempio base: memoization con variabile statica
Un primo modo semplice per implementare la memoization è usare una variabile statica interna alla funzione.
<?php
function fibonacci(int $n): int
{
static $cache = [];
if (isset($cache[$n])) {
return $cache[$n];
}
if ($n <= 1) {
$cache[$n] = $n;
return $n;
}
$result = fibonacci($n - 1) + fibonacci($n - 2);
$cache[$n] = $result;
return $result;
}
echo fibonacci(40); // Molto più veloce con memoization
Cosa succede in questo esempio
static $cache = [];definisce un array che viene inizializzato solo la prima volta.- Se il valore è già in
$cache, viene restituito subito. - Altrimenti la funzione calcola il risultato, lo salva in cache e lo restituisce.
Questo pattern è molto semplice ma è limitato a quella specifica funzione e a una sola firma di parametri.
Creare una funzione di memoization riutilizzabile
Possiamo generalizzare la logica in una funzione memoize che accetta
una callable e restituisce una nuova callable memoizzata.
<?php
/**
* Restituisce una versione memoizzata della callable passata.
*
* @param callable $fn
* @return callable
*/
function memoize(callable $fn): callable
{
$cache = [];
return function (...$args) use (&$cache, $fn) {
// Creiamo una chiave unica in base agli argomenti
$key = md5(serialize($args));
if (array_key_exists($key, $cache)) {
return $cache[$key];
}
$result = $fn(...$args);
$cache[$key] = $result;
return $result;
};
}
// Esempio di funzione costosa
function slow_sum(int $a, int $b): int
{
// Simuliamo un'operazione pesante
usleep(500000); // 0.5 secondi
return $a + $b;
}
// Creiamo la versione memoizzata
$fast_sum = memoize('slow_sum');
echo $fast_sum(2, 3); // Prima chiamata: lenta
echo $fast_sum(2, 3); // Seconda chiamata: istantanea, valore preso dalla cache
Dettagli importanti
-
Serializzazione degli argomenti: usiamo
serializeper convertire gli argomenti in una stringa che faccia da chiave. Questo ci permette di gestire numeri, stringhe, array e oggetti serializzabili. -
Uso di
md5: il risultato diserializepuò essere lungo; applicaremd5genera una chiave di lunghezza fissa, più compatta. Per molti casi d’uso è più che sufficiente. - Scope della cache: la cache è chiusa nella closure, quindi è privata per quella funzione memoizzata.
Memoization con funzioni anonime e closure
La memoization diventa molto naturale quando si lavora con funzioni anonime in PHP.
<?php
$slowOperation = function (string $value): string {
// Simuliamo una chiamata esterna pesante
usleep(300000); // 0.3 secondi
return strtoupper($value);
};
$memoizedSlowOperation = memoize($slowOperation);
echo $memoizedSlowOperation('ciao'); // prima volta: lenta
echo $memoizedSlowOperation('ciao'); // seconda volta: cache
Grazie a memoize possiamo trasformare qualunque callable
in una versione memoizzata senza modificare il codice originale.
Aggiungere una politica di invalidazione della cache
A volte non vogliamo che i risultati restino in cache per sempre. Possiamo quindi aggiungere una scadenza (TTL) alle voci della cache.
<?php
/**
* Memoization con TTL (time to live) in secondi.
*
* @param callable $fn
* @param int $ttl
* @return callable
*/
function memoize_with_ttl(callable $fn, int $ttl): callable
{
$cache = [];
return function (...$args) use (&$cache, $fn, $ttl) {
$key = md5(serialize($args));
$now = time();
if (isset($cache[$key])) {
[$value, $expiresAt] = $cache[$key];
if ($expiresAt >= $now) {
return $value;
}
// Scaduto: rimuoviamo la voce
unset($cache[$key]);
}
$result = $fn(...$args);
$cache[$key] = [$result, $now + $ttl];
return $result;
};
}
In questo modo ogni risultato è valido solo per un certo numero di secondi e viene ricalcolato automaticamente dopo la scadenza.
Memoization e oggetti: cache per metodi di istanza
Possiamo applicare la memoization anche a metodi di classe. Per esempio, possiamo creare un trait riutilizzabile che fornisce un semplice sistema di memoization per metodi.
<?php
trait Memoizable
{
protected array $memoizationCache = [];
/**
* Esegue un'operazione memoizzata legata al nome del metodo.
*
* @param string $method
* @param callable $fn
* @param mixed ...$args
* @return mixed
*/
protected function memoizeMethod(string $method, callable $fn, ...$args): mixed
{
$key = $method . ':' . md5(serialize($args));
if (array_key_exists($key, $this->memoizationCache)) {
return $this->memoizationCache[$key];
}
$result = $fn(...$args);
$this->memoizationCache[$key] = $result;
return $result;
}
}
class ProductService
{
use Memoizable;
public function getExpensiveData(int $productId): array
{
return $this->memoizeMethod(__FUNCTION__, function () use ($productId) {
// Simuliamo query pesante
usleep(400000);
return [
'id' => $productId,
'name' => 'Prodotto ' . $productId,
'price' => 99.90,
];
}, $productId);
}
}
$service = new ProductService();
// Prima volta: esegue la "query"
$data1 = $service->getExpensiveData(10);
// Seconda volta: restituisce dalla cache interna all'istanza
$data2 = $service->getExpensiveData(10);
Vantaggi di questa soluzione
- La cache è legata all’istanza dell’oggetto.
- Possiamo memoizzare più metodi usando lo stesso trait.
- Il codice rimane pulito e concentrato sulla logica di dominio.
Memoization in combinazione con una cache esterna
Finora abbiamo visto solo cache in memoria valide per la singola richiesta. Se vogliamo che il risultato sia riutilizzabile anche tra richieste diverse, dobbiamo appoggiarci a un sistema di cache esterno come Redis, Memcached o APCu.
Un esempio con APCu potrebbe essere il seguente.
<?php
/**
* Memoization che usa APCu come backend cache.
*
* @param callable $fn
* @param int $ttl
* @param string $prefix
* @return callable
*/
function memoize_apcu(callable $fn, int $ttl = 60, string $prefix = 'memo_'): callable
{
return function (...$args) use ($fn, $ttl, $prefix) {
$key = $prefix . md5(serialize($args));
$success = false;
$value = apcu_fetch($key, $success);
if ($success) {
return $value;
}
$result = $fn(...$args);
apcu_store($key, $result, $ttl);
return $result;
};
}
In questo caso la cache viene mantenuta da APCu e può essere condivisa tra più richieste sullo stesso server, migliorando ulteriormente le prestazioni.
Quando evitare la memoization
La memoization non è sempre la scelta giusta. È meglio evitarla quando:
- gli input sono estremamente variabili e difficilmente si ripetono;
- la funzione ha effetti collaterali (scrittura su file, invio email, ecc.);
- la quantità di memoria disponibile è limitata e il rischio di riempirla è alto;
- i dati cambiano molto spesso e il rischio di avere risultati obsoleti è significativo.
Linee guida pratiche
- Identifica le funzioni pure e costose: sono le migliori candidate per la memoization.
- Progetta una strategia di chiavi robusta (serialize, JSON, ecc.).
- Considera se una cache per richiesta è sufficiente o se serve una cache condivisa.
- Implementa una qualche forma di invalidazione o TTL se i dati possono cambiare.
- Misura le prestazioni prima e dopo per verificare il beneficio reale.
Conclusione
La memoization in PHP è una tecnica relativamente semplice da implementare ma può portare a miglioramenti significativi nelle prestazioni quando applicata correttamente. Partendo da una semplice variabile statica fino ad arrivare a soluzioni più avanzate con trait e backend di cache esterni, hai a disposizione un ventaglio di strumenti flessibile per ottimizzare il tuo codice.
Il passo successivo è identificare nel tuo progetto le funzioni più costose e valutare dove una strategia di memoization possa avere il maggior impatto positivo.