Aliasing (programmazione)
In programmazione, il termine aliasing indica la situazione in cui una stessa posizione di memoria è associata a nomi simbolici diversi all'interno di un programma.
Significato
modificaTutti i simboli coinvolti (in inglese aliased names) fanno da "ponte" verso una stessa zona di memoria: di conseguenza, le informazioni scritte servendosi di uno di essi sono "visibili" tramite tutti gli altri, perché i dati interessati dall'operazione di scrittura non si trovano direttamente nel simbolo, ma in una zona di memoria "comune".
Esempi
modificaNel seguito è presentato un elenco (non esaustivo) di meccanismi che permettono di avere aliasing nei linguaggi di programmazione che li supportano.
Puntatori
modificaUn modo semplice di realizzare un alias in C++ è servirsi di un puntatore. Ad esempio,
int i = 0;
int* pi = &i;
La zona di memoria occupata dalla variabile i è accessibile tramite due aliases: la variabile i e la variabile puntatore pi. Esse sono due variabili distinte e indipendenti l'una dall'altra, però il valore contenuto in una di esse - il puntatore - è un numero speciale, in particolare un indirizzo di memoria, che tramite l'operatore di dereferenziazione permette di leggere e scrivere nella zona di memoria puntata dall'indirizzo &i. Di conseguenza questo codice scrive il valore 5 sulla console:
* pi = 5;
cout << i;
in quanto l'istruzione cout << i legge, tramite la variabile i, il valore 5 inserito al suo interno tramite il puntatore pi.
- Strict aliasing rule
Lo standard ISO per il linguaggio di programmazione C (inclusa la nuova versione C99, vedere sezione 6.5, paragrafo 7) proibisce, con alcune eccezioni, che puntatori di tipi indipendenti tra loro referenzino la stessa posizione di memoria. Questa regola è nota come strict aliasing.
Array
modificaAlcuni linguaggi di programmazione, come il C e il C++, non impongono alcun controllo sugli indici utilizzati per accedere alle celle degli array (il cosiddetto array bounds checking). In altre parole, è possibile che un programma acceda a celle dell'array che non gli appartengono. Sotto particolari condizioni, questo meccanismo permette di accedere ad una variabile tramite l'array.
Ad esempio, se a è un array di interi di x celle, memorizzato in una certa posizione di memoria, e nelle posizioni immediatamente successive è memorizzata una variabile i, di tipo intero, allora è possibile accedere ad i usando la notazione a[x].[1] Quindi, nell'esempio, si può accedere ad i con la scrittura a[4].
Questo è possibile perché gli array sono realizzati in realtà come blocchi di posizioni di memoria contigue, così che il programma memorizza internamente un puntatore p alla prima di queste posizioni di memoria, e accedere all'array con indice k significa semplicemente accedere alla memoria presente alla posizione p + (k * s) dove s è la dimensione di una cella dell'array (ovvero il numero di celle di memoria occupate da una singola cella dell'array).
Risulta evidente che questo meccanismo è reso possibile dalla particolare implementazione del compilatore in uso: ad esempio, implementazioni differenti potrebbero porre dello spazio tra gli array e le variabili allocate sullo stack delle invocazioni a funzione, così da allineare le variabili a posizioni di memoria che siano multiple della word specifica dell'architettura del sistema in uso. Questo è possibile perché il C non impone una regola generale che stabilisca come debbano essere disposti i dati allocati in memoria.[2]
Unioni
modificaLe unioni sovrappongono variabili differenti in una stessa posizioni di memoria. L'uso delle unioni per accedere ad una stessa posizione di memoria tramite variabili differenti è tuttavia sconsigliato.
Programmazione orientata agli oggetti
modificaNei linguaggi di programmazione orientati agli oggetti, il fenomeno dell'aliasing si verifica quando più variabili contengono copie di uno stesso riferimento ad un certo oggetto. Si parla di riferimenti e non necessariamente di puntatori perché questo meccanismo si trova anche in quei linguaggi che non fanno uso di puntatori, come il Java.
C++
modificaIn C++ gli oggetti sono "contenuti" direttamente nelle variabili, nel senso che in ogni variabile sono memorizzati proprio i dati che compongono l'oggetto e non già un puntatore ad esso. La copia di una variabile di tipo oggetto in un'altra sovrascrive i dati contenuti nella prima con i dati contenuti nella seconda, tuttavia mantiene distinte le rispettive zone di memoria; di conseguenza, le successive operazioni di scrittura effettuate su una variabile oggetto interessano solo quella copia specifica e non influenzano né l'originale, né le altre copie.
Per ottenere l'aliasing, bisogna definire esplicitamente un puntatore all'area di memoria occupata dall'oggetto. Essa può essere associata ad una variabile (nel qual caso il puntatore deve referenziare la variabile) o essere stata allocata dinamicamente, cioè tramite l'operatore new. In entrambi i casi, il puntatore permetterà l'accesso all'oggetto tramite l'operatore ->
invece che l'operatore .
In alternativa, si può progettare appositamente la classe interessata, in modo che le diverse copie di uno stesso oggetto utilizzino internamente dei puntatori ad una zona di memoria comune (allocata tramite new): ad essere soggetta ad aliasing sarà però questa zona di memoria e non già gli oggetti stessi, i quali restano del tutto distinti tra loro.
Java
modificaIn Java vige la seguente regola: non si accede mai direttamente ad un oggetto, ma sempre e solo tramite un reference che punta ad esso. A livello concettuale, i reference possono essere considerati come puntatori utilizzabili con determinate restrizioni: ad esempio, non supportano le operazioni aritmetiche, non possono essere usati per deallocare esplicitamente la memoria, ed è possibile referenziare solo oggetti (non esistono "reference a variabili").
Una variabile non contiene direttamente i dati dell'oggetto, ma un reference che punta all'oggetto stesso. Di conseguenza, copiare una variabile significa in realtà copiare il reference, e ovviamente questo significa creare due alias che puntano all'oggetto. In effetti, l'operatore x.y ha proprio il significato di accedi al membro y dell'oggetto puntato dal reference contenuto nella variabile x.
Nel codice seguente dapprima si crea una lista, la si modifica tramite una variabile stringhe (aggiungendo un elemento) e, tramite l'altra variabile listaDiStringhe, si "osserva" la modifica:
ArrayList<String> stringhe = new ArrayList<String>();
ArrayList<String> listaDiStringhe = stringhe;
// Aggiungo una stringa alla lista tramite il reference contenuto nella variabile stringhe:
stringhe.add("abcde");
// Quindi la riga seguente scriverà "abcde" su schermo:
System.out.println(stringhe.get(0));
// stringhe e listaDiStringhe si riferiscono allo stesso oggetto, di conseguenza
// anche la riga seguente scriverà "abcde" su schermo:
System.out.println(listaDiStringhe.get(0));
Vantaggi
modificaIn alcuni casi l'aliasing viene usato intenzionalmente. Ad esempio, esso è d'uso comune in Fortran; in altri linguaggi può essere usato apposta per trarne determinati benefici.
Il Perl definisce per alcuni costrutti, come il for-each, un comportamento che sfrutta le caratteristiche dell'aliasing, il che permette di modificare facilmente determinate strutture dati con codice più sintetico e intuitivo. Ad esempio:
my @array = (1, 2, 3);
foreach my $element (@array) {
# $element fa da ''alias'' per ciascuno degli
# elementi di @array, uno per ogni ciclo,
# quindi incrementare $element nell'i-esimo ciclo
# significa modificare l'i-esimo elemento
# dell'array.
$element++;
}
print "@array \n";
Questo codice stampa come risultato la riga
2 3 4
Se non si vuole che l'interno del ciclo modifichi l'array, si può copiare il contenuto dell'indice in un'altra variabile e svolgere le operazioni su questa copia.
Svantaggi
modificaIn determinati casi l'aliasing delle variabili può portare dei problemi in fase di esecuzione del programma. Seguono alcuni esempi comuni.
Effetti collaterali
modificaL'aliasing dei parametri passati a una subroutine permette a quest'ultima di modificarne il valore, generando così un effetto collaterale (in genere non desiderato).
Aliasing e ottimizzazione
modificaL'aliasing spesso ostacola o rende più complesso il compito di ottimizzare il codice eseguibile affidato a compilatori o programmi appositi.
- Inlining dei valori
Ad esempio, si supponga di avere nel programma una variabile x a cui si assegna il valore 5. Il compilatore potrebbe ottimizzare le istruzioni successive sostituendo la costante 5 all'istruzione che richiede la lettura del valore di x. Tuttavia, se il linguaggio di programmazione permette di usare i puntatori, fare questo non è più possibile: infatti il programma potrebbe accedere in scrittura alla memoria occupata da x tramite un puntatore y, ad esempio tramite un'istruzione *y = 10, e ciò produrrebbe codice eseguibile errato, perché quelle che appaiono nel codice sorgente come letture della variabile x non risentirebbero del fatto che il contenuto della variabile è cambiato da 5 a 10.
Per questo motivo, il compilatore deve svolgere controlli aggiuntivi e raccogliere informazioni sui puntatori che sono definiti nel programma, e chiedersi: x può essere un alias di *y? Se la risposta è no, allora l'ottimizzazione può essere svolta senza problemi.
- Instruction reordering
Un'altra tecnica per ottimizzare i programmi è cambiare l'ordine delle singole istruzioni che sono eseguite in una subroutine (facendo in modo che i cambiamenti non siano visibili dall'esterno della stessa: il codice ottimizzato produce gli stessi risultati ma con una differente sequenza di istruzioni). Se il compilatore stabilisce che x non è un alias di *y, allora il codice che legge o scrive su x può essere spostato prima o dopo dell'assegnazione *y = 10, nel caso in cui ciò favorisca lo instruction scheduling o l'esecuzione di ulteriori ottimizzazioni.
XOR swap
modificaIl noto algoritmo chiamato in inglese XOR swap scambia tra loro due variabili numeriche senza l'ausilio di una terza variabile temporanea. L'algoritmo viene applicato su due ingressi numerici x e y, eseguendo in sequenza questi passi:
- calcola il valore di x XOR y ed inseriscilo nella variabile x
- calcola il valore di x XOR y ed inseriscilo nella variabile y
- calcola il valore di x XOR y ed inseriscilo nella variabile x
Questo algoritmo funziona solo se i valori x e y sono memorizzati in posizioni di memoria distinte, ovvero se non c'è aliasing: in caso contrario, la prima delle tre istruzioni inserisce 0 nella memoria comune, con il risultato che x e y valgono entrambi zero al termine del terzo passo. Ciò rende necessario applicare un controllo sugli ingressi, come nella seguente implementazione in linguaggio C:
void xorSwap (int *x, int *y) {
if (*x != *y) {
*x ^= *y;
*y ^= *x;
*x ^= *y;
}
}
Nel codice presentato si confrontano i valori dei due ingressi, invece che i rispettivi puntatori, in quanto lo scambio di due valori uguali è evidentemente superfluo. Una versione alternativa, che fa uso del passaggio dei parametri per riferimento invece che per puntatore:
void xorSwap (int& x, int& y) {
if (x != y) {
x ^= y;
y ^= x;
x ^= y;
}
}
Note
modificaCollegamenti esterni
modifica- (EN) Denis Howe, Aliasing, in Free On-line Dictionary of Computing. Disponibile con licenza GFDL