Quote:
Originariamente inviato da yossarian
il compilatore generico c'è per hlsl, per open64 e c'è anche per glsl. Secondo te, ora che stanno per lanciare fermi, nVidia si mette a scrivere un nuovo compilatore per opengl? Oppure, secondo te, nei driver unificati c'è una parte comune o abbiamo tanti compilatori quante sono le architetture dei chip supportati? Sai che due chip che hanno identica architettura ma, ad esmepio, differente numero dei registri, hanno differenti ISA? Ad esempio, G80 è differente da G70 che è diverso da NV40 che non è neppure parente di NV30 e tutti sono diversi da G92, da GT200 ecc. E tutti saranno molto diversi da fermi. Non esiste un compilatore ottimizzato per una determinata architettura con nessun tipo di API. Ogni compilatore è diviso in due partI: una standard che genera bytecode e una che fa compilazione JIT e gira, per i chip dx10 o successivi, su una VM.
|
Infatti per ogni architettura nvidia come ati e come ogni sviluppatore di chip grafico ha un compilatore ad hoc (uno per NV30, NV40, G80, ecc...). Se non fosse così non lo chiamaerebbero neppure compilatore ma traduttore (infatti la parte "standard" nel sistema operativo che agisce sull'HLSL si chiama HLSL translator, non HLSL compiler; la parte che compila è sempre dentro nel driver), visto che per sua natura dal compilatore esce codice macchina, che è quindi "personalizzato" e diverso per ogni serie di chip, e quindi ognuno avrà il suo bel compilatore. Riguarda anche questo l'onere dello sviluppo dei driver.
Quote:
Originariamente inviato da yossarian
allora avevo capito bene! Il modello che proponi è con le sostituzioni at compile time? Esattamente quello che nVidia non vuole. Grosso spreco di risorse, più traffico nel bus. E, comunque, le sostituzioni si possono fare solo su codice binario.
|
invece proprio quello che, in qualsiasi ambito, si cerca di fare è poter fare tutto il possibile a compile time, per poi ridurre il lavoro e quindi tempo perso a run-time. Infatti siccome il compile -time sa fa una volta e il run-time tutte le volte, i benefici sono ovvi. C'è maggior lavoro di programmazione, ma gli sviluppatori dei driver sono li anche per quello. Non c'è poi nessuno spreco di risorse, niente traffico sul bus, visto che avvenendo nel driver a compile-time (dello shader, run-time dell'applicazione) è tutto PRIMA che lo shader venga eseguito.
Quote:
Originariamente inviato da yossarian
ma anche no. Parliamo di ambiti dove il problema principale è fare le ottimizzazioni il più in fretta possibile e non perdere tempo a ottimizzare, magari per anni, per una determinata architettura. Questo perchè cambiano le architetture e queste si devono adeguare rapidamente alle applicazioni vecchie e nuove. Inoltre, lo schema indicato da nVidia è piuttosto eloquente: da un parte il codice compilato che non deve essre ottimizzato (la stragrande maggioranza) che è quello indicato con "codice pr la cpu"; dall'altro quello che richiede la compilazione JIT che è una piccola parte ed è anche quello che viene ottimizzato. Quindi non si fa la ricompilazione di tutto ma solo quello destinato alla gpu diventa bytecode per girare su VM. Inoltre, l'utilità di scindere il due parti l'operazione di compilazione permette di unire i vantaggi della compilazione at compile time (con compiler standard) e dell'ottimizzazione at run time per architetture che, con ottimizzazione dinamica arrivano a guadagnare, statisticamente, anche un 30-40% in più rispetto ad una fatta at compile time (quindi ci sono anche evidennti vantaggi prestazionali)
|
ti stai riferendo probabilmente allo schema contenuto nella seconda pagina del .doc giò liknato in precedenza:
http://www.capsl.udel.edu/conference...Papers/101.doc
Come scritto nel post precedente inserire CUDA nel discorso confonde e basta. Questo schema prima di tutto non è uno schema di cosa avviene a runtime ma della catena che segue il codice dell'applicazione durante la fase di sviluppo, e quindi quando si parte dal sorgente. L'ho già spiegato prima in modo + dettagliato ma faccio un breve sunto (se si vuole dipiù basta leggere qualche post precedente). Siamo al punto che lo sviluppatore ha finito il suo progetto e deve compilare.Il grafico si divide in due dopo essere passato in "cudafe" perchè deve dividere il codice c generico dell'applicazione destinato alla CPU dal codice cuda che dovrà essere gestito dalla GPU. La parte sinistra verrà quindi gestita in maniera tradizionale usando gli strumenti di compilazione che lo sviluppatore usa solitamente (se siamo in visual studio per esempio usare il compilatore c di quell'ambiente). La parte destra invece prende una strada diversa, perchè dovrà essere compilata non per girare sulla CPU ma sulla GPU presente nel sistema, quindi ci sono tutti qui bei moduli software che servono a fa arrivare un codice "utilizzabile" dal solito OCG/JIT in modo che possa gestirlo e compilarlo come se fosse uno shader. Tutta sta roba nella grafica 3d non c'è perchè non c'è nulla da dividere (l'applicativo e gli shader sono già divisi) e la parte del grafico che li sta a sinistra NVIDIA non la vede nemmeno perchè non passando da "cudafe" non sa neanche che esista (è una battuta, ovviamente se gli shader arrivano al driver ci sarà una applicazione che glieli invia...), come non ci saranno tutte quelle operazioni della parte destra per adattare il codeice alla GPU. Sulla ultima parte riguardante le prestazioni, mi sembra di averne parlato ampiamente già nei miei post precedenti. Posso solo aggiungere che niente è più veloce di un linguaggio compilato nativamente. Infatti, parlando di programmazione generica e non grafica (dove è più facile per tutti quelli che leggono capire), un programma compilato in c è imbattibile per chiunque, che sia JAVA, uno qualsiasi della suite .net (c# ad esempio), perchè questi ultimi sono interpretati e non compilati. La compilazione JIT è stata introdotta soltanto per tentare di ridurre il gap, che comunque rimane. Se come riferimento prendo le prestazione senza la fase di JIT, il c è anche 10-20 volte più veloce. Nella grafica se ci fosse stata scelta avrebbero sicuramente compilato tutto anche li; ma visto che a differenza delle CPU in cui, almeno per il mondo windows che è quello di cui stiamo parlando, il x86 rimane incontrastato e ci sono solo delle estensioni (mmx, sse, ecc...) che si possono decidere o meno di utilizzare (e la granparte dei programmi non usa), nelle GPU l'architettura cambia in continuazione. Se gli sviluppatori compilassero i sorgenti degli shader come compilano i sorgenti dell'applicativo/gioco, visto che i tempi di sviluppo dei gichi tripla A di oggi rihiedono anni, alla data di uscita ci sarebbero già le nuove schede che non funzionerebbero. Per cui si è scelta questa strada di "compromesso" in cui la parte shader viene compilata a run-time, ma comunque compilata prima di essere eseguita.
Quote:
Originariamente inviato da yossarian
nVIdia dice che le ottimizzazioni si fanno JIT. Ora, seguendo lo schema visto per CUDA, è evidente che il "ramo cpu" non presenta alcun optimizer e non riconosce neppure l'architettura del chip. Quindi non può in alcun modo fare ottimizzazioni per fermi. Immagina un driver che faccia la sostituzione di cui dici at compile time nel "ramo cpu". La sostituzione avviene indiscriminatamente per tutti i chip e, così, fermi lavora in maniera ottimale, GT200 diventa un chiodo e tutti gli altri neppure partono  . Viceversa, se le ottimizzazioni i fanno nel "ramo gpu", PTX ricinisce l'architettura, sa che sta girando su fermi e effettua la sostituzione, oppure che si tratta di GT200 e lascia invariate le MADD. Però, come specificato e più volte ripetuto, le ottimizzazioni avvengono JIT e non at compile time.
|
Riguarda sempre CUDA e qundi vale il discorso precedente. Voglio solo sottlineare ancora, se ce ne fosse bisogno, che del "ramo cpu" alla GPU non gliene può fregare di meno, ovvio che li non viene compilato niente (da NVIDIA), perchè non gira su NVIDIA ma sul processore, ed infatti quella parte di codice viene trattata dal compilatore della piattaforma di sviluppo dello sviluppatore. Anche questo già detto, ma è anche ovvio che tutta questa fase non ha nessuna pertinenza con l'esecuzione del programma, ma serve a chi lo fa. Infatti nel PC di chi esegue l'applicativo il ramo sinistro non esiste (e anche buona parte di quello destro).
Quote:
Originariamente inviato da yossarian
skizzo, mi sei simpatico e sei anche preparato, ma questo non l'ho proprio capito. Gli shader sono una cosa avulsa dall'applicazione?
Ok, torniamo seri (i complimenti sono sentiti, però).
|
Vedo che erano le 2 del mattino, magari eri più stravolto di me... ma davvero hai dei dubbi sul fatto che gli shader siano distinti dall'applicazione? Questa è talmente grossa che non so neanche da dove partire. Faccio un breve schemino base:
applicazione (per mia semplicità scritta in pseudo c)
int main (int argc, char** argv)
{
inizializzo_contesto();
leggo_config.ini();
carico_modelli();
carico_texture();
carico_shader();
...
inizializzo_luci();
inizializzo_VBO();
...
mainloop();
}
all'inizio subito dopo aver inizializzato il contesto/i (se uso varie librerie), leggo un file di configurazione che mi dice i valori di alcuni settaggi che posso impostare; poi passo a caricare i modelli, le texture (le quali possono venire subito spostate nella vram), i modelli poligonali del mondo e carico gli shader. Dentro la carica shader si carica, compila, linka, ecc... gli shader che possono essere in file distinti (io facci così) in nuo solo oppure, per mantenere meglio la proprietà intellettuale se si è implementato un algoritmo originale, possono essere anche piazzati sottoforma di stringa all'interno del sorgente c, ma è un po scomodo poi modificarli. Questo ad esempio si applica in casi in cui si debba modificare in base ad alcuni stati di esecuzione lo shader che si deve applicare, che comunque deve poi passare alla compilazione, link, ecc...
Ad esempio la funzione che inizializza gli shader che uso io è più o meno così (per un singolo shader):
// geometry shader
GLchar *GeometryStageVSname = "Shaders/GeometryStageVS.vs";
GLchar *GeometryStageFSname = "Shaders/GeometryStageFS.fs";
GLubyte *GeometryStageVStext;
GLubyte *GeometryStageFStext;
create_vertex_shader_object(cpw, "Geometry", &GeometryStageVS);
create_fragment_shader_object(cpw, "Geometry", &GeometryStageFS);
attach_shader(cpw, &GeometryStageVSname, &GeometryStageVStext, &GeometryStageVS);
attach_shader(cpw, &GeometryStageFSname, &GeometryStageFStext, &GeometryStageFS);
compile_shader(cpw, "Geometry", "vertex", &GeometryStageVS);
compile_shader(cpw, "Geometry", "fragment", &GeometryStageFS);
create_attach_program_object(cpw, &GeometryProgram, &GeometryStageVS, &GeometryStageFS);
link_program_object(cpw, "Geometry", &GeometryProgram);
validate_program_object(cpw, "Geometry", &GeometryProgram);
Ci sono tutte le fasi preliminari che avvengono prima dell'esecuzione e che istruiscono il compilatore su cosa e quando deve tradurre.
C'è poi la fase di inizializzazione che mi sembra autoesplicativa tranne forse per la il VBO che è il VertxBufferObject e che serve per "compattare" e inviare in un formato gestibile più velocemente (e nella vram) i modelli caricati iin precedenza. Poi si lancia il mainloop, che rappresenta il ciclo di rendering che sarà eseguito all'infinito. Durante questo ciclo, dovrò renderizzare i modelli che ho caricato. Mettiamo (sto sempre semplificando molto) di avere una funzione che piazza un albero in un certo punto. Questa funzione conterrà anche i "settaggi" che servono per definirlo (colore, texture, posizione, ecc...) ed anche la chiamata al nostro benedetto shader che utilizzerà queste informazioni come input per disegnare l'albero usando gli algoritmi di illuminazione, ombre, ecc... che abbiamo scritto nello shader (ne servirà più di uno, ma facciamo finta di no) e che, tramite quels'ultimo, la GPU applicherà ai vertici e pixel che gli "arrivano" (dipende se è un vertex o un pixel shader). Quindi:
imposto_colore();
uso_texture23();
traslo_nello_spazio();
glUseProgram(LightningProgram);
disegno_albero(qua dentro ci saranno specificati i vertici, o comunque quale parte dei VBO usare);
E' importante sottolineare che al momento della carico_shader() se stessi eseguendo il gioco sarei nella situazione di run-time dell'applicazione, ma di compile-time dello shader, il quale comunque una volta terminata questa fase non va in esecuzione nella GPU fino al raggiungimento di una glUseProgram.
Quando lo sviluppatore ha finito il gioco, ha delle cartelle che contengono il codice sorgente del gioco, le texture, i suoni, i modelli, ecc... . Ci sarà probabilmetne anche una cartella che contiene gli shader (se non ha deciso di codificarlo in una stringa nel sorgente, ma per quello che succede dopo non cambia niente e rende solo più difficile seguire il discorso). Alla fine quindi compila il sorgente del gioco ed ottiene un bell'exe (fa sparire così il sorgente che tiene per se). Poi mette tutto su DVD e lo mette in vendita. L'utente compra il dvd, installa il gioco e si ritrova nella stessa situazione dello sviluppatore, ma senza il sorgente. Ha però tutti i modelli, texture, ecc... tra cui anche gli shader, ancora da compilare (per gli ovvi motivi spiegati più in alto nel post), addirittura in sorgente se è un gioco opengl (cioè se li apro con notepad vedo prorpio il codice GLSL ad alto livello, provate a scaricarvi un demo opengl già compilato che contenga shader GLSL e potrete verificare).
Quando l'utente fa doppio click sull'eseguibile, il gioco esegue le funzioni descritte in precedenza, compresa la carico_shader() in cui faccio la chiamata alla compilazione. Il driver riceve gli shader e li compila. Quando arriverà il momento della glUseProgram(LightningProgram), vengono messi in esecuzione i vertex e pixel shader compilati (magari anche geometry shader se siamo in una versione delle directx/opengl che li usa) che verranno eseguiti sui successivi dati in arrivo, finche non chiamerò una glUseProgram su uno shader diverso.
Un aneddoto divertente che ho scoperto tempo fa riguarda gli shader per calcolare l'SSAO (screen space ambient occlusion), che stavo cercando di capire bene in modo da implementarla. L'ambient occlusion è un effetto che simula l'illuminazione globale (che a differenza dei classici modelli locali tiene conto delle riflessioni/rifrazioni delle luci) considerando la vicinanza e inclinazione tra i vari poligoni che compongono un modello cercando di calcolare dove ci saranno delle zone + scure (angoli, cavità, ecc...) in cui la luce non "passa" per bene. E' una descrizione brutale ma se volete saperne di più in rete è una tecnica ampiamente descritta, almeno qualitativamente. E' quindi ovvio che calcolando la visibilità di ogni poligono rispetto agli altri (cioè quanta ombra ogni poligono potrebbe fare a gli altri che compongono la scena), richiede un tempo quadratico e quindi improponiobile in realtime, soprattutto se calcolata poi per pixel e non per vertex. E' stata per esempio utilizzata in shrek. Crytek, nel fantomatico Crysis (ed è uno dei tanti motivi per cui è pesante come la morte) ha introdotto una tecnica per approssimare l'AO in screen space (da cui SSAO), in modo che i calcoli vengano fatti solo sui pixel che compongono l'immagine visualizzata e non anche i pixel delle superfici nascoste o non visualizzate, ma che comunque concorrerebbero alla occlusione e quindi si dovrebbe tenerne conto. Il paper,presentato nel 2007 che descriveva in modo molto sisntetico e qualitativo la tecnica, è stato oggetto di "culto" per diverso tempo in cui molti hanno tentato di replicare la tecnica, ma con risultati inferiori. Questo fino a che qualcuno (con due OO così), sbirciando nelle cartelle del gioco ha trovato gli shader e aprendoli è riuscito + o - a capirne la tecnica pratica di base. Avevo provato anch'io a guardarli (non ho crysis ma li avevano subito piazzati in rete), ma non erano molto intuitivi, poco commentati e per giunta in directx per cui ho rinunciato. C'è anche da dire che se non vengono utilizzati dei nomi autoesplicativi per le variabili e i sampler delle texture, uno dovrebbe capire che cosa rappresentano in base a che uso ne viene fatto nello shader, ma se guardo lo shader per capire cosa fa, stiamo a cavallo...
Quote:
Originariamente inviato da yossarian
io ho capito quello che vuoi dire, ma non è possibile farlo; non in questo caso. Non stiamo parlando della sostituzione dell'istruzione che si fa una tantum: ad esempio, forzare l'AA in un gioco da driver. In quel caso, il tutto avviene all'inizio, modificando una sola impostaizone all'atto del caricamento del gioco. A tutto il resto pensa la gpu che ha il suo algortmo di edge detect che entra in funzione in automatico quando viene forzato il MSAA. Qui parliamo di una sostituzione che si deve fare su tutte le istruzioni di pixel, vertex e geometry shader e texture blending, tenendo conto anche di eventuali operazioni di tessellatin che generano un numero molto più elevato di vertici su cui si devono applicare le stesse operazioni, di shader procedurali e di vertex e geometry shader che generano un gran numero di pixel su cui si devono operare delle madd. E il tutto avviene all'interno della gpu e non al suo esterno e non è neppure pensabile che, alla bisogna, le istruzioni che si rendono necessarie vengono ottimizzate dalla cpu e caricate al volo (passerebbero secoli). In realtà è la prima volta che ci si trova di fronte ad una situazione in cui si potrebbe rendere necessaria una sostituzione di istruzioni su vastissima scala.
Per risolvere il problema tu proponi un'ottimizzazione, di fatto, at compile time che nVidia (ma non solo lei) non gradisce, che mal si adatta ad un'architettura superscalare (che necessità di un compilatore dinamico per poter meglio ottimizzare) e che comporta un notevole dispendio di risorse. Inoltre, ripeto, la stessa nVidia parla di ottimizzazini JIT e non at compile time.
|
Il JIT si fa a compile-time! è compile-time dello shader e run-time dell'applicazione, ma visto che stiamo parlando di GPU è compile-time! in fatti il JIt è un compilatore e compila; per compilare dovrà pure leggerlo il codice no? quindi se lo shader ha 200 istruzioni, dopo la compilazione magari ci sono 1000 MADD, bene: il driver le cancella e gli mette 1000 FMA. quanto vuoi che ci metta? siccome poi sta cosa la deve fare una volta sola (perchè sta compilando il codice, non lo sta eseguendo, l'esecuzione si farà sul codice che esce da quà e che si va poi a piazzare per essere eseguito dagli stream processor), è evidente che sarà ben più efficiente. E con una volta solo non intendo ovviamente che se ci sono 1000 MADD con UNA sostituzione sono a posto: è ovvio che le cambio tutte e 1000. Ma diciamo che lo shader viene eseguito 60 volte al secondo. LA GPU farebbe 1000*60 sostituzioni al secondo (nel migliore dei casi: è molto probabile che ci siano dei cicli e che quindi nel codice ci siano meno istruzioni di quante siano effettivamente le esecuzioni, anche se non so le l'assembler di ogni GPU prevede i goto o similari come in x86 oppure "srotola" il ciclo) se così non fosse contro le 0 se la sostituzione venisse (come avviene) effettuata dal JIT prima dell'inizio dell'elaborazione dello shader. Che poi quelle numerose sostituzioni possano incidere poco e siano tecnicamente altamente mascherabili non lo metto in dubbio (infatti per cose che non si possono "aggiustare" a compile time bisogna giocoforza intervenire a run-time), ma battere lo 0 mi risulta sia davvero difficile.
Quote:
Originariamente inviato da yossarian
CUDA è c-like come hlsl e glsl e non ha nulla di complesso: è open64 con qualche ottimizzazione per chip nVidia. Studiarlo è utile per capire come funzionano i chip, quali sono le loro potenzialità e qual è la strategia di nVidia (anche in relazione alle ottimizzazioni).
|
Mi sembra di avere già spiegato perchè nella nostra discussione CUDA crei solo confusione e non sia per niente d'aiuto
Quote:
Originariamente inviato da yossarian
A me risulta che una compilazione JIT prevede, invece, proprio la compilazione mentre l'applicazione sta girando. Se si compila prima tutto il codice e lo si ottimizza parliamo di compile time. Semmai può essere che si tratti di un aparte del codice e, mentre la gpu lavora su questo, la cpu ottimizza un'altra porzione, ma, allora, il traffico si crea eccome e si crea anche una gran confusione. Chi dà alla cpu le informazioni sulle istruzioni chiamate sulla gpu? Come sa quale parte del codice ottimizzare se non sa su cosa sta lavorando la gpu perchè non ha accesso al suo interno? Come fa a sapere che un'istruzione non abbia generato dati che devono essere processati prima di altri già schedulati? In parole povere, in base a quale criterio decide l'ordine in cui procedere?
se, poi, parliamo di compile time, allora è un altro paio di maniche (ma nVidia dice JIT e un'architettura superscalare ha bisogno di compilazione dinamica).
|
Sempre lo stesso inghippo: Come già spiagato, la tecnica JIT è stata introdotta per evitare la fase di interpretazione dei linguaggi interpretati (può sembrare un controsenso ma è così). Per esempio JAVA:
sviluppatore
sorgente->(compilatore)->bytecode
utente (senza JIT):
bytecode->(interprete)->esecuzione
L'interprete è l'anello debole della catena perchè deve tradurre in linguaggio macchina istruzione per istruzione, e (in una implementazione senza scorciatotie) se c'è un loop da eseguire 100 volte, lo traduce 100 volte. Con il JIT invece appena prima dell'esecuzione (ho fatto doppio click con i mouse) compila direttamente il codice, in modo da poterlo eseguire più velocemente, Ovviamente questo comporta che il lancio sarà più lungo, ma è ampiamente compensato dalla maggior prestazione in esecuzione. Il JIT per gli shader lavora un po più "tranquillo", perchè quando l'applicativo lancia la compile, link, ecc..., non è che l'operazione successiva è l'esecuzione dello shader, ma probabilmente ci saranno altre cose da fare (come da esempio poco sopra). E' quindi un compile-time reale, ma viene chiamato JIT comunque perchè
1) viene fatto mentre l'applicativo sta già girando e lo shader partirà di li a poco.
2) viene comunque rieseguita tutte le volte che rieseguo l'applicazione
Quote:
Originariamente inviato da yossarian
si tratta di ottimizzazioni fortemente dipendenti dall'ordine delle istruzioni che arriverebbero già ottimizzate. Vedi punto sopra: la cpu non può sapere su cosa sta lavorando la gpu e non può, di conseguenza, fare sostituzine JIT delle istruzioni necessarie. La gestione dei thread è un processo dinamico e non statico; ripeto, qui non parliamo di forzare l'AA via driver all'atto del lancio dell'applicazione.
|
la gestione dei thread infatti viene fatta tutta dalla GPU (come molte altre cose), l'ho specificato più volte. Visto che comunque in base a quello che dico io le sostituzioni sono una delle mansioni del JIT e che quindi vengono fatte prima che lo shader passi in esecuzione, la CPU sa benissimo cosa sta facendo la GPU riguardo a quel particolare shader: nulla.
Quote:
Originariamente inviato da yossarian
ti ho già spiegato perchè non è possibile fare una "semplice sostituzione" at compile time: perchè si parla di migliaia di semplici sostituzioni che devono servire ad elaborare dati che ancora non sono stati creati e di cui non si conosce nè il numero né gli altri parametri tra cui l'ordine in cui dovranno essere processati. (la cpu conosce solo i valori iniziali dei vertici di un frame). Inoltre, ripeto per l'ennesima volta, un'architetura superscalare vuole un compiler dinamico (pena pessima ottimizzazione) e nVdia non parla di preephole optimization at compile time (ci sarà un motivo)?
|
che c'entra che i vertici/dati non ci sono? se scrivo un programma in c che prende un file txt e leggendolo sostituisce tutte le occorrenze "berlusconi" con "presidente del consiglio" restituendo un secondo file, non è che quando lo compilo ho bisogno che il compilatore abbia a disposizione i dati di input (in questo caso il TXT). Il compilatore semplicemente traduce in codice macchina le istruzioni che trova, le quali prevedono anche queste sostituzioni di stringhe. LA MADD con FMA è un po la stessa cosa: il JIT vede che ci dovrebbe essere MADD a, b, c, d e ci piazza FMA a, b, c, d. Semplice e statico.
Quote:
Originariamente inviato da yossarian
invece io ritengo, per quanto esposto, che la sostituzione, ammesso che sia possibile e che si farà, avverrà sulla gpu.
E mi pare che siamo di nuovo ad un punto morto e, poichè ho un avita sociale, un lavoro e un articolo da scrivere (tranquilli non è su fermi, anche se ci sarebbe materiale per una serie completa degna di beautiful  ), ritengo poco costruttivo contìnuare una discussione in cui ognuno ha espresso i suoi concetti (anche più volte) senza arrivare ad una conclusione di sorta. 
|
Per me va bene, anzi è anche meglio, visto che negli ultimi post sto/stiamo ripetendo le stesse cose. Spero almeno che qualcuno (basta 1 e sono contento) si sia letto per bene il materiale linkato ed abbia maturato l'interesse di approfondire un po e farsi una sua idea non basata su ciò che diciamo noi.