Perché i compilatori non inseriscono automaticamente deallocazioni?


63

In lingue come C, il programmatore dovrebbe inserire chiamate gratuitamente. Perché il compilatore non lo fa automaticamente? Gli umani lo fanno in un ragionevole lasso di tempo (ignorando i bug), quindi non è impossibile.

EDIT: per riferimento futuro, ecco un'altra discussione che ha un esempio interessante.


125
E questo, ragazzi, è il motivo per cui vi insegniamo la teoria della calcolabilità. ;)
Raffaello

7
Questo non è un problema di calcolabilità poiché gli umani non possono decidere in tutti i casi. È un problema di completezza; le dichiarazioni di deallocazione contengono informazioni che, se rimosse, non possono essere completamente recuperate dall'analisi a meno che tale analisi non includa informazioni sull'ambiente di distribuzione e l'operazione prevista, che il codice sorgente C non contiene.
Nat

41
No, è un problema di calcolabilità. E ' indecidibile se un determinato pezzo di memoria dovrebbe essere deallocato. Per un programma fisso, nessun input dell'utente o altre interferenze esterne.
Andrej Bauer,

1
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat . Tutti i commenti che non affrontano specificamente la domanda e come può essere migliorato verranno eliminati a vista.
Raffaello

2
@BorisTreukhov, per favore, portalo nella chatroom. No, non credo che Andrej stia dicendo che l'analisi dell'evasione è "impossibile" (anche se determinare esattamente cosa significhi in questo contesto non è chiaro per me). Un'analisi della fuga perfettamente precisa è indecidibile. Per tutti: per favore, portalo nella chatroom . Pubblica qui solo i commenti che mirano a migliorare la domanda - altre discussioni e commenti dovrebbero essere pubblicati nella chat.
DW

Risposte:


81

Perché non è possibile stabilire se il programma utilizzerà nuovamente la memoria. Ciò significa che nessun algoritmo può determinare correttamente quando chiamare free()in tutti i casi, il che significa che qualsiasi compilatore che ha tentato di farlo produrrebbe necessariamente alcuni programmi con perdite di memoria e / o alcuni programmi che hanno continuato a utilizzare la memoria che era stata liberata. Anche se hai assicurato che il tuo compilatore non ha mai fatto il secondo e hai permesso al programmatore di inserire chiamate per free()correggere quei bug, sapere quando chiamare free()quel compilatore sarebbe ancora più difficile che sapere quando chiamare free()quando si utilizza un compilatore che non ha provato aiutare.


12
Abbiamo una domanda che riguarda la capacità degli umani di risolvere problemi indecidibili . Non posso darvi un esempio di un programma che sarebbe compilato in modo errato perché dipende da quale algoritmo viene utilizzato dal compilatore. Ma qualsiasi algoritmo produrrà un output errato per infiniti programmi diversi.
David Richerby,

1
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Gilles 'SO- smetti di essere malvagio' il

2
Ragazzi, portatelo in chat . Tutto ciò che non è direttamente correlato alla risposta stessa e al modo in cui può essere migliorato verrà eliminato.
Raffaello

2
Molte cose che i compilatori fanno felicemente sono indecidibili in generale; non saremmo arrivati ​​da nessuna parte nel mondo dei compilatori se avessimo sempre ceduto al teorema di Rice.
Tikhon Jelvis,

3
Questo è irrilevante. Se è indecidibile per tutti i compilatori, è indecidibile anche per tutti gli umani. Tuttavia ci aspettiamo che gli umani si inseriscano free()correttamente.
Paul Draper,

53

Come ha giustamente sottolineato David Richerby, il problema è ineccepibile in generale. La vivacità degli oggetti è una proprietà globale del programma e può in generale dipendere dagli input del programma.

Anche la raccolta dei rifiuti dinamica precisa è un problema indecidibile! Tutti i garbage collector del mondo reale usano la raggiungibilità come approssimazione conservativa sulla necessità o meno di un oggetto assegnato in futuro. È una buona approssimazione, ma è comunque un'approssimazione.

Ma questo è vero solo in generale. Uno dei più famosi cop-out nel settore dell'informatica è "è impossibile in generale, quindi non possiamo fare nulla". Al contrario, ci sono molti casi in cui è possibile fare qualche passo avanti.

Le implementazioni basate sul conteggio dei riferimenti sono molto simili a "il compilatore che inserisce le deallocazioni" in modo tale che è difficile dire la differenza. Il conteggio automatico dei riferimenti di LLVM (usato in Objective-C e Swift ) è un famoso esempio.

Inferenza della regione e raccolta dei rifiuti in fase di compilazione sono le aree di ricerca attualmente attive. Risulta molto più facile in linguaggi dichiarativi come ML e Mercury , dove non è possibile modificare un oggetto dopo che è stato creato.

Ora, sul tema degli umani, ci sono tre modi principali in cui gli umani gestiscono manualmente le allocazioni:

  1. Comprendendo il programma e il problema. Gli esseri umani possono mettere oggetti con vite simili nello stesso oggetto di allocazione, per esempio. Compilatori e netturbini devono dedurlo, ma gli esseri umani hanno informazioni più precise.
  2. Utilizzando selettivamente la contabilità non locale (ad es. Conteggio dei riferimenti) o altre tecniche di allocazione speciali (ad es. Zone) solo quando necessario. Ancora una volta, un essere umano può sapere questo dove un compilatore deve dedurlo.
  3. Male. Tutti sanno dei programmi distribuiti nel mondo reale che hanno perdite lente, dopo tutto. O in caso contrario, a volte i programmi e le API interne devono essere ristrutturati in base alla durata della memoria, riducendo la riusabilità e la modularità.

I commenti non sono per una discussione estesa. Se desideri discutere di dichiarativo vs funzionale, fallo in chat .
Gilles 'SO- smetti di essere cattivo' il

2
Questa è di gran lunga la migliore risposta alla domanda (che troppe risposte non affrontano nemmeno). Avresti potuto aggiungere un riferimento al lavoro pionieristico di Hans Boehm su GC conservativo : en.wikipedia.org/wiki/Boehm_garbage_collector . Un altro punto interessante è che la vivacità dei dati (o utilità in senso esteso) può essere definita rispetto a una semantica astratta o a un modello esecutivo. Ma l'argomento è davvero ampio.
babou,

29

È un problema di incompletezza, non un problema di indecidibilità

Mentre è vero che il posizionamento ottimale delle dichiarazioni di deallocazione è indecidibile, questo non è semplicemente il problema qui. Poiché è indecidibile sia per gli umani che per i compilatori, è impossibile selezionare sempre consapevolmente il posizionamento ottimale della deallocazione, indipendentemente dal fatto che si tratti di un processo manuale o automatico. E poiché nessuno è perfetto, un compilatore sufficientemente avanzato dovrebbe essere in grado di superare le prestazioni umane nell'ipotesi di posizionamenti approssimativamente ottimali. Quindi, l' indecidibilità non è il motivo per cui abbiamo bisogno di dichiarazioni esplicite di deallocazione .

Ci sono casi in cui le conoscenze esterne informano il posizionamento delle dichiarazioni di deallocazione. Rimuovere tali affermazioni equivale quindi a rimuovere parte della logica operativa e chiedere a un compilatore di generare automaticamente quella logica equivale a chiedergli di indovinare cosa stai pensando.

Ad esempio, supponiamo che tu stia scrivendo un Read-Evaluate-Print-Loop (REPL) : l'utente digita un comando e il tuo programma lo esegue. L'utente può allocare / deallocare la memoria digitando i comandi nel proprio REPL. Il codice sorgente specifica cosa dovrebbe fare il REPL per ogni possibile comando utente, inclusa la deallocazione quando l'utente digita il comando per esso.

Ma se il codice sorgente C non fornisce un comando esplicito per la deallocazione, il compilatore dovrebbe dedurre che dovrebbe eseguire l'ubicazione quando l'utente immette il comando appropriato nel REPL. Quel comando è "deallocare", "libero" o qualcos'altro? Il compilatore non ha modo di sapere quale vuoi che sia il comando. Anche se programmate in logica per cercare quella parola di comando e REPL la trova, il compilatore non ha modo di sapere che dovrebbe rispondere ad essa con deallocazione a meno che non lo dica esplicitamente nel codice sorgente.

tl; dr Il problema è che il codice sorgente C non fornisce al compilatore conoscenze esterne. L'indecidibilità non è il problema perché è lì se il processo è manuale o automatizzato.


3
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat . Tutti gli ulteriori commenti che non affrontano specificamente le carenze di questa risposta e come possono essere corretti verranno eliminati a vista.
Raffaello

23

Attualmente, nessuna delle risposte pubblicate è completamente corretta.

Perché i compilatori non inseriscono automaticamente deallocazioni?

  1. Alcuni lo fanno. (Spiegherò più tardi.)

  2. In pratica, puoi chiamare free()subito prima che il programma si concluda. Ma c'è una necessità implicita nella tua domanda di chiamare free()il prima possibile.

  3. Il problema di quando chiamare free()un programma C non appena la memoria è irraggiungibile è indecidibile, vale a dire per qualsiasi algoritmo che fornisce la risposta a tempo finito, c'è un caso che non copre. Questo - e molte altre indecidibilità di programmi arbitrari - possono essere provati dal problema dell'arresto .

  4. Un problema indecidibile non può sempre essere risolto a tempo finito da nessun algoritmo, sia esso un compilatore o un essere umano.

  5. Gli umani (provano a) scrivere in un sottoinsieme di programmi C che possono essere verificati per la correttezza della memoria dal loro algoritmo (loro stessi).

  6. Alcune lingue ottengono il n. 1 costruendo il n. 5 nel compilatore. Non consentono programmi con usi arbitrari di allocazione della memoria, ma piuttosto un sottoinsieme decidibile di essi. Foth e Rust sono due esempi di linguaggi che hanno allocazioni di memoria più restrittive rispetto a quelle di C malloc(), che possono (1) rilevare se un programma è scritto al di fuori del loro insieme decidibile (2) inserire automaticamente deallocazioni.


1
Capisco come fa Rust. Ma non ho mai sentito parlare di un Forth che ha fatto questo. Puoi elaborare?
Milton Silva,

2
@MiltonSilva, Forth - almeno la sua implementazione più elementare e originale - ha solo uno stack, non un heap. Rende l'allocazione / deallocazione spostando il puntatore dello stack di chiamate, un'attività che il compilatore può facilmente eseguire. Forth è stato progettato per indirizzare hardware molto semplice, e talvolta la memoria non dinamica è tutto ciò che è praticabile. Ovviamente non è una soluzione praticabile per programmi non banali.
Paul Draper,

10

"Gli umani lo fanno, quindi non è impossibile" è un errore ben noto. Non capiamo necessariamente (per non parlare del controllo) le cose che creiamo: il denaro è un esempio comune. Tendiamo a sopravvalutare (a volte in modo drammatico) le nostre possibilità di successo in materia tecnologica, specialmente quando i fattori umani sembrano assenti.

Le prestazioni umane nella programmazione del computer sono molto scarse e lo studio dell'informatica (carente in molti programmi di educazione professionale) aiuta a capire perché questo problema non ha una soluzione semplice. Potremmo un giorno, forse non troppo lontano, essere sostituiti dall'intelligenza artificiale sul posto di lavoro. Anche allora, non ci sarà un algoritmo generale che ottiene la deallocazione corretta, automaticamente, sempre.


1
L'errore di accettare la premessa della fallibilità umana e tuttavia di supporre che le macchine pensanti create dall'uomo possano essere ancora infallibili (cioè meglio degli umani) è meno noto ma più intrigante. L'unico presupposto dal quale l'azione può procedere è che la mente umana ha il potenziale per calcolare perfettamente.
Wildcard il

1. Non ho mai detto che le macchine pensanti potrebbero essere infallibili. Meglio degli umani è quello che sono già, in molti casi. 2. L'aspettativa di perfezione (anche potenziale) come prerequisito per l'azione è un'assurdità.
André Souza Lemos,

"Potremmo un giorno, forse non troppo lontano, essere sostituiti dall'intelligenza artificiale sul posto di lavoro." Questo, in particolare, è una sciocchezza. Gli umani sono la fonte di intenti nel sistema. Senza gli umani, non esiste uno scopo per il sistema. L '"intelligenza artificiale" potrebbe essere definita come l' apparenza di una decisione intelligente del presente da parte delle macchine, determinata in realtà da decisioni intelligenti di un programmatore o di un progettista di sistemi in passato. Se non è necessaria alcuna manutenzione (che deve essere eseguita da una persona), l'intelligenza artificiale (o qualsiasi sistema lasciato inaspettato e completamente automatico) fallirà.
Wildcard il

L'intento, nell'uomo come nelle macchine, viene sempre dall'esterno .
André Souza Lemos,

Totalmente falso. (E anche "al di fuori" non definisce una fonte. ) O stai affermando che l'intenzione in quanto tale non esiste realmente, o stai affermando che l'intenzione esiste ma non proviene da nessuna parte. Credi forse che l'intento possa esistere indipendentemente dallo scopo? Nel qual caso si fraintende la parola "intento". Ad ogni modo, una dimostrazione di persona cambierebbe presto idea su questo argomento. Lascerò dopo questo commento poiché le parole da sole non possono portare a una comprensione di "intento", quindi ulteriori discussioni qui sono inutili.
Wildcard il

9

La mancanza di gestione automatica della memoria è una caratteristica della lingua.

C non dovrebbe essere uno strumento per scrivere facilmente il software. È uno strumento per far fare al computer qualunque cosa tu gli dica di fare. Ciò include l'allocazione e la deallocazione della memoria al momento della tua scelta. C è un linguaggio di basso livello che usi quando vuoi controllare il computer con precisione, o quando vuoi fare le cose in un modo diverso da quello che i progettisti di librerie linguistiche / standard si aspettavano.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
DW

2
In che modo questa è una risposta alla (parte CS della) domanda?
Raffaello

6
@Raphael L'informatica non significa che dovremmo cercare risposte tecniche oscure. I compilatori fanno molte cose impossibili nel caso generale. Se vogliamo la gestione automatica della memoria, possiamo implementarla in molti modi. C non lo fa, perché non dovrebbe farlo.
Jouni Sirén,

9

Il problema è principalmente un artefatto storico, non un'impossibilità di implementazione.

Il modo in cui la maggior parte dei compilatori C genera codice è che il compilatore vede solo ogni file sorgente alla volta; non vede mai l'intero programma contemporaneamente. Quando un file di origine chiama una funzione da un altro file di origine o una libreria, tutto il compilatore vede è il file di intestazione con il tipo restituito della funzione, non il codice effettivo della funzione. Ciò significa che quando esiste una funzione che restituisce un puntatore, il compilatore non ha modo di sapere se la memoria a cui punta il puntatore deve essere liberata o meno. Le informazioni per decidere che non vengono mostrate al compilatore in quel momento. Un programmatore umano, dall'altra parte, è libero di cercare il codice sorgente della funzione o la documentazione per scoprire cosa bisogna fare con il puntatore.

Se guardi a linguaggi di basso livello più moderni come C ++ 11 o Rust, scoprirai che hanno risolto il problema principalmente rendendo esplicita la proprietà della memoria nel tipo di puntatore. In C ++ userete un unique_ptr<T>invece di un piano T*per contenere la memoria e unique_ptr<T>assicuratevi che la memoria venga liberata quando l'oggetto raggiunge la fine dell'ambito, a differenza del piano T*. Il programmatore può passare la memoria l'uno unique_ptr<T>dall'altro, ma può essercene solo uno che unique_ptr<T>punta alla memoria. Quindi è sempre chiaro chi possiede la memoria e quando deve essere liberata.

C ++, per motivi di compatibilità con le versioni precedenti, consente ancora la gestione manuale della memoria vecchio stile e quindi la creazione di bug o modi per aggirare la protezione di a unique_ptr<T>. Rust è ancora più rigoroso in quanto applica regole di proprietà della memoria tramite errori del compilatore.

Per quanto riguarda l'indecidibilità, il problema di arresto e simili, sì, se si attesta la semantica C non è possibile decidere per tutti i programmi quando liberare la memoria. Tuttavia, per la maggior parte dei programmi attuali, non per esercizi accademici o software difettosi, sarebbe assolutamente possibile decidere quando liberare e quando non farlo. Dopo tutto, questo è l'unico motivo per cui l'essere umano può capire quando liberarsi o meno in primo luogo.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Raffaello

6

Altre risposte si sono concentrate sulla possibilità di eseguire la raccolta dei rifiuti, alcuni dettagli su come è stato fatto e alcuni dei problemi.

Un problema che non è stato ancora affrontato è l'inevitabile ritardo nella raccolta dei rifiuti. In C, quando un programmatore chiama free (), quella memoria è immediatamente disponibile per il riutilizzo. (Almeno in teoria!) Quindi un programmatore può liberare la propria struttura da 100 MB, allocare un'altra struttura da 100 MB un millisecondo dopo, e aspettarsi che l'utilizzo complessivo della memoria rimanga lo stesso.

Questo non è vero con Garbage Collection. I sistemi di raccolta dati inutili hanno qualche ritardo nel restituire la memoria non utilizzata all'heap e questo può essere significativo. Se la struttura da 100 MB non rientra nell'ambito di applicazione e dopo un millisecondo il programma imposta un'altra struttura da 100 MB, è ragionevole aspettarsi che il sistema utilizzi 200 MB per un breve periodo. Quel "breve periodo" può essere millisecondi o secondi a seconda del sistema, ma c'è ancora un ritardo.

Se stai usando un PC con concerti di RAM e memoria virtuale, ovviamente non lo noterai mai. Se stai utilizzando un sistema con risorse più limitate (ad esempio, un sistema incorporato o un telefono), questo è qualcosa che devi prendere sul serio. Questo non è solo teorico - l'ho visto personalmente creare problemi (come nel crash del tipo di problemi del dispositivo) quando si lavora su un sistema WinCE usando .NET Compact Framework e si sviluppa in C #.


In teoria potresti eseguire un GC prima di ogni allocazione.
adrianN,

4
@adrianN Ma in pratica questo non è fatto perché sarebbe mentale. Il punto di Graham è ancora valido: i GC comportano sempre un notevole sovraccarico, sia in termini di runtime che in termini di memoria aggiuntiva richiesta. È possibile modificare questo equilibrio verso uno dei due estremi, ma fondamentalmente non è possibile rimuovere il sovraccarico.
Konrad Rudolph,

Il "ritardo" nel momento in cui la memoria viene liberata è più un problema in un sistema di memoria virtuale che in un sistema con risorse limitate. Nel primo caso, potrebbe essere meglio per un programma utilizzare 100 MB rispetto a 200 MB anche se il sistema ha 200 MB disponibili , ma nel secondo caso non ci sarà alcun vantaggio nell'esecuzione del GC prima del necessario a meno che i ritardi non siano più accettabili durante alcuni parti del codice rispetto ad altre.
supercat

Non riesco a vedere come questo tenta di rispondere alla domanda (parte CS della).
Raffaello

1
@Raphael Ho spiegato un problema ben noto con il principio della raccolta dei rifiuti, che è (o dovrebbe essere) insegnato in CS come uno dei suoi svantaggi di base. Ho anche dato la mia esperienza personale di averlo visto in pratica, per dimostrare che non è un problema puramente teorico. Se non hai capito qualcosa a riguardo, sono felice di parlarti per migliorare la tua conoscenza della materia.
Graham,

4

La domanda presume che una deallocazione sia qualcosa che il programmatore dovrebbe dedurre da altre parti del codice sorgente. Non è. "A questo punto del programma, il riferimento di memoria FOO non è più utile" sono le informazioni conosciute solo nella mente del programmatore fino a quando non sono codificate (in linguaggi procedurali) una dichiarazione di deallocazione.

Non è teoricamente diverso da qualsiasi altra riga di codice. Perché i compilatori non inseriscono automaticamente "A questo punto nel programma, controllare il registro BAR per input" o "Se la chiamata di funzione restituisce un valore diverso da zero, uscire dalla subroutine corrente" ? Dal punto di vista del compilatore il motivo è "incompletezza", come mostrato in questa risposta . Ma qualsiasi programma soffre di incompletezza quando il programmatore non gli ha detto tutto quello che sa.

Nella vita reale, le deallocazioni sono lavori grugniti o scaldabagni; i nostri cervelli li riempiono automaticamente e si lamentano, e il sentimento "il compilatore potrebbe farlo altrettanto bene o meglio" è vero. In teoria, tuttavia, non è così, anche se fortunatamente altre lingue ci offrono una maggiore scelta della teoria.


4
"'A questo punto del programma, il riferimento di memoria FOO non è più utile' sono le informazioni conosciute solo nella mente del programmatore" - è chiaramente sbagliato. a) Per molti FOO, è banale capirlo, ad esempio variabili locali con semantica di valore. b) suggerisci che il programmatore lo sappia sempre, il che è chiaramente un presupposto eccessivamente ottimista; se fosse vero, non avremmo gravi bug a causa della cattiva gestione della memoria è un software critico per la sicurezza. Che, ahimè, lo facciamo.
Raffaello

Sto solo suggerendo che il linguaggio è stato progettato per i casi in cui il programmatore non sa FOO non è più utile. Sono d'accordo, chiaramente, questo di solito non è vero, ed è per questo che abbiamo bisogno di analisi statiche e / o raccolta dei rifiuti. Che, evviva, lo facciamo. Ma la domanda del PO è: quando queste cose non sono così preziose come i dealloc codificati a mano?
Travis Wilson,

4

Cosa viene fatto: esiste la garbage collection e ci sono compilatori che utilizzano il conteggio dei riferimenti (Objective-C, Swift). Quelli che fanno il conteggio dei riferimenti hanno bisogno dell'aiuto del programmatore evitando cicli di riferimento forti.

La vera risposta al "perché" è che gli autori di compilatori non hanno trovato un modo abbastanza buono e abbastanza veloce da renderlo utilizzabile in un compilatore. Dato che gli autori di compilatori sono in genere piuttosto intelligenti, potresti concludere che è molto, molto difficile trovare un modo che sia abbastanza buono e abbastanza veloce.

Uno dei motivi per cui è molto, molto difficile è ovviamente che è indecidibile. In informatica, quando parliamo di "decidibilità" intendiamo "prendere la decisione giusta". I programmatori umani possono ovviamente decidere facilmente dove collocare la memoria, perché non si limitano alle decisioni corrette . E spesso prendono decisioni sbagliate.


Non riesco a vedere un contributo qui.
babou,

3

In lingue come C, il programmatore dovrebbe inserire chiamate gratuitamente. Perché il compilatore non lo fa automaticamente?

Perché la durata di un blocco di memoria è una decisione del programmatore, non del compilatore.

Questo è tutto. Questo è il progetto di C. Il compilatore non può sapere quale fosse l'intenzione di allocare un blocco di memoria. Gli umani possono farlo, perché conoscono lo scopo di ogni blocco di memoria e quando questo scopo viene servito, così può essere liberato. Fa parte del progetto del programma in fase di scrittura.

C è un linguaggio di basso livello, quindi i casi di passaggio di un blocco della memoria a un altro processo o persino a un altro processore sono abbastanza frequenti. In casi estremi, un programmatore può allocare intenzionalmente un pezzo di memoria e non riutilizzarlo mai più solo per esercitare pressione sulla memoria su altre parti del sistema. Il compilatore non ha modo di sapere se il blocco è ancora necessario.


-1

In lingue come C, il programmatore dovrebbe inserire chiamate gratuitamente. Perché il compilatore non lo fa automaticamente?

In C e in molte altre lingue, c'è davvero una possibilità per fare in modo che il compilatore faccia l'equivalente di questo nei casi in cui è chiaro al momento della compilazione quando dovrebbe essere fatto: l'uso di variabili di durata automatica (cioè normali variabili locali) . Il compilatore è responsabile di disporre di spazio sufficiente per tali variabili e di rilasciare tale spazio al termine della loro (ben definita) durata.

Con le matrici a lunghezza variabile che sono una caratteristica C dal C99, gli oggetti a durata automatica servono, in linea di principio, sostanzialmente tutte le funzioni in C che fanno gli oggetti allocati dinamicamente di durata calcolabile. In pratica, ovviamente, le implementazioni in C possono porre limiti pratici significativi sull'uso dei VLA - cioè le loro dimensioni possono essere limitate a causa dell'allocazione in pila - ma questa è una considerazione di implementazione, non una considerazione di progettazione del linguaggio.

Quegli oggetti il ​​cui uso previsto impedisce di dare loro una durata automatica sono precisamente quelli la cui durata non può essere determinata al momento della compilazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.