Introduzione
In questo articolo vedremo come sia possibile ottimizzare una tabella di database sfruttando, nella gestione dei dati memorizzati, le operazioni tra numeri binari. Per tranquillizzare quanti alle parole "numeri binari" siano tentati di abbandonare queste pagine, anticiperò che la trattazione sarà sia teorica che pratica. Inoltre l'applicazione dei concetti esposti risulterà molto semplice... tanto da poter dimenticare, dopo poco, la teoria su cui si poggia.
Immaginiamo di voler raccogliere le preferenze sportive dei nostri utenti fornendo loro la possibilità di scegliere una o più opzioni tra calcio, basket, tennis, sci e volley. La soluzione più comune consiste nel predisporre una tabella con tanti campi quante sono le possibili scelte. Tali campi funzioneranno da flag, ovvero saranno impostati a vero o falso a seconda che l'utente esprima o meno una preferenza per questa o quella disciplina sportiva. Un simile approccio risulta per alcuni aspetti poco flessibile e difficilmente scalabile. Ogniqualvolta si decida di ampliare il numero di opzioni disponibili si dovrà aggiungere un nuovo campo.
Ma, a pensarci bene, un numero intero non è altro che una serie di bit impostati a 0 o 1, quindi, una perfetta sequenza di flag adatta a memorizzare le informazioni desiderate. Sulla base di quest'idea potremmo sostituire i cinque campi previsti con un unico campo numerico intero. Chiaramente non dovrà risultare eccessivamente complicato manipolare i bit altrimenti pagheremmo l'ottimizzazione del database con un maggior appesantimento del codice PHP.
Le nozioni fondamentali
Per meglio inquadrare il discorso partiamo dalle basi. Come si è detto nel sistema binario un numero intero viene rappresentato mediante una sequenza di cifre (bit) che possono assumere solo i valori 1 o 0. 00000011 è un esempio di numero binario. In questo tipo di notazione la posizione delle cifre viene numerata da destra a sinistra a partire da zero. Il primo bit, quello di posizione zero, viene detto meno significativo mentre l'ultimo, quello più a sinistra, si definisce più significativo. La tabella seguente presenta gli elementi essenziali della rappresentazione binaria.
posizione | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
potenza decimale | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 |
valore decimale | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
In base allo schema proposto 00000011 corrisponde al numero decimale 3, infatti sono posti a 1 solo i bit di posizione zero e uno, con un semplice calcolo si ottiene (21 + 20) = (2 + 1) = 3; analogamente 00000100 corrisponde a 4 infatti l'unico bit posto a 1 è quello di posizione due, da cui 22= 4 e così via.
Torniamo al nostro esempio e cominciamo con l'assegnare un valore numerico a ciascuna opzione secondo il seguente schema:
calcio: | 1 |
basket: | 2 |
tennis: | 4 |
sci: | 8 |
volley: | 16 |
che corrisponde in termini di bit a:
calcio: | 00000001 |
basket: | 00000010 |
tennis: | 00000100 |
sci: | 00001000 |
volley: | 00010000 |
Come si può notare assegnando tali valori impostiamo ad 1 uno solo dei bit della sequenza, ovvero impostiamo il famoso flag. Per quanto riguarda la tabella di database, riferendoci a MySQL, potremo utilizzare un campo tinyint unsigned per memorizzare le preferenze degli utenti. Questo ci garantisce un range di valori da 0 a 255. Se prevedete un numero maggiore di opzioni utilizzate un campo intero più grande.
Per semplicità di esposizione creiamo un array corrispondente alle diverse discipline sportive. In una situazione reale sarà più comune estrarli mediante una query da una tabella del database destinata a contenere tali valori in modo che siano facilmente amministrabili.
<?php
$choice = array(
'calcio' => 1,
'basket' => 2,
'tennis' => 4,
'sci' => 8,
'volley' => 16
);
?>
Vediamo di seguito le principali operazioni con cui dovremo confrontarci adottando una simile soluzione.
Impostare un bit ad uno
Per settare un bit a 1 in una fissata posizione facciamo ricorso all'operazione OR inclusivo: (numero1 | numero2). Questo operatore analizza bit a bit i numeri e fornisce come risultato 1 solo se almeno uno dei due bit è pari a 1. Consideriamo come esempio il numero 5, la cui rappresentazione binaria è 00000101, e vediamo come impostare ad 1 anche il bit di posizione uno (ricordo che si parte sempre da zero nel numerare la posizione). Dovremo semplicemente eseguire un OR con la sequenza 00000010 (numero decimale 2), che presenta una serie di zeri tranne nella posizione uno. In altri termini l'operazione risulta: 5 | 2 = 7
5 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | OR |
2 | -> | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | = |
7 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
Se le cose possono sembrare un po' complicate nella teoria l'applicazione pratica risulta molto più semplice. Supponiamo di gestire le preferenze di un utente memorizzate nella variabile $user_prefs e di voler impostare la preferenza relativa al tennis. Basterà l'istruzione seguente:
<?php
$user_prefs = $user_prefs | $choice['tennis'];
// con notazione compatta
$user_prefs |= $choice['tennis'];
?>
Tutto qui!
Verificare l'impostazione di un bit
Per controllare se un bit, in una fissata posizione, sia impostato ad 1 ricorriamo all'operazione logica AND: (numero1 & numero2). Tale operatore analizza bit a bit i numeri e fornisce come risultato 1 solo se entrambi i bit sono pari ad 1. Prendiamo sempre in considerazione il numero 5, 00000101. Per verificare se il bit di posizione due (partiamo sempre da zero) valga 1 eseguiamo un AND con la sequenza 00000100 che corrisponde al numero decimale 4.
L'operazione risulta: 5 & 4 = 4
5 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | AND |
4 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | = |
4 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
Analogamente se volessimo verificare se il bit di posizione uno è impostato ad 1 dovremmo eseguire l'operazione 5 & 2 il cui risultato è 0 ad indicare che il test ha dato esito negativo. Vediamo nel caso del nostro esempio come verificare se l'utente ha scelto tennis o meno:
<?php
if($user_prefs & $choice['tennis'])
{
// ha scelto tennis
}
else
{
// non ha scelto tennis
}
?>
Impostare un bit a zero
Per azzerare un bit useremo due operatori NOT (o INVERSE) e AND: (numero1 & ~numero2). Se nel numero 5, 00000101, vogliamo azzerare il bit di posizione due dovremo utilizzare 00000100, cioé il decimale 4.
Infatti 5 & ~4 = 1, ovvero invertiamo i bit del numero 4 ~00000100 = 11111011 in modo tale che applicando l'AND tutti i bit del numero 5 rimangano invariati, tranne quello di posizione 2 che viene forzato a 0.
5 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | AND |
~4 | -> | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | = |
1 | -> | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Tornando al nostro esempio per disabilitare il bit corrispondente al tennis procediamo così:
<?php
$user_prefs &= ~$choice['tennis'];
?>
Invertire un bit
Per questa operazione ricorriamo all'operatore XOR (OR esclusivo): (numero1 ^ numero2). Nel confronto bit a bit questo operatore fornisce 1 solo se uno dei due bit è impostato a 1 e l'altro a 0. Negli altri casi produce 0. Se nel numero 5, 00000101, vogliamo invertire il bit di posizione uno dovremo eseguire un XOR con 00000010, ovvero il decimale 2.
Infatti 5 ^ 2 = 7, vediamo l'operazione binaria:
5 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | XOR |
2 | -> | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | = |
7 | -> | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
Continuando con l'esempio per invertire la scelta corrispondente al tennis, ovvero impostare la preferenza se non lo è o toglierla se è impostata, procediamo così:
<?php
$user_prefs ^= $choice['tennis'];
?>
Schema riassuntivo
La seguente tabella riassume le operazioni sopra descritte:
impostare una preferenza | $user_prefs |= $valore |
eliminare una preferenza | $user_prefs &= ~$valore |
invertire una preferenza | $user_prefs ^= $valore |
verificare una preferenza | $user_prefs & $valore |
Conclusioni
Il semplice esempio sulla base del quale si è sviluppato l'articolo, ha permesso di introdurre quelli che in PHP sono conosciuti come bitwise operators potete trovare la documentazione ufficiale all'indirizzo http://www.php.net/manual/en/language.operators.bitwise.php. Per brevità non sono stati trattati gli operatori di shift destro e sinistro.
Vi invito a fare attenzione alle relazioni di precedenza tra operatori: gli operatori binari vengono applicati dopo gli operatori aritmetici e di comparazione. Questo può indurre in errore, ad esempio if(4 & 5 ! = 0) anziché verificare positivamente che il bit di posizione due vale 1 nel numero 5, prima verifica che 5 sia diverso da zero il ché è vero (ed equivale a 1), poi calcola 4 & 1 = 0. In caso di dubbio è sempre meglio ricorrere alle parentesi: if( (4 & 5) ! = 0). Per maggiori informazioni http://www.php.net/manual/en/language.operators.php#language.operators.precedence.
In chiusura vorrei accennare al fatto che questa tecnica talora viene utilizzata per gestire in maniera sofisticata i permessi di accesso ad un'area riservata, un esempio lo trovate nell'articolo Gestire gli utenti con PHP: i permessi di Gabriele Farina. Infatti è possibile far sí che un livello di privilegio erediti i permessi di un altro. Brevemente si consideri la situazione seguente:
capo reparto: | 00000111 | -> 7 |
collaudatore: | 00000010 | -> 2 |
meccanico: | 00000001 | -> 1 |
Come si vede meccanico e collaudatore hanno permessi distinti mentre il capo reparto eredita automaticamente i permessi di entrambi avendo i corrispondenti bit di posizione zero e uno impostati a 1. Se un meccanico tenta di accedere ad un'area in cui vengono richiesti i permessi di collaudatore, valore 2, verrà bloccato infatti 1 & 2 = 0. Al contrario un capo reparto no, infatti 7 & 2 = 2 che è il valore richiesto. Come si può notare le applicazioni possono essere varie sta' a voi trovarne di nuove.