Lingue compilate vs. interpretate


284

Sto cercando di capire meglio la differenza. Ho trovato molte spiegazioni online, ma tendono alle differenze astratte piuttosto che alle implicazioni pratiche.

La maggior parte delle mie esperienze di programmazione è stata con CPython (dinamico, interpretato) e Java (statico, compilato). Tuttavia, capisco che esistono altri tipi di lingue interpretate e compilate. A parte il fatto che i file eseguibili possono essere distribuiti da programmi scritti in linguaggi compilati, ci sono vantaggi / svantaggi per ciascun tipo? Spesso sento persone che sostengono che le lingue interpretate possono essere utilizzate in modo interattivo, ma credo che anche le lingue compilate possano avere implementazioni interattive, giusto?


32
Hai scelto esattamente le lingue peggiori per questo confronto. Entrambi sono compilati. L'unica vera differenza tra loro è il JITer, e anche Python ne ha una parziale (psyco).
Ignacio Vazquez-Abrams,

1
Un buon esempio di un linguaggio compilato interattivo è Clojure: tutto è completamente compilato (prima nella JVM e poi nel codice nativo tramite la JIT). Tuttavia, gran parte della ricompilazione avviene in modo dinamico e lo sviluppo viene spesso eseguito in una shell REPL interattiva in cui è possibile valutare qualsiasi funzione desiderata nell'ambiente in esecuzione.
Mikera,

Standard ML è un altro linguaggio compilato interattivo; il compilatore incorporato emette anche codice macchina nativo reale.
Donal Fellows,


Risposte:


460

Una lingua compilata è quella in cui il programma, una volta compilato, è espresso nelle istruzioni della macchina target. Ad esempio, un'operazione "+" aggiuntiva nel codice sorgente potrebbe essere tradotta direttamente nell'istruzione "ADD" nel codice macchina.

Un linguaggio interpretato è quello in cui le istruzioni non vengono eseguite direttamente dalla macchina target, ma vengono invece lette ed eseguite da un altro programma (che normalmente è scritto nella lingua della macchina nativa). Ad esempio, la stessa operazione "+" verrebbe riconosciuta dall'interprete in fase di esecuzione, che chiamerebbe quindi la propria funzione "add (a, b)" con gli argomenti appropriati, che eseguirà quindi l'istruzione "ADD" del codice macchina .

Puoi fare tutto ciò che puoi fare in una lingua interpretata in una lingua compilata e viceversa: entrambi sono Turing completi. Entrambi presentano tuttavia vantaggi e svantaggi per l'implementazione e l'uso.

Ho intenzione di generalizzare completamente (i puristi mi perdonano!) Ma, approssimativamente, ecco i vantaggi dei linguaggi compilati:

  • Prestazioni più veloci utilizzando direttamente il codice nativo della macchina target
  • Opportunità di applicare ottimizzazioni abbastanza potenti durante la fase di compilazione

E qui ci sono i vantaggi delle lingue interpretate:

  • Più facile da implementare (scrivere buoni compilatori è molto difficile !!)
  • Non è necessario eseguire una fase di compilazione: può eseguire il codice direttamente "al volo"
  • Può essere più conveniente per le lingue dinamiche

Si noti che le tecniche moderne come la compilazione bytecode aggiungono ulteriore complessità: ciò che accade qui è che il compilatore ha come target una "macchina virtuale" che non è la stessa dell'hardware sottostante. Queste istruzioni della macchina virtuale possono quindi essere compilate nuovamente in una fase successiva per ottenere il codice nativo (ad es. Come fatto dal compilatore JIT JVM Java).


1
Non tutte le lingue compilate richiedono una fase di compilazione lenta. Le implementazioni di Common Lisp serie sono compilatori e spesso non si preoccupano di un interprete, preferendo compilare molto velocemente al volo. D'altra parte, Java ha bisogno di una fase di compilazione, e di solito è visibile.
David Thornley,

2
@Kareem: il compilatore JIT esegue solo 1) e 2) una volta , dopodiché è il codice nativo fino in fondo. L'interprete deve fare sia 1) che 2) ogni volta che viene chiamato il codice (che può essere molte, molte volte ...). Quindi, nel tempo, il compilatore JIT vince con un lungo margine.
Mikera,

3
Sì, bytecode viene tradotto in codice macchina ad un certo punto durante l'esecuzione complessiva del programma (al contrario di prima dell'esecuzione del programma, come nel caso di un compilatore tradizionale). Ma un dato codice potrebbe essere eseguito 10 milioni + volte durante l'esecuzione complessiva del programma. (Probabilmente) viene compilato una sola volta dal bytecode al codice macchina. Quindi l'overhead di runtime di JIT è piccolo e può essere ignorato per programmi a esecuzione prolungata. Dopo che il compilatore JIT ha terminato di fare il suo lavoro, eseguirai effettivamente il codice macchina puro fino in fondo.
Mikera,

2
Questa è in realtà una falsa dicotomia. Non c'è nulla di intrinseco in un linguaggio che lo compili interpretato. Non è altro che un malinteso ampiamente diffuso. Molte lingue hanno entrambe le implementazioni e tutte le lingue possono avere entrambe.
mmachenry,

2
@mmachenry non è una falsa dicotomia. "linguaggio di programmazione" comprende sia la progettazione che l'implementazione. Mentre in senso teorico una data definizione di linguaggio può essere sia compilata che interpretata, nella pratica del mondo reale ci sono notevoli differenze nell'attuazione. Nessuno ha ancora risolto come compilare in modo efficace determinati costrutti del linguaggio, ad esempio: è un problema di ricerca aperto.
Mikera,

99

Una lingua in sé non è né compilata né interpretata, lo è solo un'implementazione specifica di una lingua. Java è un esempio perfetto. Esiste una piattaforma basata su bytecode (JVM), un compilatore nativo (gcj) e un interprete per un superset di Java (bsh). Allora, che cos'è Java adesso? Bytecode compilato, compilato nativamente o interpretato?

Altre lingue, che vengono compilate e interpretate, sono Scala, Haskell o Ocaml. Ognuna di queste lingue ha un interprete interattivo, nonché un compilatore in codice byte o codice macchina nativo.

Quindi in genere classificare le lingue per "compilato" e "interpretato" non ha molto senso.


3
Sono d'accordo. Oppure diciamo: ci sono compilatori nativi (che creano codice macchina per la CPU da mangiare) e compilatori non così nativi (creazione di elementi tokenizzati, cioè codice intermedio, che alcuni compilatori just-in-time compilano in codice macchina prima ( o durante) runtime ONCE), e ci sono "veri" non compilatori che non producono mai codice macchina e non consentono mai alla CPU di eseguire il codice. Questi ultimi sono interpreti. Oggi, i compilatori nativi che producono direttamente codice macchina (CPU) in fase di compilazione stanno diventando sempre più rari. Delphi / Codegear è uno dei migliori sopravvissuti.
TheBlastOne,

57

Inizia a pensare in termini di a: esplosione del passato

Tanto tempo fa, molto tempo fa, viveva nella terra degli interpreti informatici e dei compilatori. Tutti i tipi di confusione derivavano dal merito dell'uno rispetto all'altro. L'opinione generale a quel tempo era qualcosa del tipo di:

  • Interprete: veloce da sviluppare (modifica ed esecuzione). Esecuzione lenta perché ogni istruzione doveva essere interpretata nel codice macchina ogni volta che veniva eseguita (pensa a cosa significasse per un ciclo eseguito migliaia di volte).
  • Compilatore: lento da sviluppare (modifica, compilazione, collegamento ed esecuzione. I passaggi di compilazione / collegamento potrebbero richiedere molto tempo). Veloce da eseguire. L'intero programma era già in codice macchina nativo.

Esisteva una differenza di uno o due ordini di grandezza tra le prestazioni di runtime tra un programma interpretato e un programma compilato. Altri punti distintivi, ad esempio la mutabilità in fase di esecuzione del codice, erano anche di qualche interesse, ma la principale distinzione ruotava attorno ai problemi di prestazioni in fase di esecuzione.

Oggi il paesaggio si è evoluto a tal punto che la distinzione compilata / interpretata è praticamente irrilevante. Molte lingue compilate fanno ricorso a servizi di runtime che non sono completamente basati su codice macchina. Inoltre, la maggior parte dei linguaggi interpretati viene "compilata" in codice byte prima dell'esecuzione. Gli interpreti a codice byte possono essere molto efficienti e competere con alcuni codici generati dal compilatore dal punto di vista della velocità di esecuzione.

La differenza classica è che i compilatori hanno generato il codice macchina nativo, gli interpreti leggono il codice sorgente e il codice macchina generato al volo usando una sorta di sistema di runtime. Oggi sono rimasti pochissimi interpreti classici, quasi tutti compilati in byte-code (o in qualche altro stato semi-compilato) che viene quindi eseguito su una "macchina" virtuale.


1
Bello - ottimo riassunto nell'ultimo paragrafo - grazie!
ckib16,

26

I casi estremi e semplici:

  • Un compilatore produrrà un eseguibile binario nel formato eseguibile nativo della macchina target. Questo file binario contiene tutte le risorse necessarie ad eccezione delle librerie di sistema; è pronto per essere eseguito senza ulteriore preparazione ed elaborazione e funziona come un fulmine perché il codice è il codice nativo per la CPU sul computer di destinazione.

  • Un interprete presenterà all'utente un prompt in un ciclo in cui può inserire istruzioni o codice e, dopo aver colpito RUNo l'equivalente, l'interprete esaminerà, scansionerà, analizzerà ed eseguirà interpretativamente ogni riga fino a quando il programma viene eseguito su un punto di arresto o un errore . Poiché ogni riga è trattata da sola e l'interprete non "impara" nulla dall'aver vista prima la riga, lo sforzo di convertire il linguaggio leggibile dall'uomo in istruzioni automatiche è sostenuto ogni volta per ogni riga, quindi è cane lento. Il lato positivo è che l'utente può ispezionare e interagire in altro modo con il suo programma in tutti i modi: modifica delle variabili, modifica del codice, esecuzione in modalità trace o debug ... qualunque cosa.

Con quelli fuori mano, lascia che ti spieghi che la vita non è più così semplice. Per esempio,

  • Molti interpreti pre-compileranno il codice che gli viene dato, quindi il passaggio di traduzione non deve essere ripetuto ancora e ancora.
  • Alcuni compilatori compilano non istruzioni di macchina specifiche della CPU ma di bytecode, una sorta di codice macchina artificiale per una macchina fittizia. Questo rende il programma compilato un po 'più portatile, ma richiede un interprete bytecode su ogni sistema di destinazione.
  • Gli interpreti bytecode (sto guardando Java qui) recentemente tendono a ricompilare il bytecode che ottengono per la CPU della sezione target appena prima dell'esecuzione (chiamata JIT). Per risparmiare tempo, questo viene spesso fatto solo per il codice che viene eseguito spesso (hotspot).
  • Alcuni sistemi che sembrano e si comportano come interpreti (Clojure, per esempio) compilano immediatamente il codice che ottengono, ma consentono l'accesso interattivo all'ambiente del programma. Questa è fondamentalmente la comodità degli interpreti con la velocità della compilazione binaria.
  • Alcuni compilatori non si compilano davvero, semplicemente pre-digeriscono e comprimono il codice. Ho sentito un po 'di tempo fa come funziona Perl. Quindi a volte il compilatore sta solo facendo un po 'di lavoro e la maggior parte è ancora interpretazione.

Alla fine, in questi giorni, l'interpretazione rispetto alla compilazione è un compromesso, con il tempo speso (una volta) la compilazione spesso ricompensato da migliori prestazioni di runtime, ma un ambiente interpretativo che offre maggiori opportunità di interazione. Compilare vs. interpretare è principalmente una questione di come il lavoro di "comprensione" del programma sia suddiviso tra diversi processi, e la linea è un po 'sfocata in questi giorni come lingue e prodotti cercano di offrire il meglio di entrambi i mondi.


23

Da http://www.quora.com/What-is-the-difference-b Between- compiled- and- interpreted-programming-languages

Non c'è differenza, perché "linguaggio di programmazione compilato" e "linguaggio di programmazione interpretato" non sono concetti significativi. Qualsiasi linguaggio di programmazione, e intendo davvero qualsiasi, può essere interpretato o compilato. Pertanto, l'interpretazione e la compilazione sono tecniche di implementazione, non attributi delle lingue.

L'interpretazione è una tecnica in base alla quale un altro programma, l'interprete, esegue operazioni per conto del programma che viene interpretato al fine di eseguirlo. Se riesci a immaginare di leggere un programma e di fare ciò che dice di fare passo dopo passo, ad esempio su un foglio di carta, è proprio quello che fa anche un interprete. Un motivo comune per interpretare un programma è che gli interpreti sono relativamente facili da scrivere. Un altro motivo è che un interprete può monitorare ciò che un programma tenta di fare durante l'esecuzione, per imporre una politica, diciamo, per sicurezza.

La compilazione è una tecnica in base alla quale un programma scritto in una lingua (la "lingua di origine") viene tradotto in un programma in un'altra lingua (la "lingua dell'oggetto"), che si spera significhi la stessa cosa del programma originale. Durante la traduzione, è comune che il compilatore cerchi anche di trasformare il programma in modi che renderanno più veloce il programma oggetto (senza cambiarne il significato!). Un motivo comune per compilare un programma è che esiste un buon modo per eseguire programmi nella lingua degli oggetti in modo rapido e senza il sovraccarico di interpretare la lingua di origine lungo il percorso.

Potresti aver indovinato, in base alle definizioni di cui sopra, che queste due tecniche di implementazione non si escludono a vicenda e potrebbero persino essere complementari. Tradizionalmente, il linguaggio degli oggetti di un compilatore era il codice macchina o qualcosa di simile, che si riferisce a qualsiasi numero di linguaggi di programmazione compresi da particolari CPU del computer. Il codice macchina verrebbe quindi eseguito "sul metallo" (anche se si potrebbe vedere, se si guarda abbastanza da vicino, che il "metallo" funziona in modo molto simile a un interprete). Oggi, tuttavia, è molto comune utilizzare un compilatore per generare il codice oggetto che deve essere interpretato, ad esempio è il modo in cui Java (e talvolta funziona ancora). Esistono compilatori che traducono altre lingue in JavaScript, che viene spesso eseguito in un browser Web, che potrebbe interpretare JavaScript, o compilarlo una macchina virtuale o un codice nativo. Abbiamo anche interpreti per il codice macchina, che può essere utilizzato per emulare un tipo di hardware su un altro. Oppure, si potrebbe usare un compilatore per generare il codice oggetto che è quindi il codice sorgente per un altro compilatore, che potrebbe persino compilare il codice in memoria appena in tempo per l'esecuzione, che a sua volta. . . hai avuto l'idea. Esistono molti modi per combinare questi concetti.


Puoi risolvere questa frase: "Esistono compilatori che traducono altre lingue in JavaScript, che viene spesso eseguito in un browser Web, che potrebbe interpretare JavaScript o compilarlo in una macchina virtuale o in un codice nativo".
Koray Tugay,

Azzeccato. Un altro errore comune è attribuire l'utilità di una lingua alle sue API esistenti.
Little Endian,

10

Il più grande vantaggio del codice sorgente interpretato rispetto al codice sorgente compilato è PORTABILITY .

Se il codice sorgente è compilato, è necessario compilare un eseguibile diverso per ogni tipo di processore e / o piattaforma su cui si desidera eseguire il programma (ad es. Uno per Windows x86, uno per Windows x64, uno per Linux x64 e così via sopra). Inoltre, a meno che il tuo codice non sia completamente conforme agli standard e non utilizzi funzioni / librerie specifiche della piattaforma, dovrai effettivamente scrivere e mantenere più basi di codice!

Se il tuo codice sorgente viene interpretato, devi solo scriverlo una volta e può essere interpretato ed eseguito da un interprete appropriato su qualsiasi piattaforma! È portatile ! Si noti che un interprete stesso è un programma eseguibile che è scritto e compilato per una piattaforma specifica.

Un vantaggio del codice compilato è che nasconde il codice sorgente all'utente finale (che potrebbe essere proprietà intellettuale ) perché invece di distribuire il codice sorgente leggibile dall'uomo originale, si distribuisce un file eseguibile binario oscuro.


1
su questi termini java non può essere considerato un "linguaggio compilato", ma la sua fase di compilazione offre i vantaggi della compilazione (verifica del tipo, rilevamento precoce degli errori, ecc.) e produce un bytecode che può essere eseguito su ogni sistema operativo, con un Java macchina virtuale fornita.
Rogelio Triviño,

7

Un compilatore e un interprete fanno lo stesso lavoro: tradurre un linguaggio di programmazione in un altro linguaggio pgoramming, solitamente più vicino all'hardware, spesso indirizzando il codice macchina eseguibile.

Tradizionalmente, "compilato" significa che questa traduzione avviene tutto in una volta, viene eseguita da uno sviluppatore e l'eseguibile risultante viene distribuito agli utenti. Esempio puro: C ++. La compilazione richiede in genere un tempo piuttosto lungo e tenta di eseguire molte costose operazioni di optmizzazione in modo che l'eseguibile risultante funzioni più velocemente. Gli utenti finali non hanno gli strumenti e le conoscenze per compilare le cose da soli, e l'eseguibile spesso deve essere eseguito su una varietà di hardware, quindi non è possibile eseguire molte ottimizzazioni specifiche dell'hardware. Durante lo sviluppo, la fase di compilazione separata implica un ciclo di feedback più lungo.

Tradizionalmente, "interpretato" significa che la traduzione avviene "al volo", quando l'utente vuole eseguire il programma. Esempio puro: vaniglia PHP. Un ingenuo interprete deve analizzare e tradurre ogni pezzo di codice ogni volta che viene eseguito, il che lo rende molto lento. Non può fare ottimizzazioni complesse e costose perché richiederebbero più tempo del tempo risparmiato nell'esecuzione. Ma può sfruttare appieno le capacità dell'hardware su cui gira. La mancanza di una fase di compilazione separata riduce i tempi di feedback durante lo sviluppo.

Ma al giorno d'oggi "compilato vs. interpretato" non è un problema in bianco o nero, ci sono sfumature nel mezzo. Gli interpreti semplici e ingenui sono praticamente estinti. Molte lingue utilizzano un processo in due fasi in cui il codice di alto livello viene tradotto in un bytecode indipendente dalla piattaforma (che è molto più veloce da interpretare). Quindi hai "compilatori just in time" che compilano il codice al massimo una volta per ogni esecuzione del programma, a volte memorizzano nella cache i risultati e persino decidono in modo intelligente di interpretare il codice che viene eseguito raramente e fanno potenti ottimizzazioni per il codice che gira molto. Durante lo sviluppo, i debugger sono in grado di cambiare codice all'interno di un programma in esecuzione anche per linguaggi tradizionalmente compilati.


1
Tuttavia, il modello di compilazione di C ++ è ereditato da C ed è stato progettato senza considerare funzionalità come i template. Questa imbarazzo contribuisce alla lunga compilazione di C ++ molto più di qualsiasi altro fattore - e lo rende un cattivo esempio.

4

Innanzitutto, un chiarimento, Java non è completamente compilato staticamente e collegato nel modo C ++. Viene compilato in bytecode, che viene quindi interpretato da una JVM. JVM può eseguire compilazioni just-in-time nel linguaggio macchina nativo, ma non è necessario.

Più precisamente: penso che l'interattività sia la principale differenza pratica. Poiché tutto viene interpretato, puoi prendere un piccolo estratto di codice, analizzarlo ed eseguirlo sullo stato corrente dell'ambiente. Pertanto, se avessi già eseguito codice che inizializzava una variabile, avresti accesso a quella variabile, ecc. Si presta davvero modo a cose come lo stile funzionale.

L'interpretazione, tuttavia, costa molto, specialmente quando si dispone di un sistema di grandi dimensioni con molti riferimenti e contesto. Per definizione, è dispendioso perché potrebbe essere necessario interpretare e ottimizzare due volte lo stesso codice (anche se la maggior parte dei runtime ha una certa memorizzazione nella cache e ottimizzazioni). Tuttavia, si paga un costo di runtime e spesso è necessario un ambiente di runtime. È anche meno probabile vedere complesse ottimizzazioni interprocedurali perché attualmente le loro prestazioni non sono sufficientemente interattive.

Pertanto, per i sistemi di grandi dimensioni che non cambieranno molto, e per alcune lingue, ha più senso precompilare e prelink tutto, fare tutte le ottimizzazioni che puoi fare. Ciò si traduce in un runtime molto snello che è già ottimizzato per la macchina target.

Per quanto riguarda la generazione di eseguibili, questo ha poco a che fare con questo, IMHO. È spesso possibile creare un eseguibile da una lingua compilata. Ma puoi anche creare un eseguibile da un linguaggio interpretato, tranne per il fatto che l'interprete e il runtime sono già impacchettati nell'esecuibile e nascosti da te. Ciò significa che generalmente pagherai comunque i costi di runtime (anche se sono sicuro che per alcune lingue ci sono modi per tradurre tutto in un eseguibile ad albero).

Non sono d'accordo sul fatto che tutte le lingue possano essere rese interattive. Alcuni linguaggi, come C, sono così legati alla macchina e all'intera struttura dei collegamenti che non sono sicuro che tu possa costruire una versione interattiva a pieno titolo significativa


C non è realmente legato a una "macchina". La sintassi e la semantica di C sono piuttosto semplici. Non dovrebbe essere particolarmente difficile implementare un interprete C, che richiede solo molto tempo (perché anche la libreria standard deve essere implementata). E a proposito, Java può essere compilato in codice macchina nativo (usando gcj).
lunaryorn,

@lunaryorn: non sono d'accordo su GCJ. GCJ offre semplicemente un ambiente basato su eseguibili. "Le applicazioni compilate sono collegate al runtime GCJ, libgcj, che fornisce le librerie delle classi principali, un garbage collector e un interprete bytecode"
Uri

2
GCJ fa produrre codice macchina nativo, e non solo un ambiente eseguibile con interprete embedded e bytecode. libgcj fornisce un interprete bytecode per supportare le chiamate dal codice nativo al bytecode Java, non per interpretare il programma compilato. Se libgcj non fornisse un interprete bytecode, GCJ non sarebbe conforme alle specifiche Java.
lunaryorn,

@lunaryorn: Ah. Ok, apprezzo il chiarimento e sono corretto. Utilizziamo principalmente Java in un ambiente Windows, quindi non provo gcj da anni.
Uri,


2

È piuttosto difficile dare una risposta pratica perché la differenza sta nella definizione della lingua stessa. È possibile creare un interprete per ogni linguaggio compilato, ma non è possibile creare un compilatore per ogni linguaggio interpretato. Riguarda molto la definizione formale di una lingua. Quindi quella roba di informatica teorica piace a nessuno all'università.


1
Sicuramente puoi costruire un compilatore per un linguaggio interpretato, ma il codice macchina compilato è esso stesso un mirror del runtime.
Aiden Bell,

2

The Python Book © 2015 Imagine Publishing Ltd, distingue semplicemente la differenza con il seguente suggerimento citato nella pagina 10 come:

Un linguaggio interpretato come Python è un linguaggio in cui il codice sorgente viene convertito in codice macchina e quindi eseguito ogni volta che il programma viene eseguito. Questo è diverso da un linguaggio compilato come C, in cui il codice sorgente viene convertito in codice macchina una sola volta: il codice macchina risultante viene quindi eseguito ogni volta che il programma viene eseguito.


1

Compilare è il processo di creazione di un programma eseguibile dal codice scritto in un linguaggio di programmazione compilato. La compilazione consente al computer di eseguire e comprendere il programma senza la necessità del software di programmazione utilizzato per crearlo. Quando viene compilato un programma, viene spesso compilato per una piattaforma specifica (ad esempio piattaforma IBM) che funziona con computer compatibili IBM, ma non con altre piattaforme (ad esempio piattaforma Apple). Il primo compilatore è stato sviluppato da Grace Hopper mentre lavorava sul computer Harvard Mark I. Oggi, la maggior parte delle lingue di alto livello includerà il proprio compilatore o disporrà di toolkit che possono essere utilizzati per compilare il programma. Un buon esempio di un compilatore usato con Java è Eclipse e un esempio di un compilatore usato con C e C ++ è il comando gcc.


0

Breve definizione (non precisa):

Lingua compilata: l' intero programma viene tradotto in una sola volta in codice macchina, quindi il codice macchina viene eseguito dalla CPU.

Linguaggio interpretato: il programma viene letto riga per riga e non appena viene letta una riga, le istruzioni della macchina per quella riga vengono eseguite dalla CPU.

Ma davvero, poche lingue in questi giorni sono puramente compilate o puramente interpretate, spesso è un mix. Per una descrizione più dettagliata con le immagini, vedere questa discussione:

Qual è la differenza tra compilazione e interpretazione?

O il mio post successivo sul blog:

https://orangejuiceliberationfront.com/the-difference-between-compiler-and-interpreter/

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.