Perché così tanti sviluppatori credono che prestazioni, leggibilità e manutenibilità non possano coesistere?


34

Mentre rispondevo a questa domanda , ho iniziato a chiedermi perché così tanti sviluppatori credessero che un buon design non dovesse tenere conto delle prestazioni perché ciò influenzerebbe la leggibilità e / o la manutenibilità.

Ritengo che un buon design tenga conto anche delle prestazioni al momento della stesura e che un buon sviluppatore con un buon design possa scrivere un programma efficiente senza influire negativamente sulla leggibilità o sulla manutenibilità.

Mentre riconosco che ci sono casi estremi, perché molti sviluppatori insistono sul fatto che un programma / progetto efficiente comporti una scarsa leggibilità e / o una scarsa manutenibilità e, di conseguenza, che le prestazioni non debbano essere considerate una progettazione?


9
Sarebbe quasi impossibile ragionare su larga scala, ma per piccoli pezzi di codice è abbastanza ovvio. Basta confrontare le versioni leggibili ed efficienti di, diciamo, quicksort.
SK-logic,

7
Mu. Dovresti iniziare sostenendo la tua affermazione secondo cui molti sviluppatori insistono sul fatto che l'efficienza porta a non mantenibilità.
Peter Taylor,

2
SK-logic: Secondo me questa è una delle parti migliori di tutti i siti di stackexchange, dal momento che si mette in discussione l'ovvio, che può essere salutare di tanto in tanto. Ciò che potrebbe essere ovvio per te potrebbe non essere ovvio per qualcun altro, e viceversa. :) La condivisione è la cura.
Andreas Johansson,

2
@Justin, no. Quel thread mi sembra presupporre una situazione in cui esiste una scelta forzata tra codice efficiente o codice gestibile. L'interrogante non dice con quale frequenza si trova in quella situazione e i rispondenti non sembrano dichiarare di trovarsi frequentemente in quella situazione.
Peter Taylor,

2
-1 per la domanda. Quando l'ho letto ho pensato che fosse un uomo di paglia per sfrattare l'unica vera risposta: "Perché non usano il pitone".
Ingo,

Risposte:


38

Penso che tali punti di vista siano di solito reazioni a tentativi di ottimizzazione (micro-) prematura , che è ancora prevalente, e di solito fa molto più male che bene. Quando uno cerca di contrastare tali visioni, è facile cadere - o almeno assomigliare - all'altro estremo.

È tuttavia vero che con l'enorme sviluppo di risorse hardware negli ultimi decenni, per la maggior parte dei programmi scritti oggi, le prestazioni hanno cessato di essere un fattore limitante. Ovviamente, si dovrebbe tener conto delle prestazioni attese e ottenibili durante la fase di progettazione, al fine di identificare i casi in cui le prestazioni possono essere (o venire) un grosso problema . E quindi è davvero importante progettare per le prestazioni sin dall'inizio. Tuttavia, la semplicità, la leggibilità e la manutenibilità complessive sono ancora più importanti . Come altri hanno notato, il codice ottimizzato per le prestazioni è più complesso, più difficile da leggere e mantenere e più soggetto a bug rispetto alla soluzione di lavoro più semplice. Pertanto, qualsiasi sforzo speso per l'ottimizzazione deve essere dimostrato - non solo creduto- apportare benefici reali, riducendo al minimo la manutenibilità a lungo termine del programma. Quindi un buon design isola le parti complesse e ad alte prestazioni dal resto del codice , che è mantenuto il più semplice e pulito possibile.


8
"Quando si cerca di contrastare tali punti di vista, è facile cadere nell'altro estremo - o almeno assomigliare a - l'altro" Ho problemi con le persone che pensano di avere la tesi opposta quando sto semplicemente bilanciando i professionisti con i contro Non solo in programmazione, in tutto.
jhocking

1
Sono così stanco di tutti discutere di questo che mi arrabbio e prendo estremi ..
Thomas Bonini

Ci sono state diverse buone risposte, ma penso che la tua abbia fatto il miglior tentativo di dettagliare le origini di questa mentalità. Grazie a tutti i soggetti coinvolti!
Justin

La mia risposta ... la maggior parte degli sviluppatori
lavora

38

Venendo alla tua domanda da parte di uno sviluppatore che lavora su codice ad alte prestazioni, ci sono diverse cose da considerare nella progettazione.

  • Non pessimizzare prematuramente. Quando hai la possibilità di scegliere tra due design uguali per complessità, scegli quello con le migliori caratteristiche prestazionali. Uno dei famosi esempi di C ++ è la prevalenza del post-incremento dei contatori (o iteratori) nei loop. Questa è una pessimizzazione prematura totalmente inutile che potrebbe non costarti nulla, ma POTREBBE, quindi non farlo.
  • In molti casi non hai ancora intenzione di avvicinarti alla micro-ottimizzazione. Le ottimizzazioni algoritmiche sono un frutto più basso e sono quasi sempre molto più facili da capire rispetto alle ottimizzazioni di basso livello.
  • Se e SOLO se le prestazioni sono assolutamente critiche, ti sporchi. In realtà, si isola il codice il più possibile prima, e POI ci si sporca e si sporca. E diventa davvero sporco lì, con schemi di cache, valutazione pigra, ottimizzazione del layout di memoria per la memorizzazione nella cache, blocchi di intrinseci inline o assembly, strato su strato di modelli, ecc. Test e documenti come un matto qui, sai che sta andando fare del male se devi fare qualsiasi manutenzione in questo codice, ma devi farlo perché le prestazioni sono assolutamente critiche. Modifica: A proposito, non sto dicendo che questo codice non possa essere bello e dovrebbe essere reso il più bello possibile, ma sarà comunque molto complesso e spesso contorto rispetto al codice meno ottimizzato.

Fallo nel modo giusto, rendilo bello, fallo in fretta. In questo ordine.


Mi piace la regola empirica: 'falla bella, falla veloce. In questo ordine'. Inizierò a usarlo.
Martin York,

Esattamente. E isolare il codice nel terzo punto, il più possibile. Perché quando si passa a hardware diverso, anche qualcosa di piccolo come un processore con una dimensione della cache diversa, queste cose possono cambiare.
KeithB,

@KeithB - ​​hai un buon punto, lo aggiungerò alla mia risposta.
Joris Timmermans,

+1: "Fai la cosa giusta, rendila bella, prendila velocemente. In questo ordine." Riepilogo molto bello, con il quale concordo al 90%. A volte riesco a correggere alcuni bug (farlo bene) solo quando lo trovo bello (e più comprensibile).
Giorgio,

+1 per "Non pessimizzare prematuramente". Il consiglio di evitare l'ottimizzazione precoce non è il permesso di impiegare in modo sfrenato algoritmi stonati. Se stai scrivendo Java e hai una raccolta che chiamerai containsmolto, usa a HashSet, non a ArrayList. Le prestazioni potrebbero non avere importanza, ma non c'è motivo di non farlo. Sfruttare le congruenze tra il buon design e le prestazioni: se si elabora una raccolta, provare a fare tutto in un unico passaggio, che sarà probabilmente più leggibile e più veloce (probabilmente).
Tom Anderson,

16

Se posso presumere di "prendere in prestito" il bel diagramma di Greengit e fare una piccola aggiunta:

|
P
E
R
F
O  *               X <- a program as first written
R   * 
M    *
A      *
N        *
C          *  *   *  *  *
E
|
O -- R E A D A B I L I T Y --

Siamo stati tutti "insegnati" che ci sono curve di compromesso. Inoltre, abbiamo tutti supposto di essere programmatori così ottimali che ogni dato programma che scriviamo è così stretto da essere sulla curva . Se un programma si trova sulla curva, qualsiasi miglioramento in una dimensione comporta necessariamente un costo nell'altra dimensione.

Nella mia esperienza, i programmi si avvicinano a qualsiasi curva solo essendo sintonizzati, ottimizzati, martellati, cerati e in generale trasformati in "codice golf". La maggior parte dei programmi offre ampi margini di miglioramento in tutte le dimensioni. Ecco cosa intendo.


Personalmente penso che ci sia un'altra estremità della curva in cui risale sul lato destro (purché ti sposti abbastanza a destra (il che probabilmente significa ripensare il tuo algoritmo)).
Martin York,

2
+1 per "La maggior parte dei programmi ha molto spazio per miglioramenti in tutte le dimensioni."
Steven,

5

Proprio perché i componenti software ad alte prestazioni sono generalmente ordini di grandezza più complessi di altri componenti software (a parità di tutte le altre cose).

Anche in questo caso non è così chiaro, se le metriche delle prestazioni sono un requisito di fondamentale importanza, è indispensabile che il progetto abbia complessità per soddisfare tali requisiti. Il pericolo è uno sviluppatore che spreca uno sprint su una funzione relativamente semplice, cercando di spremere qualche millisecondo in più dal suo componente.

Indipendentemente da ciò, la complessità della progettazione ha una correlazione diretta con la capacità di uno sviluppatore di apprendere rapidamente e acquisire familiarità con tale progettazione, e ulteriori modifiche alla funzionalità in un componente complesso possono causare errori che potrebbero non essere rilevati dai test unitari. I progetti complessi hanno molte più sfaccettature e possibili casi di test da considerare per rendere l'obiettivo del 100% di copertura unitaria un sogno ancora più arduo.

Detto questo, va notato che un componente software con prestazioni scarse potrebbe funzionare male solo perché è stato scritto in modo sciocco e inutilmente complesso sulla base dell'ignoranza dell'autore originale, (effettuando 8 chiamate al database per costruire una singola entità quando solo uno lo farebbe , codice completamente inutile che si traduce in un singolo percorso di codice indipendentemente, ecc ...) Questi casi riguardano più il miglioramento della qualità del codice e gli aumenti delle prestazioni che si verificano come conseguenza del refactor e NON necessariamente la conseguenza prevista.

Supponendo tuttavia un componente ben progettato, sarà sempre meno complesso di un componente altrettanto ben progettato ottimizzato per le prestazioni (a parità di altre condizioni).


3

Non è così tanto che queste cose non possono coesistere. Il problema è che il codice di tutti è lento, illeggibile e non realizzabile alla prima iterazione. Il resto del tempo viene impiegato per migliorare ciò che è più importante. Se questa è una prestazione, allora provaci. Non scrivere codice dispettosamente orribile, ma se deve essere solo X veloce, allora fallo X velocemente. Credo che prestazioni e pulizia siano sostanzialmente non correlate. Il codice performante non causa codice brutto. Tuttavia, se passi il tempo a ottimizzare ogni bit di codice per essere veloce, indovina cosa non hai trascorso facendo il tuo tempo? Rendi il tuo codice pulito e gestibile.


2
    |
    P
    E
    R
    F
    O *
    R * 
    M *
    A *
    N *
    C * * * * *
    E
    |
    O - LEGGIBILITÀ -

Come potete vedere...

  • Sacrificare la leggibilità può aumentare le prestazioni, ma solo così tanto. Dopo un certo punto, devi ricorrere a mezzi "reali" come algoritmi e hardware migliori.
  • Inoltre, la perdita di prestazioni a scapito della leggibilità può avvenire solo in una certa misura. Successivamente, puoi rendere il tuo programma leggibile quanto desideri senza influire sulle prestazioni. Ad esempio, l'aggiunta di commenti più utili non influisce negativamente sulle prestazioni.

Quindi, prestazioni e leggibilità sono solo modestamente correlate - e nella maggior parte dei casi, non ci sono veri e propri incentivi che preferiscono il primo al secondo. E sto parlando di lingue di alto livello.


1

A mio avviso, le prestazioni dovrebbero essere considerate quando si tratta di un problema reale (o, ad esempio, un requisito). Non farlo tende a portare a microottimizzazioni, che potrebbero portare a codice più offuscato solo per risparmiare qualche microsecondo qua e là, che a sua volta porta a codice meno gestibile e meno leggibile. Invece, si dovrebbe concentrarsi sui veri colli di bottiglia del sistema, se necessario , e porre l'accento sulle prestazioni lì.


1

Il punto non è la leggibilità dovrebbe sempre superare l'efficienza. Se sai dall'inizio che il tuo algoritmo deve essere altamente efficiente, sarà uno dei fattori che utilizzi per svilupparlo.

Il fatto è che la maggior parte dei casi non ha bisogno di un codice rapido accecante. In molti casi l'IO o l'interazione dell'utente causano molti più ritardi rispetto all'esecuzione dell'algoritmo. Il punto è che non dovresti fare di tutto per rendere qualcosa di più efficiente se non sai che è il collo di bottiglia.

L'ottimizzazione del codice per le prestazioni spesso lo rende più complicato perché in genere implica fare le cose in modo intelligente, anziché nel modo più intuitivo. Il codice più complicato è più difficile da mantenere e più difficile da raccogliere per gli altri sviluppatori (entrambi sono costi che devono essere considerati). Allo stesso tempo, i compilatori sono molto bravi a ottimizzare i casi comuni. È possibile che il tuo tentativo di migliorare un caso comune significhi che il compilatore non riconosce più il modello e quindi non può aiutarti a velocizzare il tuo codice. Va notato che ciò non significa scrivere quello che vuoi senza preoccuparti delle prestazioni. Non dovresti fare nulla che sia chiaramente inefficiente.

Il punto è non preoccuparsi di piccole cose che potrebbero migliorare le cose. Usa un profiler e vedi che 1) quello che hai ora è un problema e 2) quello in cui lo hai cambiato è stato un miglioramento.


1

Penso che la maggior parte dei programmatori abbia questa sensazione semplicemente perché la maggior parte delle volte, il codice delle prestazioni è un codice basato su molte più informazioni (sul contesto, la conoscenza dell'hardware, l'architettura globale) rispetto a qualsiasi altro codice nelle applicazioni. La maggior parte del codice esprimerà solo alcune soluzioni a problemi specifici che sono incapsulati in alcune astrazioni in modo modulare (come funzioni) e ciò significa limitare la conoscenza del contesto solo a ciò che entra in quell'incapsulamento (come parametri di funzione).

Quando si scrive per prestazioni elevate, dopo aver corretto eventuali optilizzazioni algoritmiche, si ottengono dettagli che richiedono una conoscenza molto maggiore del contesto. Ciò potrebbe naturalmente sopraffare qualsiasi programmatore che non si senta abbastanza concentrato per il compito.


1

Perché il costo del riscaldamento globale (da quei cicli di CPU aggiuntivi ridimensionati da centinaia di milioni di PC più enormi strutture del data center) e una durata della batteria mediocre (sui dispositivi mobili dell'utente), come richiesto per eseguire il loro codice scarsamente ottimizzato, raramente si presenta sulla maggior parte prestazioni del programmatore o revisioni tra pari.

È un'esternalità economica negativa, simile a una forma di inquinamento ignorato. Quindi il rapporto costi / benefici di pensare alle prestazioni è mentalmente distorto dalla realtà.

I progettisti hardware hanno lavorato duramente per aggiungere funzionalità di risparmio energetico e ridimensionamento del clock alle CPU più recenti. Spetta ai programmatori consentire all'hardware di sfruttare queste funzionalità più spesso, non rompendo ogni ciclo di clock della CPU disponibile.

AGGIUNTO: Nei tempi antichi, il costo di un computer era di milioni, quindi l'ottimizzazione del tempo della CPU era molto importante. Quindi il costo per lo sviluppo e la manutenzione del codice è diventato maggiore del costo dei computer, quindi l'ottimizzazione è andata decisamente in disaccordo rispetto alla produttività del programmatore. Ora, tuttavia, un altro costo sta diventando maggiore del costo dei computer, il costo di alimentazione e raffreddamento di tutti quei data center sta diventando maggiore del costo di tutti i processori all'interno.


A parte la domanda se i PC abbiano contribuito al riscaldamento globale, anche se fosse reale: è un errore, che una maggiore efficienza energetica porta a una minore domanda di energia. Quasi il contrario è vero, come si può vedere dal primo giorno in cui un PC è apparso sul mercato. Prima di ciò, alcune centinaia o migliaia di mainframe (ognuna dotata virtualmente della propria centrale elettrica) consumavano molta meno energia rispetto ad oggi, dove 1 minuto di CPU calcola molto più di allora ad una frazione del costo e della domanda di energia. Tuttavia, la domanda totale di energia per l'informatica è più elevata di prima.
Ingo,

1

Penso che sia difficile raggiungere tutti e tre. Due penso che possano essere fattibili. Ad esempio, penso che sia possibile ottenere efficienza e leggibilità in alcuni casi, ma la manutenibilità potrebbe essere difficile con il codice microtun sintonizzato. Il codice più efficiente del pianeta in genere mancherà sia di manutenibilità che di leggibilità, come è probabilmente ovvio per la maggior parte, a meno che tu non sia il tipo in grado di capire la mano Codice SIMD multithread vettorializzato a mano che Intel scrive con assembly incorporato o il più efficace -edge algoritmi utilizzati nel settore con documenti matematici di 40 pagine pubblicati solo 2 mesi fa e 12 biblioteche di codice per una struttura di dati incredibilmente complessa.

Micro-Efficiency

Una cosa che suggerirei che potrebbe essere contraria all'opinione popolare è che il codice algoritmico più intelligente è spesso più difficile da mantenere rispetto all'algoritmo più microtunito. Questa idea secondo cui i miglioramenti della scalabilità generano più denaro per il codice micro-sintonizzato (es: schemi di accesso compatibili con la cache, multithreading, SIMD, ecc.) Sono qualcosa che sfiderei, almeno avendo lavorato in un settore pieno di estremamente complesso strutture di dati e algoritmi (il settore degli effetti visivi), specialmente in settori come l'elaborazione della mesh, perché il botto potrebbe essere grande ma il dollaro è estremamente costoso quando si introducono nuovi algoritmi e strutture di dati di cui nessuno ha mai sentito parlare da quando sono marchi nuovo. Inoltre, io '

Quindi questa idea che le ottimizzazioni algoritmiche prevalgono sempre, diciamo, sulle ottimizzazioni legate ai modelli di accesso alla memoria è sempre qualcosa con cui non sono del tutto d'accordo. Ovviamente se stai usando una sorta di bolla, nessuna quantità di micro-ottimizzazione può aiutarti lì ... ma, a ragion veduta, non credo sia sempre così ben definita. E probabilmente le ottimizzazioni algoritmiche sono più difficili da mantenere rispetto alle microottimizzazioni. Troverei molto più facile mantenere, per esempio, Embree di Intel che prende un algoritmo BVH classico e semplice e ne sintonizza semplicemente la cazzata rispetto al codice OpenVDB di Dreamwork per modi all'avanguardia di accelerare algoritmicamente la simulazione fluida. Quindi, almeno nel mio settore, mi piacerebbe vedere più persone che hanno familiarità con l'architettura del computer e micro-ottimizzazione di più, come ha fatto Intel quando sono entrati in scena, al contrario di trovare migliaia e migliaia di nuovi algoritmi e strutture di dati. Con efficaci micro-ottimizzazioni, le persone potrebbero potenzialmente trovare sempre meno ragioni per inventare nuovi algoritmi.

Ho lavorato in una base di codice legacy prima in cui quasi ogni singola operazione dell'utente aveva una propria struttura di dati e un algoritmo unici dietro (aggiungendo fino a centinaia di strutture di dati esotici). E la maggior parte di loro aveva caratteristiche prestazionali molto distorte, essendo strettamente applicabili. Sarebbe stato molto più semplice se il sistema potesse ruotare intorno a un paio di dozzine di strutture di dati ampiamente applicabili, e penso che sarebbe potuto essere il caso se fossero state micro-ottimizzate molto meglio. Cito questo caso perché la micro-ottimizzazione può potenzialmente migliorare enormemente la manutenibilità in questo caso se ciò significa la differenza tra centinaia di strutture di dati micro-pessimizzate che non possono nemmeno essere utilizzate in modo sicuro per scopi di sola lettura rigorosi che comportano mancate mancanze della cache e destra vs.

Linguaggi Funzionali

Nel frattempo alcuni dei codici più gestibili che abbia mai incontrato erano ragionevolmente efficienti ma estremamente difficili da leggere, poiché erano scritti in linguaggi funzionali. In generale, la leggibilità e la superabilità sono idee contrastanti secondo me.

È davvero difficile rendere il codice leggibile, mantenibile ed efficiente tutto in una volta. In genere è necessario scendere a compromessi in una di quelle tre, se non due, come compromettere la leggibilità per la manutenibilità o compromettere la manutenibilità per l'efficienza. Di solito è la manutenibilità che soffre quando cerchi molti degli altri due.

Leggibilità vs. manutenibilità

Ora, come detto, credo che la leggibilità e la manutenibilità non siano concetti armoniosi. Dopo tutto, il codice più leggibile per la maggior parte di noi mortali è mappato in modo molto intuitivo ai modelli di pensiero umani, e i modelli di pensieri umani sono intrinsecamente soggetti a errori: " Se questo accade, fallo. Se ciò accade, fallo. Altrimenti. Oops. , Ho dimenticato qualcosa! Se questi sistemi interagiscono tra loro, questo dovrebbe accadere in modo che questo sistema possa farlo ... oh aspetta, che dire di quel sistema quando questo evento viene attivato?"Ho dimenticato la citazione esatta, ma qualcuno una volta ha detto che se Roma fosse costruita come un software, ci vorrebbe solo un uccello che atterra su un muro per farla crollare. Questo è il caso della maggior parte dei software. È più fragile di quanto ci interessi spesso Alcune righe di codice apparentemente innocuo qua e là potrebbero fermarlo al punto da farci riconsiderare l'intero progetto, e linguaggi di alto livello che mirano a essere il più leggibili possibile non fanno eccezione a tali errori di progettazione umana .

I linguaggi puri funzionali sono quasi invulnerabili a ciò come si può ottenere (nemmeno vicino a invulnerabili, ma relativamente molto più vicini della maggior parte). E questo in parte perché non si associano in modo intuitivo al pensiero umano. Non sono leggibili. Costringono su di noi schemi di pensiero che ci obbligano a risolvere i problemi con il minor numero possibile di casi speciali utilizzando la minima quantità di conoscenza possibile e senza causare effetti collaterali. Sono estremamente ortogonali, permettono spesso di cambiare e cambiare il codice senza sorprese, così epico che dobbiamo ripensare il design su un tavolo da disegno, fino al punto di cambiare idea sul design generale, senza riscrivere tutto. Non sembra essere più facile da mantenere di così ... ma il codice è ancora molto difficile da leggere,


1
"Micro-efficienza" è un po 'come dire "Non esiste nulla come l'accesso alla memoria O (1)"
Caleth,

0

Un problema è che il tempo limitato per gli sviluppatori significa che qualsiasi cosa tu cerchi di ottimizzare toglie il tempo a dedicare altre questioni.

C'è un esperimento piuttosto buono fatto su questo a cui si fa riferimento nel Codice completo di Meyer. A diversi gruppi di sviluppatori è stato chiesto di ottimizzare velocità, utilizzo della memoria, leggibilità, robustezza e così via. Si è riscontrato che i loro progetti hanno ottenuto un punteggio elevato in qualunque cosa venisse loro chiesto di ottimizzare, ma inferiori in tutte le altre qualità.


Ovviamente puoi dedicare più tempo, ma alla fine inizi a chiederti perché gli sviluppatori si prenderebbero del tempo libero programmando emacs per esprimere l'amore per i loro figli, e a quel punto in pratica sei Sheldon della teoria del Big Bang
deworde,

0

Perché programmatori esperti hanno imparato che è vero.

Abbiamo lavorato con un codice snello e mediocre che non presenta problemi di prestazioni.

Abbiamo lavorato su un sacco di codice che, per risolvere i problemi di prestazioni, è MOLTO complesso.

Un esempio immediato che mi viene in mente è che il mio ultimo progetto includeva 8.192 tabelle SQL con frammentazione manuale. Ciò era necessario a causa di problemi di prestazioni. L'impostazione per selezionare da 1 tabella è molto più semplice che selezionare e mantenere 8.192 frammenti.


0

Ci sono anche alcuni famosi pezzi di codice altamente ottimizzato che piegheranno il cervello della maggior parte delle persone a sostegno del fatto che il codice altamente ottimizzato è difficile da leggere e comprendere.

Ecco il più famoso penso. Tratto da Quake III Arena e attribuito a John Carmak, anche se penso che ci siano state diverse iterazioni di questa funzione e non è stato originariamente creato da lui ( non è grande Wikipedia? ).

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}
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.