Una lunga intervista con Cliff Click, il padre della JIT compilation in Java

Una lunga intervista con Cliff Click, il padre della JIT compilation in JavaCliff Click — CTO dell'azienda Cratus (sensori IoT per migliorare i processi), fondatore e cofondatore di diverse startup (tra cui Rocket Realtime School, Neurensic e H2O.ai) con diversi exit di successo. Cliff scrisse il suo primo compilatore all'età di 15 anni (Pascal per TRS Z-80)! È più conosciuto per il suo lavoro su C2 in Java (the Sea of Nodes IR). Questo compilatore ha dimostrato al mondo che JIT può produrre codice di qualità, diventando uno dei fattori chiave che hanno reso Java una delle principali piattaforme software moderne. In seguito, Cliff ha aiutato l'azienda Azul Systems a costruire un mainframe a 864 core con software in pura Java, che supportava pause GC su un heap da 500 gigabyte in meno di 10 millisecondi. In generale, Cliff ha avuto l'opportunità di lavorare su tutti gli aspetti della JVM.

 
Questo post di Hubra è una lunga intervista con Cliff. Parleremo dei seguenti argomenti:

  • Passaggio a ottimizzazioni a basso livello
  • Come fare un grande refactoring
  • Modello di costo
  • Formazione su ottimizzazioni a basso livello
  • Esempi pratici di miglioramento delle prestazioni
  • Perché creare un proprio linguaggio di programmazione
  • Carriera di ingegnere delle prestazioni
  • Sfide tecniche
  • Qualche nota sull'allocazione dei registri e sulla multithreading
  • La sfida più grande della vita

Intervistatori:

  • Andrey Satarin di Amazon Web Services. Nella sua carriera ha lavorato a progetti molto diversi: ha testato un database distribuito NewSQL in Yandex, un sistema di rilevamento basato su cloud presso Kaspersky Lab, un gioco multiplayer in Mail.ru e un servizio di calcolo dei prezzi delle valute in Deutsche Bank. È interessato al testing di sistemi backend e distribuiti su larga scala.
  • , che partecipa ai comitati di programma di un'incredibile quantità di conferenze... sembra che ora siano cinque alla volta. Anche nell'altro BOF su «Modern CS in real world» si discuteva di NVM e ci sono arrivati in modo completamente autonomo. di Netcracker. Da dieci anni si occupa di prestazioni e scalabilità di NetCracker OS, un software utilizzato dagli operatori di telecomunicazioni per automatizzare i processi di gestione della rete e delle apparecchiature di rete. È appassionato di prestazioni di Java e Oracle Database. Autore di oltre una dozzina di miglioramenti delle prestazioni nel driver JDBC ufficiale di PostgreSQL.

Passaggio a ottimizzazioni a basso livello

Andrey: Sei una persona conosciuta nel mondo della compilazione JIT, di Java e della prestazione in generale, giusto? 

Cliff: Esattamente!

Andrey: Iniziamo con alcune domande generali sul lavoro sulle prestazioni. Cosa pensi della scelta tra ottimizzazioni ad alto livello e a basso livello come il lavoro a livello CPU?

Cliff: Qui è tutto semplice. Il codice più veloce è quello che non viene mai eseguito. Pertanto, è sempre necessario partire da un alto livello, lavorare sugli algoritmi. Una notazione O migliore batterà una notazione O peggiore, a meno che non intervengano delle costanti abbastanza grandi. Le questioni a basso livello vengono affrontate per ultime. Di solito, se hai ottimizzato bene tutto il resto della pila e c'è ancora qualcosa di interessante rimasto – questo è il basso livello. Ma come si inizia da un alto livello? Come si fa a sapere se è stato fatto abbastanza lavoro a livello alto? Beh… non lo si sa. Non ci sono ricette pronte. Devi capire il problema, decidere cosa intendi fare (per non compiere passi non necessari in seguito) e allora puoi già tirare fuori il profiler, che può dirti qualcosa di utile. A un certo punto, realizzi che hai eliminato le cose superflue e che è giunto il momento di dedicarti alla fine-tuning a basso livello. Questo è senza dubbio una forma d'arte. Molte persone fanno cose superflue, ma si muovono così velocemente che non hanno tempo da dedicare alle prestazioni. Ma ciò fino a quando la questione non diventa critica. Di solito, il 99% del tempo a nessuno importa cosa sto facendo, fino al momento in cui su un percorso critico non compare qualcosa di importante che interessa a qualcuno. Ed è allora che tutti iniziano a chiederti

Come fare un grande refactoring

Andrey: Come lavori sulle prestazioni? Questo è un problema trasversale. Ad esempio, ti è mai capitato di affrontare problemi derivanti dall'interazione di un gran numero di funzionalità già esistenti?

Cliff: Cerco di evitarlo. Se so che le prestazioni diventeranno un problema, rifletto prima di iniziare a codificare, specialmente sulle strutture dati. Ma spesso scopri tutto ciò molto più tardi. E allora devi ricorrere a misure estreme e fare quello che definisco 'riscrivi e domina': devi prendere un pezzo abbastanza grande. Una parte del codice dovrà comunque essere riscritta a causa di problemi di prestazioni o per qualcos'altro. Qualunque sia la ragione per riscrivere il codice, è quasi sempre meglio riscrivere un pezzo più grande piuttosto che uno più piccolo. In quel momento tutti iniziano a tremare per paura: 'Oh cielo, non possiamo toccare così tanto codice!'. Ma, di fatto, questo approccio funziona quasi sempre molto meglio. Devi affrontare subito un grande problema, delineare un grande cerchio intorno ad esso e dire: tutto ciò che è dentro il cerchio lo riscriverò. Il confine è molto più piccolo del contenuto all'interno che deve essere sostituito. E se un tale tracciamento dei confini consente di lavorare all'interno in modo perfetto – hai le mani libere, fai quello che vuoi. Non appena hai capito il problema, il processo di riscrittura diventa molto più semplice, quindi mordi un grosso pezzo!
Allo stesso tempo, quando riscrivi un grosso pezzo e capisci che le prestazioni diventeranno un problema, puoi subito iniziare a preoccupartene. Di solito questo si traduce in cose semplici come 'non copiare i dati, gestisci i dati nel modo più semplice possibile, rendili più piccoli'. Nelle grandi riscritture, ci sono modi standard per migliorare le prestazioni. E ruotano quasi sempre attorno ai dati.

Modello di costo

Andrey: In uno dei podcast hai parlato dei modelli di costo nel contesto delle prestazioni. Puoi spiegare cosa intendevi?

Cliff: Certo. Sono nato in un'epoca in cui le prestazioni della CPU erano estremamente importanti. E quest'era sta tornando di nuovo – il destino non è privo di ironia. Ho cominciato a vivere ai tempi delle macchine a otto bit, il mio primo computer funzionava con 256 byte. Proprio byte. Tutto era molto piccolo. Bisognava contare le istruzioni e appena abbiamo iniziato a progredire verso linguaggi di programmazione più alti, i linguaggi si sono assunti sempre di più. C'era l'Assember, poi il Basic, poi il C, e il C si occupava di molte più dettagli, come la gestione dei registri e la scelta delle istruzioni. Ma lì tutto era abbastanza chiaro e se creavo un puntatore a un'istanza di variabile, avrei ottenuto un load, e il costo di quell'istruzione è noto. L'hardware fornisce un numero noto di cicli macchina, quindi la velocità di esecuzione di diverse operazioni può essere calcolata semplicemente sommando tutte le istruzioni che prevedi di eseguire. Ogni compare/test/branch/call/load/store poteva essere sommato e si poteva dire: ecco il tempo di esecuzione. Occupandosi del miglioramento delle prestazioni, noterai sicuramente quali numeri corrispondono ai piccoli cicli caldi. 
Ma non appena ti sposti su Java, Python e cose simili, ti allontani molto in fretta dall'hardware di basso livello. Qual è il costo della chiamata a un getter in Java? Se il JIT in HotSpot ha fatto tutto correttamente inlining, sarà un load, ma se non lo ha fatto – sarà una chiamata di funzione. Poiché la chiamata cade su un ciclo caldo, annullerà tutte le altre ottimizzazioni in quel ciclo. Pertanto, il costo reale sarà molto maggiore. E perdi immediatamente la capacità di guardare un pezzo di codice e capire se dovremmo eseguirlo in termini di frequenza di clock della CPU, memoria utilizzata e cache. Tutto ciò diventa interessante solo se ti immergi davvero nelle prestazioni.
Attualmente ci troviamo in una situazione in cui le velocità dei processori non aumentano quasi più da un decennio. I tempi antichi stanno tornando! Non puoi più contare su buone prestazioni in single-thread. Ma se improvvisamente ti dedichi ai calcoli paralleli – è incredibilmente difficile, tutti ti guardano come se fossi James Bond. I guadagni decuplicati di solito si verificano nei luoghi in cui qualcuno ha tralasciato qualcosa. La parallelità richiede molto lavoro. Per ottenere quel guadagno decuplicato, devi comprendere il modello di costo. Cosa e quanto costa. E per farlo, devi capire come il linguaggio si adatti all'hardware sottostante.
Martin Thompson ha scelto una parola fantastica per il suo blog Simpatia Meccanica! È fondamentale capire cosa sta per fare l'hardware, come lo farà e perché lo fa. Utilizzando queste informazioni, è piuttosto semplice iniziare a contare le istruzioni e scoprire dove va a finire il tempo di esecuzione. Se non hai una preparazione adeguata, stai semplicemente cercando un gatto nero in una stanza buia. Vedo continuamente persone che ottimizzano le prestazioni e non hanno la minima idea di che diavolo stiano facendo. Si affannano molto e non vanno quasi da nessuna parte. E quando prendo lo stesso pezzo di codice, ci metto un paio di piccoli hack e ottengo un guadagno di cinque o dieci volte, loro dicono: beh, non è giusto, sapevamo già che eri migliore. È straordinario. Di cosa stavo parlando… il modello di costo consiste nel sapere che tipo di codice scrivi e quanto velocemente funziona in media nel contesto generale.

Andrey: E come si fa a tenere in mente un tale volume? Si raggiunge attraverso molta esperienza, o no? Dove si acquisisce questa esperienza?

Cliff: Beh, la mia esperienza l'ho acquisita non nel modo più semplice. Ho programmato in Assembly quando era ancora possibile capire ogni singola istruzione. Può sembrare sciocco, ma da allora ho nella mia mente, nella mia memoria, un set di istruzioni Z80. Non ricordo i nomi delle persone un minuto dopo aver parlato con loro, ma ricordo il codice scritto 40 anni fa. È divertente, sembra un sindrome di "scienziato idiota».

Formazione su ottimizzazioni a basso livello

Andrey: C'è un modo più semplice per entrare nel settore?

Cliff: Sì e no. Il ferro di cui facciamo tutti uso non è cambiato molto nel tempo. Tutti usano x86, tranne gli smartphone basati su Arm. Se non ti occupi di qualche tipo di embedded hardcore, hai tutto lo stesso. Bene, andiamo avanti. Anche le istruzioni non sono cambiate da secoli. Devi andare a scrivere qualcosa in Assembly. Un po', ma abbastanza per iniziare a capire. Tu stai sorridendo, ma io parlo molto seriamente. Devi capire la corrispondenza tra il linguaggio e l'hardware. Dopo di che, devi andare, scrivere un po' e realizzare un piccolo compilatore giocattolo per un piccolo linguaggio giocattolo.
Da dove ottenere un modello di costo? Beh, si può leggere qualcosa, certo... Ma penso che il modo migliore sia agire. Creare un piccolo compilatore e questo sarà il miglior modo per comprendere il modello di costo e farlo proprio. Un piccolo compilatore, che possa essere utilizzato per programmare un forno a microonde – è un compito da principianti. Beh, intendo dire che se hai già delle abilità di programmazione, dovrebbero essere sufficienti. Tutte queste cose come analizzare una stringa, che sarà qualche espressione algebrica, estrarre da essa le istruzioni delle operazioni matematiche nell'ordine corretto, prendere i valori corretti dai registri – tutto questo si fa facilmente. E mentre lo fai, si imprime nella mente. Credo che tutti sappiano cosa faccia un compilatore. E questo darà una comprensione del modello di costo.

Esempi pratici di miglioramento delle prestazioni

Andrey: Su cosa dovremmo prestare attenzione quando lavoriamo sulle prestazioni?

Cliff: Strutture dati. A proposito, è da un po' che non tengo queste lezioni... Rocket School. È stato divertente, ma richiedeva così tanto impegno, e ho anche una vita da vivere! Va bene. Quindi, in una delle lezioni grandi e interessanti, "Dove va la tua performance", ho dato agli studenti un esempio: due e mezzo gigabyte di dati fintech letti da un file CSV e poi dovevamo calcolare il numero di prodotti venduti. Normali dati di mercato a ticker. Pacchetti UDP, trasformati in formato testuale, a partire dagli anni '70. Chicago Mercantile Exchange – tutte queste cose come petrolio, mais, fagioli di soia, e simili. Dovevamo contare questi prodotti, il numero di transazioni, il volume medio del movimento di fondi e beni, ecc. Questa è una matematica commerciale piuttosto semplice: trovare il codice del prodotto (questo è 1-2 caratteri in una tabella hash), ottenere la somma, aggiungerla a uno dei set di transazioni, aggiungere il volume, aggiungere il costo e un paio di altre cose. Matematica molto semplice. La realizzazione giocattolo era molto diretta: tutto si trova in un file, leggo il file e mi muovo attraverso di esso, separando le singole registrazioni in righe Java, cercando le cose necessarie e sommando secondo la matematica descritta sopra. E funziona con una certa velocità.

Con un approccio del genere, è ovvio cosa stia succedendo, e i calcoli paralleli non aiuteranno, giusto? Si scopre che un aumento delle prestazioni di cinque volte può essere ottenuto semplicemente scegliendo le strutture dati giuste. E questo sorprende anche i programmatori esperti! Nel mio caso specifico, il punto era che non si dovrebbero effettuare allocazioni di memoria nel ciclo caldo. Bene, non è tutta la verità, ma in generale – non dovresti allocare "una volta ogni X", quando X è sufficientemente grande. Quando X è due e mezzo gigabyte, non dovresti allocare nulla "una volta per lettera", o "una volta per riga", o "una volta per campo", nulla di simile. È proprio su questo che si sprecano tempo e risorse. Come funziona tutto ciò? Immagina che io stia effettuando una chiamata String.split() o BufferedReader.readLine(). Readline crea una stringa da un insieme di byte ricevuti in rete, una volta per ogni riga, per ciascuna delle centinaia di milioni di righe. Prendo questa stringa, la analizzo e la scarto. Perché scarto – beh, l'ho già elaborata, tutto qui. Quindi, per ogni byte letto da questi 2,7G, vengono scritti due caratteri nella stringa, ovvero già 5,4G, e non mi servono più, quindi vengono scartati. Se guardiamo alla larghezza di banda della memoria, stiamo caricando 2,7G, che passano attraverso la memoria e il bus di memoria nel processore, e poi il doppio viene inviato a una stringa in memoria, e tutto ciò viene rielaborato quando si crea ogni nuova stringa. Ma devo leggerla, l'hardware la legge, anche se successivamente tutto verrà rielaborato. E devo scriverla, perché ho creato una stringa e le cache sono piene – la cache non può contenere 2,7G. Pertanto, per ogni byte letto leggo altri due byte aggiuntivi e scrivo due byte aggiuntivi, e alla fine abbiamo un rapporto di 4:1 – in questo rapporto stiamo sprecando senza motivo la larghezza di banda della memoria. E poi scopriamo che se faccio String.split() – lo faccio lontano dall'essere l'ultima volta, ci possono essere altre 6-7 particolari all'interno. Pertanto, il codice classico di lettura CSV seguito da parsing delle righe porta a perdite di larghezza di banda della memoria nell'ordine di 14:1 rispetto a ciò che realmente desidereresti avere. Se si eliminano queste allocazioni, si può ottenere un'accelerazione di cinque volte.

E non è che sia molto difficile. Se guardi il codice da una prospettiva giusta, tutto diventa abbastanza semplice, non appena realizzi la sostanza del problema. Non bisogna smettere mai di allocare memoria: il problema è solo che stai allocando qualcosa e essa muore subito, bruciando nel processo una risorsa importante, che in questo caso è la larghezza di banda della memoria. E tutto ciò si traduce in un abbassamento delle prestazioni. Su x86 di solito è necessario accendere attivamente i cicli della CPU, mentre qui hai bruciato tutta la memoria molto prima. La soluzione è ridurre il numero di allocazioni. 
Un'altra parte del problema è che, se avvii un profiler quando la banda di memoria è esaurita, proprio nel momento in cui succede, di solito ti aspetti il ritorno dalla cache, perché è piena di spazzatura che hai appena generato, tutte quelle righe. Di conseguenza, ogni operazione di load o store diventa lenta, perché portano a Miss in cache: l'intera cache è diventata lenta, in attesa che la spazzatura se ne vada. Quindi il profiler mostrerà solo rumore casuale caldo, spalmato lungo tutto il ciclo: non ci sarà nessuna istruzione calda o posizione nel codice separata. Solo rumore. E se osservi i cicli GC, saranno tutti nella Young Generation e super veloci – microsecondi o millisecondi al massimo. Infatti, tutta questa memoria muore immediatamente. Allocando miliardi di gigabyte, lui li taglia, taglia e taglia di nuovo. Tutto ciò accade molto rapidamente. Quindi, ci sono cicli GC economici, rumore caldo lungo tutto il ciclo, ma vogliamo ottenere un incremento di 5 volte. In quel momento, qualcosa dovrebbe scattare nella tua testa e farti pensare: «perché?!». L'overflow della banda di memoria non viene visualizzato nel debugger classico, devi avviare il debugger dei contatori di prestazione hardware e vederlo da solo e direttamente. Altrimenti, puoi sospettarlo da questi tre sintomi. Il terzo sintomo è quando guardi cosa stai allocando, chiedi al profiler e lui risponde: «Hai creato un miliardo di righe, ma GC ha funzionato gratuitamente». Non appena accade questo, capisci che hai generato troppi oggetti e bruciato tutta la larghezza di banda della memoria. C'è un modo per capire ciò, ma non è ovvio. 

Il problema è nella struttura dei dati: una struttura nuda alla base di tutto ciò che accade, è troppo grande, occupa 2.7G su disco, quindi è molto indesiderabile fare una copia di questa cosa - si desidera caricarla direttamente dal buffer di byte di rete nei registri, per non dover leggere e scrivere nella stringa cinque volte. Sfortunatamente, Java di default non ti offre una libreria del genere all'interno del JDK. Ma in fondo è banale, vero? Fondamentalmente, si tratta di 5-10 righe di codice che serviranno a implementare il proprio caricatore di stringhe bufferizzato, che riproduce il comportamento della classe string, fungendo al contempo da involucro attorno al buffer di byte sottostante. Di conseguenza, si scopre che si lavora quasi come se si trattasse di stringhe, ma in realtà ci sono puntatori che si muovono verso il buffer, e i byte grezzi non vengono copiati da nessuna parte, e così si riutilizzano gli stessi buffer, volta dopo volta, e il sistema operativo è felice di occuparsi delle cose per cui è progettato, come la doppia bufferizzazione nascosta di questi buffer di byte, mentre tu stesso non macini più un flusso infinito di dati non necessari. A proposito, capite che quando si lavora con il GC si garantisce che ogni allocazione di memoria non sarà visibile al processore dopo l'ultimo ciclo di GC? Quindi, tutto questo non può proprio essere nella cache, e ciò che succede dopo è un errore garantito al 100%. Quando si lavora con un puntatore, su x86 leggere un registro dalla memoria richiede 1-2 cicli, e non appena ciò avviene, paghi, paghi, paghi, perché tutta la memoria è NINE cache – e questa è la vera costo dell'allocazione della memoria. Il vero costo.

In altre parole, le strutture dati sono le cose più difficili da cambiare. E una volta che ti sei reso conto di aver scelto la struttura dati sbagliata, che in seguito comprometterà le prestazioni, di solito richiede un notevole lavoro per correggerla. Se non lo fai, la situazione peggiorerà. Prima di tutto, è importante considerare le strutture dati. Il vero costo qui è associato alle strutture dati pesanti, che tendi a usare nel modo "ho copiato la struttura dati X nella struttura dati Y, perché Y mi piace di più esteticamente". Ma l'operazione di copia (che sembra economica) consuma in realtà banda di memoria e qui si annida tutto il tempo di esecuzione perso. Se ho una gigantesca stringa JSON e voglio trasformarla in un albero DOM strutturato di POJO o qualcosa del genere, l'operazione di parsing di questa stringa e la costruzione di POJO, seguita da ulteriori accessi a POJO, comporterà un costo aggiuntivo, che non è economico. A meno che tu non acceda a POJO molto più frequentemente che alla stringa. In alternativa, si potrebbe cercare di decodificare la stringa ed estrarre solo quello che serve, senza convertirlo in POJO. Se tutto questo avviene in un contesto che richiede prestazioni massime, niente POJO: devi in qualche modo lavorare direttamente sulla stringa.

Perché creare un proprio linguaggio di programmazione

Andrey: Hai detto che per comprendere il modello di costo, è necessario scrivere un piccolo linguaggio…

Cliff: Non un linguaggio, ma un compilatore. Linguaggio e compilatore sono due cose diverse. La differenza principale è nella tua testa. 

Andrey: A proposito, so che stai sperimentando la creazione di linguaggi propri. Perché?

Cliff: Perché posso! Sono in parte in pensione, quindi questo è il mio hobby. Ho trascorso tutta la vita a realizzare lingue di qualcun altro. Inoltre, ho lavorato molto sullo stile di codifica. E anche perché vedo problemi in altre lingue. Vedo che ci sono modi migliori per fare cose familiari. E ne approfitterei. Sono semplicemente stanco di vedere problemi in me stesso, in Java, in Python, in qualsiasi altra lingua. Attualmente scrivo in React Native, JavaScript ed Elm come hobby, che non riguarda la pensione, ma un lavoro attivo. E scrivo anche in Python e probabilmente continuerò a lavorare sull'apprendimento automatico per i backend Java. Ci sono molte lingue popolari e ognuna ha caratteristiche interessanti. Ognuna è buona per qualcosa di proprio e si può cercare di unire tutte queste caratteristiche. Quindi, sto studiando cose che mi interessano, il comportamento del linguaggio, cerco di inventare una semantica ragionevole. E finora ci riesco! Al momento sto lottando con la semantica della memoria, perché voglio averla come in C e Java, e ottenere un forte modello di memoria e semantica della memoria per i caricamenti e gli archiviazioni. Inoltre, avere un'inferenza automatica dei tipi come in Haskell. Sto cercando di mescolare l'inferenza dei tipi simile a Haskell con una memoria che funzioni come in C e Java. A questo mi dedico da 2-3 mesi, per esempio.

Andrey: Se stai costruendo un linguaggio che prende i migliori aspetti da altre lingue, hai mai pensato che qualcuno potrebbe fare il contrario: prendere le tue idee e usarle per le proprie?

Cliff: È proprio così che nascono nuovi linguaggi! Perché Java è simile a C? Perché C aveva una buona sintassi, che tutti capivano, e Java è stata ispirata da quella sintassi, aggiungendo sicurezza dei tipi, controllo dei limiti degli array, GC, e migliorando alcune cose di C. Hanno aggiunto anche le loro. Ma si sono ispirati piuttosto fortemente, giusto? Tutti stanno sulle spalle di giganti che sono venuti prima di te: è così che avviene il progresso.

Andrey: Da quanto ho capito, il tuo linguaggio sarà sicuro per quanto riguarda l'uso della memoria. Hai pensato di implementare qualcosa come il borrow checker di Rust? Lo hai esaminato, cosa ne pensi?

Cliff: Bene, scrivo in C da un'eternità, con tutte queste malloc e free, e gestisco manualmente il ciclo di vita. Sapete, il 90-95% del ciclo di vita gestito manualmente ha una struttura simile. E è davvero, davvero doloroso occuparsi di questo manualmente. Mi piacerebbe che il compilatore semplicemente dicesse cosa sta accadendo e cosa ho ottenuto con le mie azioni. Per alcune cose, il borrow checker lo fa automaticamente. E dovrebbe anche fornire informazioni in modo automatico, comprendere tutto e non caricarmi con il dover esprimere questa comprensione. Dovrebbe fare almeno un'analisi di escape locale, e solo se non ci riesce, allora bisogna aggiungere annotazioni sui tipi che descriveranno il ciclo di vita – e uno schema simile è molto più complesso del borrow checker, o di qualsiasi altro checker di memoria esistente. La scelta tra 'tutto a posto' e 'non ho capito nulla' – no, dovrebbe esserci qualcosa di migliore. 
Quindi, come persona che ha scritto molto codice in C, ritengo che avere supporto per la gestione automatica del lifetime sia una cosa estremamente importante. E sono anche stanco di quanto Java utilizzi memoria, con la principale lamentela riguardo al GC. Quando si allocca memoria in Java, non ti restituisce la memoria che era locale all'ultimo ciclo GC. Nei linguaggi con una gestione della memoria più precisa, questo non accade. Se chiami malloc, ottieni immediatamente memoria che di solito è stata appena utilizzata. Di solito fai delle operazioni temporanee con la memoria e la restituisci subito. E torna immediatamente nel pool di malloc, e il ciclo successivo di malloc la riutilizza di nuovo. Pertanto, il reale utilizzo della memoria si riduce a un insieme di oggetti vivi in un determinato momento, più eventuali perdite. E se non hai perdite in modo del tutto inappropriato, la maggior parte della memoria si accumula nella cache e nel processore, e questo funziona rapidamente. Ma richiede una gestione manuale significativa della memoria tramite malloc e free, chiamati nel giusto ordine e nel posto giusto. Rust può gestire tutto ciò in modo corretto e, in molti casi, può offrire anche migliori prestazioni, poiché il consumo di memoria è ridotto solo ai calcoli attuali, a differenza dell'attesa del successivo ciclo GC, che libererà memoria. Alla fine, abbiamo trovato un modo molto interessante per migliorare le prestazioni. E piuttosto potente - nel senso che mi sono occupato di argomenti simili nella gestione dei dati per il fintech, e questo portava a un incremento delle prestazioni di circa cinque volte. È un aumento piuttosto significativo, specialmente in un mondo in cui i processori non diventano più veloci e continuiamo a aspettare miglioramenti.

Carriera di ingegnere delle prestazioni

Andrey: Vorrei anche chiederti della carriera in generale. Sei diventato famoso grazie al lavoro sul JIT in HotSpot, poi sei passato ad Azul, che è anch'essa un'azienda JVM. Ma ti sei occupato di più di hardware che di software. E poi all'improvviso ti sei concentrato su Big Data e Machine Learning, per poi passare alla rilevazione delle frodi. Come ci sei arrivato? Sono aree di sviluppo molto diverse.

Cliff: Ho iniziato a programmare ormai da molto tempo e ho avuto esperienze in attività molto diverse. E quando la gente dice: «Oh, sei tu quello che ha fatto il JIT per Java!», è sempre divertente. Eppure prima di questo mi occupavo di un clone di PostScript, quel linguaggio che Apple una volta usava per le sue stampanti laser. E prima ancora avevo realizzato un'implementazione del linguaggio Forth. Penso che il tema centrale per me sia lo sviluppo di strumenti. Per tutta la vita ho creato strumenti, attraverso i quali altre persone scrivono i loro programmi incredibili. Ma mi sono anche occupato dello sviluppo di sistemi operativi, driver, debugger a livello di kernel, linguaggi per lo sviluppo di sistemi operativi, che iniziavano in modo triviale, ma che nel tempo diventavano sempre più complessi. Tuttavia, il tema principale rimane comunque lo sviluppo di strumenti. Una grande parte della mia vita è passata tra Azul e Sun, ed è stata dedicata a Java. Ma quando mi sono occupato di Big Data e Machine Learning, ho indossato di nuovo il mio cappello da cerimonia e ho detto: «Oh, ora abbiamo un problema non banale, e qui stanno succedendo molte cose interessanti e ci sono persone che stanno facendo qualcosa». Questo è un ottimo percorso di sviluppo da seguire.

Sì, mi piacciono molto i calcoli distribuiti. Il mio primo lavoro è stato all'università in C, su un progetto pubblicitario. Si trattava di calcoli distribuiti su chip Zilog Z80, che raccoglievano dati per il riconoscimento testuale ottico analogico, effettuato da un vero analizzatore analogico. Era un tema interessante e completamente fuori dal comune. Ma c'erano problemi, una parte non veniva riconosciuta correttamente, quindi dovevamo estrarre l'immagine e mostrarla a una persona che l'aveva già letta a occhio e comunicava cosa si diceva, e per questo c'erano lavori con dati, e questi lavori avevano il loro linguaggio. C'era un backend che gestiva tutto questo: Z80 in esecuzione in parallelo con terminali vt100 attivi – uno per persona, e c'era un modello di programmazione parallela su Z80. Un certo pezzo di memoria condivisa che veniva condiviso da tutti gli Z80 all'interno di una configurazione a stella; veniva condiviso anche il backplane, metà della RAM era condivisa all'interno della rete, e l'altra metà era privata o utilizzata per qualcos'altro. Un sistema distribuito parallelo semplificato con memoria condivisa... semi-condivisa. Quando è stato... Già non ricordo, da qualche parte a metà degli anni '80. Davvero tanto tempo fa. 
Sì, direi che 30 anni è abbastanza tempo. I compiti legati ai calcoli distribuiti esistono da molto tempo, le persone da tempo combattono con Beowulf-cluster. Tali cluster sembrano come… Per esempio: c'è Ethernet e il tuo veloce x86 è collegato a questo Ethernet, e ora vuoi ottenere una fake shared memory, perché nessuno poteva allora dedicarsi alla codifica di calcoli distribuiti, era troppo complicato e quindi c'era una fake shared memory con protezione delle pagine di memoria su x86, e se scrivevi in questa pagina, dicevamo agli altri processori che se avessero avuto accesso alla stessa shared memory, dovevano scaricarla da te. In questo modo, è emerso qualcosa di simile a un protocollo di supporto alla coerenza della cache e software per questo. Concetto interessante. Il vero problema, ovviamente, era altro. Tutto ciò funzionava, ma rapidamente ti trovavi a dover affrontare problemi di prestazioni, poiché nessuno comprendeva il modello di prestazioni a un livello sufficientemente buono: quali fossero i pattern di accesso alla memoria, come fare in modo che i nodi non si pingassero continuamente l'un l'altro, e così via.

In H2O ho concepito quanto segue: gli sviluppatori stessi sono responsabili di identificare dove si nasconde il parallelismo e dove non c'è. Ho ideato un modello di codifica che rende facile e semplice scrivere codice ad alte prestazioni. Invece, scrivere codice lento è difficile, risulterà sgradevole. È necessario impegnarsi seriamente per scrivere codice lento, occorrerà utilizzare metodi non standard. Il codice che rallenta è evidente a prima vista. Di conseguenza, di solito si scrive codice che funziona velocemente, ma ci si deve chiedere che fare nel caso di memoria condivisa. Tutto ciò è legato a grandi array e il comportamento lì è simile a grandi array non volatili in parallelo in Java. Cioè, immaginate che due thread scrivano in un array parallelo, uno di essi vince, mentre l'altro, di conseguenza, perde, e non sapete quale sia quale. Se non sono volatili, l'ordine può essere qualsiasi – e funziona davvero bene. Le persone si preoccupano dell'ordine delle operazioni, posizionano correttamente volatile e nei luoghi giusti si aspettano problemi di prestazioni legati alla memoria. Altrimenti scriverebbero semplicemente codice sotto forma di cicli da 1 a N, dove N è qualche trilione, sperando che tutti i casi complessi diventino automaticamente paralleli – e lì non funziona. Ma in H2O non è Java e non è neppure Scala, si potrebbe considerare come "Java meno meno", se si vuole. È uno stile di programmazione molto chiaro e sembra scrivere codice semplice in C o Java con cicli e array. Ma da questo punto di vista, la memoria può essere gestita in terabyte. Uso ancora H2O. Di tanto in tanto lo uso in vari progetti – ed è ancora la cosa più veloce, di decine di volte superiore ai concorrenti. Se stai lavorando con Big Data su dati a colonne, è molto difficile superare H2O.

Sfide tecniche

Andrey: Qual è stata la sfida più grande della tua carriera?

Cliff: Stiamo discutendo della parte tecnica o di quella non tecnica della questione? Direi che le sfide più grandi non sono tecniche. 
Per quanto riguarda le sfide tecniche, le ho semplicemente superate. Non so nemmeno quale sia stata la più grande, ma ci sono state diverse sfide piuttosto interessanti che hanno richiesto molto tempo e impegno mentale. Quando sono andato in Sun, ero sicuro di poter creare un compilatore veloce, ma molti senior mi dicevano che non sarei mai riuscito a farlo. Ma ho continuato su questa strada, ho scritto un compilatore fino all'allocatore di registri, ed è stato piuttosto veloce. Era tanto veloce quanto il moderno C1, ma all'epoca l'allocatore era molto più lento, e guardando indietro, questo era un problema della grande struttura dati. Avevo bisogno di essa per scrivere un allocatore di registri grafico e non capivo il dilemma tra l'espressività del codice e la velocità, che esisteva in quell'epoca ed era molto importante. Si è rivelato che la struttura dati di solito superava le dimensioni della cache sugli x86 dell'epoca e quindi, se inizialmente pensavo che l'allocatore di registri avrebbe occupato il 5-10% del tempo totale di JIT, in realtà si è tradotto in un 50%.

Il tempo passava, il compilatore diventava sempre più chiaro e performante, smetteva di generare codice orrido in un numero maggiore di casi, e le prestazioni cominciavano a somigliare a quelle di un compilatore C. Se, ovviamente, non scrivi qualcosa di brutto che nemmeno il C riesce ad accelerare. Se scrivi codice come in C, ottieni prestazioni simili a quelle del C in un numero maggiore di casi. E man mano che andavamo avanti, il codice cominciava ad coincidere asintoticamente con il livello C, l'allocatore di registri cominciò a somigliare a qualcosa di finale... a prescindere che il tuo codice funzionasse velocemente o lentamente. Continuai a lavorare sull'allocatore, affinché producesse assegnazioni migliori. Diventava sempre più lento, ma offriva prestazioni sempre migliori nei casi in cui nessun altro riusciva. Potevo immergermi nell'allocatore di registri, spendere un mese di lavoro, e all'improvviso tutto il codice cominciava a girare il 5% più velocemente. Questo accadeva ripetutamente e l'allocatore di registri divenne una sorta di opera d'arte – tutti lo amavano o lo odiavano, e le persone dell'accademia facevano domande su "perché tutto fosse fatto proprio in quel modo", perché non scansione lineare, e qual è la differenza. La risposta è sempre la stessa: un allocatore basato sulla colorazione del grafo più un lavoro molto accurato con il codice del buffer è uguale a uno strumento di vittoria, la migliore combinazione che nessuno può superare. E questa è una cosa abbastanza non ovvia. Tutto il resto che fa il compilatore sono cose piuttosto consolidate, anche se anch'esse portate a un livello artistico. Ho sempre fatto cose che avrebbero dovuto trasformare il compilatore in un'opera d'arte. Ma nulla di tutto ciò era straordinario – tranne l'allocatore di registri. Il punto è che bisogna lavorare con cautela tagliare sotto carico e, se ciò avviene (posso spiegare in dettaglio se è interessante), significa che si può effettuare l'inlining in modo più aggressivo, senza il rischio di oltrepassare il punto di rottura della prestazione. All'epoca c'erano un sacco di compilatori completi, zeppi di gadget e fronzoli, che avevano allocatori di registri, ma nessuno riuscì mai a farlo di nuovo.

Il problema è che, se aggiungi metodi soggetti a inlining, aumentando sempre di più l'area di inlining, il set di valori utilizzati supera istantaneamente il numero di registri, e sei costretto a spillare. Il livello critico di solito si verifica quando l'allocatore cede, e un buon candidato per lo spillage vale l'altro; inizi a spillare cose davvero strane. Il valore dell'inlining qui è che perdi parte dell'overhead, l'overhead della chiamata e del salvataggio, puoi vedere i valori all'interno e puoi ottimizzarli ulteriormente. Il costo dell'inlining è che si genera un gran numero di valori vivi, e se il tuo allocatore di registri spillera più di quanto necessario, perdi immediatamente. Per questo motivo, la maggior parte degli allocatori ha un problema: quando l'inlining oltrepassa una certa soglia, inizia a spillare tutto ed è possibile che le prestazioni vengano spazzate via. Coloro che implementano il compilatore aggiungono alcune euristiche: ad esempio, per fermare l'inlining, partendo da una dimensione sufficientemente grande, poiché le allocazioni rovineranno tutto. Così si crea una frattura nel grafico delle prestazioni – fai inlining, fai inlining, le prestazioni crescono lentamente – e poi bam! – crolla a picco, perché hai inlinato troppo. Così è andata fino all'arrivo di Java. Java richiede molto più inlining, quindi ho dovuto rendere il mio allocatore molto più aggressivo, affinché si stabilizzasse e non crollasse, e se hai inlinato troppo – inizia a spillare, ma poi c'è comunque un momento in cui

Qualche nota sull'allocazione dei registri e sulla multithreading

Vladimir: Problemi come l'allocazione dei registri sembrano essere un tema infinito e senza fine. È interessante sapere se ci sia stata un'idea che sembrava promettente e poi è fallita nella pratica?

Cliff: Certo! L'allocazione dei registri è un'area in cui, per risolvere un problema NP-completo, cerchi di trovare delle euristiche. E non riuscirai mai a trovare una soluzione perfetta, giusto? È semplicemente impossibile. Vedi, la compilazione Ahead of Time funziona male anch'essa. Qui si parla di casi medi. Della prestazione tipica, quindi puoi andare a misurare qualcosa che consideri una buona prestazione tipica – dopotutto, stai lavorando per migliorarlo! L'allocazione dei registri è un argomento interamente dedicato alla prestazione. Una volta che hai il primo prototipo, che funziona e fa quello che deve, inizia il lavoro sulla prestazione. Devi imparare a misurare bene. Perché è importante? Se hai dati chiari, puoi guardare in diverse aree e vedere: ah, questo ha aiutato qui, ma là è tutto andato male! Nascono delle buone idee, aggiungi una nuova euristica e all'improvviso tutto inizia a funzionare leggermente meglio in media. O magari non inizia. Ho avuto un sacco di casi in cui abbiamo combattuto per cinque percento di prestazione, che distingueva il nostro sviluppo dal precedente allocatore. E ogni volta si presentava così: hai vinto in un punto, hai perso in un altro. Se hai buoni strumenti di analisi delle prestazioni, puoi identificare le idee perdenti e capire perché stanno perdendo. Forse vale la pena mantenere tutto com'è, oppure affrontare seriamente il tuning fine, o andare a sistemare qualcos'altro. È un intero insieme di cose! Ho realizzato questo fantastico hack, ma serve anche questo, e questo, e quest'altro – e la loro combinazione complessiva porta a dei miglioramenti. E le soluzioni singole possono fallire. Questa è la natura del lavoro su problemi NP-completi.

Vladimir: Sembra che cose come la verniciatura negli allocatori siano una questione già risolta. Beh, per voi sembra risolta, a giudicare da quello che raccontate, quindi vale comunque la pena…

Cliff: Non è risolta in quanto tale. Devi trasformarla in "risolta". Ci sono problemi difficili e devono essere affrontati. Una volta che ciò è fatto, arriva il momento di lavorare sulle prestazioni. Questa attività deve essere presa sul serio: fare benchmark, raccogliere metriche, spiegare situazioni in cui, tornando a una versione precedente, il tuo vecchio hack ha ricominciato a funzionare (o viceversa, ha smesso di funzionare). E non retrocedere finché non ottieni qualcosa. Come ho già detto, se ci sono idee geniali che non hanno funzionato, nel campo dell'allocazione dei registri, le idee sono praticamente infinite. Ad esempio, puoi leggere pubblicazioni scientifiche. Anche se ora questo campo si sta muovendo molto più lentamente ed è diventato più chiaro rispetto ai suoi tempi di gioventù. Tuttavia, in questo campo lavora un'infinità di persone e tutte le loro idee meritano di essere provate, tutte aspettano il loro momento. E non puoi dire quanto siano buone se non provi. Quanto bene si integrano con tutto il resto nel tuo allocatore, visto che l'allocatore fa molte cose, e alcune idee nel tuo specifico allocatore potrebbero non funzionare, mentre in un altro allocatore funzionerebbero benissimo. Il modo principale per vincere per un allocatore è rimuovere le parti lente dal percorso principale e forzare la divisione lungo i confini dei percorsi lenti. Quindi, se vuoi avviare la GC, seguire il percorso lento, deottimizzarti, generare un'eccezione, tutto in quel modo – sai che questi eventi sono relativamente rari. E lo sono davvero, ho verificato. Fai lavoro extra e grazie a questo svaniscono molte limitazioni su quei percorsi lenti, ma non è molto importante perché sono lenti e raramente usati. Ad esempio, il puntatore nullo – non si verifica mai, giusto? Devi avere diversi percorsi per diverse cose, ma non devono interferire tra loro nel principale. 

Vladimir: Cosa pensate della multicoresenza quando ci sono migliaia di core? È una cosa utile?

Cliff: Il successo delle GPU dimostra che è piuttosto utile!

Vladimir: Sono abbastanza specializzati. E per quanto riguarda i processori a uso generale?

Cliff: Bene, questo era il modello di business di Azul. La risposta è arrivata ancora nell'era in cui le persone amavano molto le prestazioni prevedibili. All'epoca era difficile scrivere codice parallelo. Il modello di codifica H2O si scala bene, ma non è un modello di uso generale. A meno che non sia leggermente più generale rispetto all'uso della GPU. Parliamo della complessità nello sviluppo di una cosa del genere o della complessità del suo utilizzo? Ad esempio, Una lezione interessante che mi ha insegnato Azul, piuttosto poco ovvia: le piccole cache vanno bene. 

La sfida più grande della vita

Vladimir: E per quanto riguarda le sfide non tecniche?

Cliff: La sfida più grande è stata non essere... gentile e buono con le persone. Di conseguenza, mi sono costantemente trovato in situazioni estremamente conflittuali. Situazioni in cui sapevo che tutto stava andando storto, ma non sapevo come andare avanti nella soluzione di questi problemi e non sono riuscito a gestirli. Molti problemi di lunga durata, che durano da decenni, sono emersi in questo modo. Il fatto che in Java esistano i compilatori C1 e C2 è una diretta conseguenza di questo. Il fatto che in Java per dieci anni non ci sia stata la compilazione multilivello è anch'esso una diretta conseguenza. È ovvio che avevamo bisogno di un sistema del genere, ma non è ovvio perché non ci fosse. Ho avuto problemi con un ingegnere... o un gruppo di ingegneri. Tanto tempo fa, quando ho iniziato a lavorare in Sun, ero... Va bene, non solo allora, ho sempre un'opinione su tutto. E credevo fosse vero che potessi semplicemente prendere questa mia verità e dirla in faccia. Soprattutto, dato che avevo ragione in modo scioccante la maggior parte del tempo. E se a qualcuno questo approccio non piace... specialmente se è chiaramente in errore e sta combinando guai... In generale, poche persone potevano tollerare tale forma di comunicazione. Anche se alcuni lo potevano, per esempio, io. Ho basato tutta la mia vita su principi meritocratici. Se mi mostri qualcosa di sbagliato, giro i tacchi e dico: hai detto una sciocchezza. Naturalmente, mi scuso e tutto il resto, riconosco i meriti, se ci sono, e compio altre azioni corrette. D'altra parte, ho ragione in modo scioccante una percentuale sorprendentemente alta del tempo. E questo non funziona molto bene nelle relazioni con le persone. Non mi sforzo di essere gentile, ma metto il problema sul tavolo. "Questo non funzionerà mai, perché una, due e tre". E loro: "Oh!". Ci sono stati anche altri effetti, che è meglio probabilmente saltare: per esempio, quelli che hanno portato al divorzio da mia moglie e a un decennio di depressione dopo.

La sfida è una lotta con le persone, con la loro percezione di ciò che puoi o non puoi fare, cosa è importante e cosa no. Ci sono state molte sfide riguardo allo stile di codifica. Continuo a scrivere molto codice e in quei tempi dovetti addirittura rallentare perché facevo troppe attività parallele e le facevo male, invece di concentrarmi su una sola. Ripensando, ho scritto metà del codice del team Java JIT, del team C2. Il prossimo programmatore più veloce scriveva la metà più lentamente, il successivo era ancora più lento ed è stata una caduta esponenziale. Il settimo in questa fila era molto, molto lento - è così che va sempre! Ho toccato un sacco di codice. Ho osservato chi scrive cosa, senza eccezioni, ho fissato il loro codice, ho riveduto ognuno di loro e continuavo a scrivere più di qualsiasi di loro. Con le persone, questo approccio non funziona molto bene. Alcuni non lo apprezzano. E quando non riescono a gestirlo, iniziano le lamentele di ogni genere. Per esempio, una volta mi è stato detto di smettere di scrivere codice perché ne scrivo troppo e questo mette in pericolo il team, e tutto questo mi è sembrato una battuta: amico, se tutto il resto del team sparisse e io continuassi a scrivere codice, perderesti solo metà del team. D'altra parte, se continuo a scrivere codice e tu perdi metà del team - questo suona come unvery bad management. Non ci ho mai riflettuto troppo, non ne ho mai parlato, ma in qualche modo era sempre nella mia testa. Nelle retrovie della mia coscienza continuava a girare il pensiero: 'Ma state scherzando?'. Quindi, il problema più grande ero io e le mie relazioni con le persone. Ora mi capisco molto meglio, ho fatto da team leader con i programmatori e ora parlo direttamente alla gente: sai, io sono quello che sono e dovrete fare i conti con me - va bene se rimango qui? E quando hanno iniziato a gestirlo, tutto ha funzionato. Non sono realmente né cattivo né buono, non ho cattive intenzioni o obiettivi egoistici, è semplicemente la mia essenza e bisogna convivere con essa.

Andrey: Recentemente si è parlato moltissimo di autoconsapevolezza per gli introversi, e in generale delle soft skills. Cosa si può dire al riguardo?

Cliff: Sì, è stata una comprensione e una lezione che ho appreso dal mio divorzio con mia moglie. Ciò che ho tratto dal divorzio è stata la comprensione di me stesso. Così ho iniziato a comprendere le altre persone. Comprendere come funziona questa interazione. Ha portato a scoperte una dopo l'altra. È emersa la consapevolezza di chi sono e cosa rappresento. Cosa faccio: o sono preoccupato per il compito, o evito il conflitto, o qualcos'altro – e tale livello di autoconsapevolezza aiuta davvero a mantenere il controllo. Dopo ciò, tutto procede molto più facilmente. Una cosa che ho scoperto non solo in me stesso, ma anche in altri programmatori, è l'impossibilità di verbalizzare i pensieri quando ci si trova in uno stato di stress emotivo. Ad esempio, sei lì a scrivere codice, sei in uno stato di flusso, e poi qualcuno corre da te e inizia a urlare in preda alla disperazione che qualcosa è andato storto e verranno adottate misure drastiche. E non riesci nemmeno a dire una parola perché sei in uno stato di stress emotivo. Le conoscenze acquisite consentono di prepararsi a quel momento, di viverlo e di passare a un piano di ritirata, dopo il quale si può già fare qualcosa. Quindi sì, quando inizi a renderti conto di come funziona tutto questo – è un evento enorme che cambia la vita. 
Io stesso non sono riuscito a trovare le parole giuste, ma ho ricordato la sequenza di azioni. Il punto è che questa reazione è tanto fisica quanto verbale, e hai bisogno di spazio. Uno spazio di questo tipo, in un senso zen. È proprio questo che bisogna spiegare, e poi subito allontanarsi – fisicamente allontanarsi. Quando tacito a parole, posso elaborare la situazione in termini di emozioni. Man mano che l'adrenalina raggiunge il cervello, ti mette in modalità 'combatti o fuggi', non riesci più a dire nulla, no – ora sei un idiota, un ingegnere da colpire, incapace di dare una risposta dignitosa o di almeno fermare l'attacco, e l'aggressore può attaccare liberamente ancora e ancora. Prima di tutto, è necessario tornare a essere se stessi, riprendere il controllo, uscire dalla modalità 'combatti o fuggi'.

Ecco perché è necessario uno spazio verbale. Uno spazio libero. Se si deve parlare, si può dichiarare questo e poi andare a cercare realmente il proprio «spazio»: fare una passeggiata nel parco, chiudersi in doccia – non importa. La cosa importante è disconnettersi temporaneamente dalla situazione. Non appena riesci a disconnetterti anche solo per qualche secondo, il controllo torna, inizi a pensare in modo lucido. «Bene, non sono un idiota, non faccio cose stupide, sono una persona piuttosto utile». Non appena sei riuscito a convincere te stesso, è tempo di passare alla fase successiva: capire cosa sia successo. Sei stato attaccato, l'attacco è venuto da dove non te lo aspettavi, è stata un'imboscata sleale e subdola. È male. Il passo successivo è capire perché l'attaccante avesse bisogno di farlo. Davvero, perché? Forse perché è lui stesso in preda alla rabbia? Perché è in preda alla rabbia? Ad esempio, perché ha fallito e non riesce ad assumersi la responsabilità? In questo modo bisogna elaborare la situazione con attenzione. Ma per questo serve uno spazio di manovra, uno spazio verbale. Il primo passo è rompere il contatto verbale. Allontanarsi dalla discussione a parole. Cancellalo, allontanati il prima possibile. Se si tratta di una conversazione telefonica, riattacca semplicemente – è un'abilità che ho acquisito dalla mia comunicazione con l'ex moglie. Se la conversazione non porta a nulla di buono, dì semplicemente «arrivederci» e riattacca. Dall'altra parte del telefono: «bla-bla-bla», tu rispondi: «ah, ciao!» e riattacchi. Rompi semplicemente la conversazione. Cinque minuti dopo, quando torna a te la capacità di pensare in modo lucido, ti sei un po' raffreddato, diventa possibile riflettere su cosa sia realmente successo e su cosa accadrà dopo. E inizia a formulare una risposta ragionata, piuttosto che reagire semplicemente sulle emozioni. Per me, una svolta nella consapevolezza di sé è stata proprio quella che, in caso di stress emotivo, non riesco a parlare. Uscire da questo stato, riflettere e pianificare come rispondere e compensare i problemi – sono questi i passi giusti quando non puoi parlare. Il modo più semplice è scappare dalla situazione in cui si manifesta lo stress emotivo e semplicemente smettere di partecipare a questo stress. Dopo di che, riconquisti la capacità di pensare, quando puoi pensare, si presenta l'opportunità di parlare e così via.

A proposito, in tribunale l'avvocato della parte avversa cerca di fare questo con te - ora è chiaro perché. Perché ha la possibilità di schiacciarti fino a un tale stato che non potrai nemmeno pronunciare il tuo nome, per esempio. Nel senso più diretto, non potrai parlare. Se ti capita di vivere questo e sai che ti troverai in un luogo dove ci sono battaglie verbali, in un luogo come il tribunale, puoi arrivare con il tuo legale. L'avvocato ti difenderà e fermerà l'attacco verbale, e lo farà in modo del tutto legale, e ti restituirà lo spazio zen perduto. Per esempio, ho dovuto chiamare la mia famiglia un paio di volte, il giudice è stato abbastanza cordiale, ma l'avvocato della parte avversa urlava e urlava contro di me, non riuscivo nemmeno a inserire una parola. In questi casi, per me funziona meglio utilizzare un mediatore. Il mediatore ferma tutta questa pressione che ti travolge in un flusso incessante, scopri lo spazio zen necessario, insieme ad esso ritorna la capacità di parlare. È un intero campo di conoscenze, in cui è necessario studiare molto, scoprire molte cose dentro di sé, e tutto questo si trasforma in soluzioni strategiche di alto livello, diverse per persone diverse. Alcuni non hanno problemi descritti sopra, di solito, non li hanno le persone che lavorano professionalmente nelle vendite. Tutte queste persone che guadagnano da vivere con le parole - cantanti, poeti, leader religiosi e politici noti, loro hanno sempre qualcosa da dire. Non hanno questi problemi, mentre io sì.

Andrey: È stato... inaspettato. Ottimo, abbiamo già parlato molto e ora è tempo di concludere questa intervista. Ci incontreremo sicuramente alla conferenza e potremo continuare questo dialogo. Ci vediamo su Hydra!

Continua la comunicazione con Cliff alla conferenza Hydra 2019, che si terrà l'11-12 luglio 2019 a San Pietroburgo. Arriverà con una presentazione «L'esperienza della memoria transazionale hardware Azul». I biglietti possono essere acquistati sul sito ufficiale.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster