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

Una lunga intervista con Cliff Click, il padre della compilazione JIT in JavaCliff Click — CTO dell'azienda Cratus (sensori IoT per migliorare i processi), fondatore e co-fondatore di diverse startup (inclusi Rocket Realtime School, Neurensic e H2O.ai), con vari exit di successo. Cliff ha scritto il suo primo compilatore a 15 anni (Pascal per TRS Z-80)! È noto soprattutto per il suo lavoro su C2 in Java (il Sea of Nodes IR). Questo compilatore ha dimostrato al mondo che JIT può produrre codice di alta qualità, un fattore chiave per l'emergere di Java come 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 puro Java, che supportava pause GC su un heap da 500 gigabyte in 10 millisecondi. In generale, Cliff ha lavorato su tutti gli aspetti della JVM.

 
Questo hubrapost è una lunga intervista con Cliff. Parleremo dei seguenti argomenti:

  • Passaggio alle ottimizzazioni di basso livello
  • Come effettuare un grande refactoring
  • Modello di costo
  • Apprendimento delle ottimizzazioni di basso livello
  • Esempi pratici di miglioramento delle prestazioni
  • Perché creare un proprio linguaggio di programmazione
  • Carriera di ingegnere delle prestazioni
  • Sfide tecniche
  • Un po' su allocazione dei registri e multithreading
  • La sfida più grande della vita

Intervista condotta da:

  • Andrey Satari 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 cloud presso Kaspersky Lab, un videogioco multiplayer in Mail.ru e un servizio di calcolo dei tassi di cambio in Deutsche Bank. È interessato al testing di sistemi backend e distribuiti su larga scala.
  • Vladimir Sitnikov di Netcracker. Lavora da dieci anni sulle prestazioni e la scalabilità di NetCracker OS — il software utilizzato dagli operatori di telecomunicazioni per automatizzare i processi di gestione della rete e delle apparecchiature di rete. È appassionato di prestazioni in Java e Oracle Database. Autore di oltre una dozzina di miglioramenti delle prestazioni nel driver JDBC ufficiale di PostgreSQL.

Passaggio alle ottimizzazioni di basso livello

Andrey: Sei una persona conosciuta nel mondo della compilazione JIT, in Java e nel lavoro sulle prestazioni in generale, giusto? 

Cliff: Esatto!

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

Cliff: Qui tutto è semplice. Il codice più veloce è quello che non viene mai eseguito. Pertanto, è sempre necessario partire da un alto livello, lavorando sugli algoritmi. Una notazione O migliore batterà una notazione O peggiore, a meno che non entrino in gioco costanti abbastanza grandi. Le cose a basso livello arrivano per ultime. Di solito, se hai ottimizzato adeguatamente il resto dello stack e c'è ancora qualcosa di interessante, allora si tratta di basso livello. Ma come si inizia da un alto livello? Come si fa a sapere se hai fatto abbastanza lavoro a un livello elevato? Bene... non c'è modo. Non ci sono ricette pronte. Devi capire il problema, decidere cosa intendi fare (per non fare passi non necessari in seguito) e solo allora puoi tirare fuori un profiler che possa dirti qualcosa di utile. A un certo punto, capisci da solo che ti sei liberato delle cose superflue ed è arrivato il momento di affinare a un basso livello. Questo è senza dubbio una forma d'arte. Molti fanno cose inutili, ma si muovono così velocemente che non hanno tempo per occuparsi delle prestazioni. Ma finché la questione non diventa urgente. Di solito, il 99% del tempo a nessuno interessa quello che faccio, fino al momento in cui non emerge un problema cruciale di cui a qualcuno importa. E lì tutti iniziano a chiederti: «perché non ha funzionato perfettamente fin dall'inizio». Insomma, c'è sempre qualcosa da migliorare nelle performance. Ma il 99% del tempo non hai indizi! Stai solo cercando di far funzionare qualcosa e nel processo capisci cosa è importante. Non si può mai sapere in anticipo che quella parte debba essere perfetta, quindi, in sostanza, devi essere perfetto in tutto. E questo è impossibile e non lo fai. Ci sono sempre un sacco di cose da riparare – ed è assolutamente normale.

Come effettuare un grande refactoring

Andrey: Come lavorate sulle prestazioni? Si tratta di un problema trasversale. Ad esempio, vi è mai capitato di affrontare questioni che sorgono a causa dell'intersezione di molte funzionalità già esistenti?

Cliff: Cerco di evitarlo. Se so che le prestazioni diventeranno un problema, ci rifletto prima di iniziare a codificare, soprattutto sulle strutture dati. Ma spesso scopri tutto questo molto più tardi. E allora è necessario ricorrere a misure estreme e fare ciò che chiamo «riscrivi e domina»: bisogna afferrare un pezzo abbastanza grande. Parte del codice dovrà comunque essere riscritta a causa di problemi di prestazioni o per qualche altro motivo. Qualunque sia la ragione per cui si riscrive il codice, è quasi sempre meglio riscrivere un pezzo più grande piuttosto che uno più piccolo. In quel momento tutti iniziano a tremare per la paura: «Oh no, non si può toccare così tanto codice!». Ma, di fatto, questo approccio funziona quasi sempre molto meglio. Bisogna affrontare subito un problema grande, tracciarne un grande cerchio attorno e dire: tutto ciò che è all'interno del cerchio lo riscriverò. Il confine è molto più piccolo rispetto al contenuto all'interno di esso che deve essere sostituito. E se una tale delimitazione dei confini permette di lavorare all'interno in modo perfetto – sei libero, fai quello che vuoi. Non appena hai compreso il problema, il processo di riscrittura diventa molto più semplice, quindi afferra un grande pezzo!
Allo stesso tempo, quando fai un grande rifacimento e ti rendi conto che le prestazioni diventeranno un problema, puoi subito iniziare a preoccupartene. Di solito si traduce in cose semplici come "non copiare i dati, gestisci i dati il più semplicemente possibile, rendili più piccoli". Nei grandi rifacimenti, ci sono modi standard per migliorare le prestazioni. E questi girano quasi sempre attorno ai dati.

Modello di costo

Andrey: In uno dei podcast hai parlato dei modelli di costo in relazione alle prestazioni. Puoi spiegare cosa intendevi con questo?

Cliff: Certo. Sono nato in un'epoca in cui le prestazioni del processore erano estremamente importanti. E quest'era sta tornando – il destino non è privo di ironia. Ho iniziato a vivere ai tempi delle macchine a otto bit, il mio primo computer operava con 256 byte. Proprio byte. Tutto era molto piccolo. Dovevamo contare le istruzioni e, man mano che iniziavamo a progredire nelle pile dei linguaggi di programmazione, i linguaggi si occupavano di sempre di più. C'era l'Assembly, poi il Basic, poi il C, e il C si occupava di molteplici dettagli, come la distribuzione dei registri e la selezione delle istruzioni. Ma tutto era piuttosto chiaro e se creavo un puntatore a un'istanza di una variabile, avrei ottenuto un caricamento, e il costo di quell'istruzione era noto. L'hardware fornisce un numero noto di cicli macchina, quindi la velocità di esecuzione di diverse operazioni può essere facilmente calcolata sommando tutte le istruzioni che intendi eseguire. Ogni compare/test/branch/call/load/store poteva essere sommato e si poteva dire: ecco il tuo tempo di esecuzione. Quando lavori sull'ottimizzazione delle prestazioni, noterai sicuramente quali numeri corrispondono a piccoli cicli caldi. 
Ma non appena inizi a utilizzare Java, Python e simili, ti allontani rapidamente dall'hardware di basso livello. Qual è il costo della chiamata a un getter in Java? Se il JIT in HotSpot funziona correttamente, inlaynato, sarà un caricamento, ma se non lo fa, sarà una chiamata a funzione. Poiché la chiamata è all'interno di un ciclo caldo, annullerà tutte le altre ottimizzazioni in quel ciclo. Pertanto, il costo reale sarà molto maggiore. E così perdi immediatamente la capacità di guardare un pezzo di codice e comprendere perché dovremmo eseguirlo in termini di cicli di clock della CPU, memoria utilizzata e cache. Tutto questo diventa interessante solo se ci si immerge davvero nelle prestazioni.
Ci troviamo ora in una situazione in cui la velocità dei processori non cresce quasi più da un decennio. I vecchi tempi stanno tornando! Non puoi più contare su buone prestazioni a thread singolo. Ma se improvvisamente ti dedichi al calcolo parallelo, è incredibilmente complicato e tutti ti guardano come se fossi James Bond. Qui il decuplicare è qualcosa che si verifica generalmente nei casi in cui qualcuno ha perso di vista qualcosa. La parallelità richiede molto lavoro. Per ottenere quel decuplimento, devi capire il modello di costo. Cosa e quanto costa. E per fare ciò devi capire come il linguaggio interagisce con l'hardware sottostante.
Martin Thompson ha scelto una parola fantastica per il suo blog Simpatia Meccanica! È fondamentale capire cosa deve fare l'hardware, come lo farà esattamente e perché lo fa. Utilizzando queste informazioni, è piuttosto semplice iniziare a contare le istruzioni e scoprire dove sta andando 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 senza avere la minima idea di cosa stiano realmente facendo. Si sforzano molto e non riescono a fare progressi significativi. Quando prendo lo stesso pezzo di codice, realizzo un paio di piccole modifiche e ottengo un miglioramento di cinque o dieci volte, loro dicono: beh, non è giusto, sapevamo già che eri migliore. Sorprendente. Di cosa stavo parlando... il modello dei costi riguarda ciò che scrivi in codice e quanto velocemente funziona in media nel contesto generale.

Andrey: E come si può mantenere in mente un tale volume? Si ottiene attraverso molta esperienza, giusto? Dove si acquisisce tale esperienza?

Cliff: Beh, ho acquisito la mia esperienza in modo piuttosto complicato. Ho programmato in Assembler nei tempi in cui era possibile comprendere ogni singola istruzione. Può sembrare sciocco, ma da allora ho nella mia testa, nella mia memoria, rimasto per sempre un insieme di istruzioni Z80. Non ricordo i nomi delle persone già un minuto dopo aver parlato, ma ricordo il codice scritto 40 anni fa. È divertente, sembra un sindrome di "scienziato pazzo».

Apprendimento delle ottimizzazioni di basso livello

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

Cliff: Sì e no. L'hardware che usiamo tutti non è cambiato molto nel tempo. Tutti usano x86, ad eccezione degli smartphone basati su Arm. Se non ti occupi di qualche embed hardcore, hai tutto lo stesso. Bene, procediamo. Anche le istruzioni non sono cambiate da secoli. Bisogna andare e scrivere qualcosa in Assembly. Poco, ma abbastanza per iniziare a capire. Tu stai sorridendo, ma io lo dico seriamente. Devi capire la corrispondenza tra linguaggio e hardware. Dopo di che, devi scrivere un po' e realizzare un piccolo compilatore giocattolo per un piccolo linguaggio giocattolo. 'Giocattolo' significa che deve essere fatto in un tempo ragionevole. Può essere super semplice, ma deve generare istruzioni. L'atto di generazione delle istruzioni permetterà di comprendere il modello di costo per il ponte tra il codice di alto livello, che tutti scrivono, e il codice macchina, che viene eseguito sull'hardware. Questa corrispondenza si fisserà nella mente nel momento in cui scrivi il compilatore. Anche il compilatore più semplice. Dopo di ciò, puoi iniziare a guardare a Java e scoprire che ha un divario semantico molto più profondo, e costruire ponti sopra di esso è molto più complicato. In Java è molto più difficile capire se il nostro ponte è buono o cattivo, cosa lo farà crollare e cosa no. Ma hai bisogno di un punto di partenza, quando guardi il codice e comprendi: 'Ecco, questo getter dovrebbe essere inlined ogni volta'. E poi scopri che a volte è così, tranne nei casi in cui il metodo diventa troppo grande e JIT inizia a inlineare tutto. La performance di questi punti può essere prevista istantaneamente. Di solito i getter funzionano bene, ma poi guardi ai grandi cicli caldi e realizzi che ci sono chiamate di funzione che non si capisce cosa facciano. È lì che risiede il problema con l'uso generalizzato dei getter, il motivo per cui non vengono inlinati: non è chiaro se sia un getter. Se hai una base di codice super piccola, puoi semplicemente memorizzarla e poi dire: ecco un getter, ecco un setter. In una grande base di codice, ogni funzione vive la propria storia, che sostanzialmente nessuno conosce. Il profiler dice che abbiamo perso il 24% del tempo su un ciclo e per capire cosa fa quel ciclo bisogna analizzare ogni funzione all'interno. Impossibile comprenderlo senza studiare la funzione, e questo rallenta notevolmente il processo di comprensione. Ecco perché non utilizzo getter e setter, ho raggiunto un nuovo livello!
Dove posso trovare un modello di costo? Beh, certo, si può leggere qualcosa... Ma penso che il modo migliore sia agire. Creare un piccolo compilatore sarà il miglior modo per comprendere il modello di costo e assimilarlo. Un piccolo compilatore che sia utile per programmare un microonde è un compito da principianti. Beh, voglio dire, se hai già abilità di programmazione, dovrebbero essere sufficienti. Tutte queste cose come analizzare una stringa che sarà una qualche espressione algebraica, estrarre da essa le istruzioni per le operazioni matematiche nell'ordine corretto, prendere i valori corretti dai registri — tutto questo si fa facilmente. E mentre lo fai, si imprime nella tua mente. Penso che tutti sappiano cosa fa un compilatore. E questo darà familiarità con il modello di costo.

Esempi pratici di miglioramento delle prestazioni

Andrey: A cosa altro bisogna prestare attenzione quando si lavora sulle prestazioni?

Cliff: Strutture dati. A proposito, non ho tenuto queste lezioni da un po'... Rocket School. È stato divertente, ma richiedeva così tanto impegno, e ho una vita anche io! Comunque, durante una delle lezioni più grandi e interessanti, "Dove va il vostro performance", ho dato agli studenti un esempio: due e mezzo gigabyte di dati fintech letti da un file CSV, e poi bisognava calcolare il numero di prodotti venduti. Dati di mercato standard. Pacchetti UDP trasformati in formato testuale, risalenti agli anni '70. Chicago Mercantile Exchange – varie cose come petrolio, mais, fagioli di soia e simili. Bisognava contare questi prodotti, il numero di operazioni, il volume medio di movimento di denaro e beni, ecc. Questa è una matematica commerciale abbastanza semplice: trovare il codice prodotto (che sono 1-2 caratteri in una tabella hash), ottenere il totale, aggiungerlo a uno dei set di operazioni, aggiungere il volume, aggiungere il costo e un paio di altre cose. Matematica molto semplice. L'implementazione giocattolo era molto diretta: tutto era nel file, leggevo il file e lo scorrevo, separando le singole registrazioni in righe Java, cercando le cose necessarie e sommando secondo la matematica descritta sopra. E questo funziona con una certa velocità.

Con questo approccio è evidente cosa sta 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 focus era sul fatto che non si dovrebbe effettuare allocazioni di memoria in un ciclo caldo. Beh, non è tutta la verità, ma in generale – non si dovrebbe allocare "una volta ogni X", quando X è sufficientemente grande. Quando X è di due e mezzo gigabyte, non bisognerebbe allocare nulla "una volta per lettera", o "una volta per riga", o "una volta per campo", niente di simile. È proprio questo a richiedere tempo. Come funziona tutto ciò? Immagina che io faccia una chiamata String.split() o BufferedReader.readLine(). Readline crea una stringa da un insieme di byte ricevuti dalla rete, una volta per ogni stringa, per ciascuna delle centinaia di milioni di stringhe. Prendo questa stringa, la analizzo e la scarto. Perché la scarto? Beh, l'ho già elaborata, quindi basta. Perciò, per ogni byte letto da questi 2.7G, verranno scritti due caratteri nella stringa, il che significa già 5.4G, e non mi servono più, quindi vengono scartati. Se guardiamo alla larghezza di banda della memoria, carichiamo 2.7G, che attraversano la memoria e il bus di memoria nel processore, e poi il doppio viene spedito nella stringa in memoria, e tutto questo viene elaborato mentre si crea ogni nuova stringa. Ma devo leggerla, l'hardware la legge, anche se poi tutto viene ri-elaborato. E devo scriverla, perché ho creato una stringa e le cache sono sovraccariche – la cache non può contenere 2.7G. Dunque, per ogni byte letto, leggo altri due byte aggiuntivi e scrivo due byte aggiuntivi, e alla fine hanno un rapporto 4:1 – in questo rapporto sprechiamo irresponsabilmente la larghezza di banda della memoria. Ma poi si scopre che se faccio String.split() – non è la prima volta che lo faccio, lì dentro potrebbero esserci altre 6-7 aree. Pertanto, il codice di lettura CSV classico con successivo parsing delle righe porta a una perdita di banda di memoria di circa 14:1 rispetto a quanto vorreste realmente avere. Se eliminate queste allocazioni, potete ottenere un incremento delle prestazioni di cinque volte.

E non è che sia molto difficile. Se guardate il codice nella giusta prospettiva, tutto diventa piuttosto semplice, non appena comprendete il problema. Non dovete smettere di allocare memoria: il problema è solo che allocando qualcosa subito muore e nel processo brucia una risorsa importante, che in questo caso è la banda di memoria. E tutto questo si traduce in un calo delle prestazioni. Su x86 di solito bisogna bruciare attivamente i cicli del processore, mentre qui avete esaurito tutta la memoria molto prima. La soluzione è ridurre il numero di allocazioni. 
Un'altra parte del problema è che, se esegui il profiler quando la banda di memoria è esaurita, proprio nel momento in cui ciò accade, di solito aspetti il ritorno della cache, poiché è piena di spazzatura che hai appena prodotto, con tutte queste righe. Pertanto, ogni operazione di load o store diventa lenta, poiché portano a miss nel cache: l'intera cache diventa lenta, in attesa che la spazzatura venga rimossa. Così, il profiler mostrerà solo un rumore casuale caldo, sfumato lungo l'intero ciclo - non ci sarà alcuna singola istruzione calda o luogo nel codice. Solo rumore. E se guardi i cicli di GC, saranno tutti nella Young Generation e super veloci - microsecondi o millisecondi al massimo. Infatti, tutta questa memoria muore all'istante. Allocando miliardi di gigabyte, li taglia e li taglia e di nuovo li taglia. Tutto ciò avviene molto rapidamente. Risultato: ci sono cicli di GC economici, un rumore caldo lungo tutto il ciclo, ma noi vogliamo ottenere un'accelerazione di 5 volte. In quel momento, qualcosa deve scattare nella tua mente e farti chiedere: «Perché?!». Un overflow della banda di memoria non viene visualizzato nel classic debugger, è necessario eseguire il debugger dei contatori hardware delle prestazioni e vederlo direttamente. Non direttamente, è possibile sospettarlo da questi tre sintomi. Il terzo sintomo è quando guardi cosa hai allocato, chiedi al profiler e lui risponde: «Hai creato un miliardo di righe, ma il GC ha lavorato gratuitamente». Non appena questo accade, capisci che hai prodotto troppi oggetti e hai bruciato tutta la banda di memoria. C'è un modo per capirlo, ma non è ovvio. 

Problema nella struttura dei dati: la struttura nuda che sta dietro a tutto ciò è troppo grande, occupa 2.7G di spazio su disco, quindi creare una copia di questa cosa è molto indesiderabile – vorresti caricarla direttamente dal buffer di rete nei registri, per non dover leggere e scrivere nel buffer ripetutamente. Sfortunatamente, Java non ti fornisce per impostazione predefinita una tale libreria con il JDK. Ma è in effetti banale, giusto? Fondamentalmente, si tratta di 5-10 righe di codice per implementare il proprio caricatore di righe bufferizzato, che replica il comportamento della classe String, fungendo da involucro attorno al buffer di byte sottostante. Il risultato è che lavori quasi come con le stringhe, ma in realtà stai muovendo puntatori sul buffer, e i byte grezzi non vengono copiati da nessuna parte, riutilizzando quindi gli stessi buffer più e più volte, mentre il sistema operativo è lieto di gestire compiti per cui è stato progettato, come la doppia bufferizzazione nascosta di questi buffer. A proposito, capite che quando si lavora con la GC si garantisce che ogni allocazione di memoria non sarà visibile alla CPU dopo l'ultimo ciclo di GC? Quindi, tutto questo non può essere nella cache, e si verifica un colpo sicuro del 100%. Quando si utilizza un puntatore, su x86 sottrarre un registro dalla memoria richiede 1-2 cicli, e non appena ciò accade, paghi, paghi, paghi, perché la memoria è tutta NOVE cache – e questo rappresenta il costo di allocazione della memoria. Il vero costo.

In altre parole, le strutture dati sono tra le più difficili da modificare. Una volta che ci si rende conto di aver scelto una struttura dati sbagliata che comprometterà le performance future, di solito è necessario fare un lavoro notevole per apportare modifiche, e se non lo si fa, le cose andranno solo peggio. È fondamentale pensare alle strutture dati. Il vero costo risiede in solide strutture dati, scelte in modo superficiale con la mentalità "ho copiato la struttura dati X nella struttura dati Y perché Y mi piace esteticamente di più". Tuttavia, l'operazione di copia (che appare economica) consuma effettivamente larghezza di banda in memoria, ed è qui che si nasconde tutto il tempo di esecuzione perso. Se ho una gigantesca stringa JSON e voglio trasformarla in un albero DOM strutturato di POJO o simili, l'operazione di parsing di quella stringa e la creazione dei POJO, seguita da un nuovo accesso ai POJO, comporterà un costo aggiuntivo – è una questione costosa. A meno che non si acceda ai POJO molto più frequentemente che alla stringa. In alternativa, si potrebbe semplicemente decodificare la stringa ed estrarre solo ciò che serve, senza convertirla in alcun POJO. Se tutto ciò accade in un contesto in cui è richiesta la massima performance, niente POJO – bisogna in qualche modo lavorare direttamente sulla stringa.

Perché creare un proprio linguaggio di programmazione

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

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

Andrey: A proposito, per quanto ne so, stai sperimentando nella creazione di linguaggi propri. Perché?

Cliff: Perché posso! Sono in parte in pensione, quindi questo è il mio hobby. Ho realizzato i linguaggi di qualcuno per tutta la vita. Ho anche lavorato molto sullo stile di codifica. Inoltre, vedo problemi in altri linguaggi. Vedo che ci sono modi migliori per fare cose familiari. E ne farei uso. Sono stanco di vedere problemi in me stesso, in Java, in Python, in qualsiasi altro linguaggio. Attualmente scrivo in React Native, JavaScript ed Elm come hobby, che non riguarda la pensione, ma un lavoro attivo. Scrivo anche in Python e probabilmente continuerò a lavorare sull'apprendimento automatico per i backend Java. Ci sono molti linguaggi popolari e tutti hanno caratteristiche interessanti. Ognuno è valido per qualcosa di unico e si possono provare a raccogliere tutte queste caratteristiche insieme. Quindi, sto studiando cose interessanti per me, il comportamento del linguaggio, cercando di inventare una semantica ragionevole. E finora ci riesco! Attualmente, sto affrontando la semantica della memoria, perché voglio averla come in C e Java, ottenendo un modello di memoria forte e una semantica della memoria per i carichi e gli archivi. Allo stesso tempo, voglio avere un'inferenza dei tipi automatica come in Haskell. Ecco, sto cercando di mescolare l'inferenza dei tipi simile a Haskell con la memoria che funge come in C e Java. Su questo ho lavorato negli ultimi 2-3 mesi, ad esempio.

Andrey: Se stai creando un linguaggio che prende i migliori aspetti da altri linguaggi, hai mai pensato che qualcuno possa fare il contrario: prendere le tue idee e usarle?

Cliff: È proprio così che nascono nuovi linguaggi! Perché Java è simile a C? Perché C aveva una sintassi valida che tutti comprendevano e Java si è ispirato a quella sintassi, aggiungendo sicurezza dei tipi, controlli sui limiti degli array, GC, e hanno migliorato alcune cose di C. Hanno aggiunto le proprie. Ma si sono ispirati piuttosto fortemente, giusto? Tutti si poggiano sulle spalle dei giganti che sono venuti prima di loro, ed è proprio così che si fa progresso.

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

Cliff: Beh, 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. Ed è davvero, davvero doloroso occuparsi di tutto questo manualmente. Vorrei che il compilatore dicesse semplicemente cosa succede e quali risultati ottieni con le tue azioni. Per alcune cose, il borrow checker fa giusto questo di default. Inoltre, dovrebbe automaticamente fornire informazioni, capire tutto e non gravarmi nemmeno del dover esporre tale comprensione. Dovrebbe almeno eseguire un'analisi di escape locale, e solo se non riesce a farlo, allora dovrei aggiungere annotazioni di tipo che descrivano il ciclo di vita – e uno schema del genere è molto più complesso del borrow checker, o di qualsiasi altro verificatore di memoria esistente. La scelta tra «tutto va bene» e «non ho capito nulla» — no, dovrebbe esserci qualcosa di meglio. 
Quindi, come persona che ha scritto molto codice in C, ritengo che avere supporto per la gestione automatica del ciclo di vita sia fondamentale. Inoltre, sono stanco di quanto Java utilizzi memoria e il problema principale è il garbage collector. Quando si richiede memoria in Java, non si riottiene la memoria che era locale nell'ultimo ciclo di garbage collection. Nei linguaggi con una gestione della memoria più precisa non è così. Se chiami malloc, ottieni immediatamente la memoria che è stata appena utilizzata. Di solito, si fanno delle operazioni temporanee con la memoria e poi si restituisce immediatamente. E viene rapidamente reintegrata nel pool di malloc, e il successivo ciclo di malloc la riutilizza. Pertanto, l'uso reale della memoria è ridotto al set di oggetti vivi in un dato momento, più le perdite. E se non ci sono perdite del tutto irricevibili, gran parte della memoria rimane nei cache e nel processore, e questo funziona rapidamente. Ma richiede molta gestione manuale della memoria tramite malloc e free, chiamati nell'ordine corretto, nel posto giusto. Rust può gestire correttamente tutto questo da solo e in molti casi offre anche prestazioni superiori, poiché l'uso della memoria si restringe solo ai calcoli attuali, a differenza dell'attesa del prossimo ciclo di gc che libererà la memoria. In fin dei conti, abbiamo ottenuto un modo molto interessante per migliorare le prestazioni. E piuttosto potente – nel senso che mi sono occupato di simili questioni nell'elaborazione dei dati per il fintech, e ciò ha permesso di ottenere un'accelerazione fino a cinque volte. Questo è un notevole incremento, soprattutto in un mondo in cui i processori non diventano più veloci, mentre continuiamo a aspettarci miglioramenti.

Carriera di ingegnere delle prestazioni

Andrey: Vorrei anche chiederti della carriera in generale. Sei diventato famoso grazie al tuo lavoro su JIT in HotSpot, per poi passare ad Azul – che è anche un’azienda JVM. Ma lì ti sei occupato più di hardware che di software. Poi, all'improvviso, ti sei spostato su Big Data e Machine Learning, e successivamente su rilevazione delle frodi. Com'è successo? Sono ambiti di sviluppo molto diversi.

Cliff: Sono un programmatore da molto tempo e ho avuto esperienze in molti ambiti diversi. Quando le persone dicono: «Oh, sei quello che ha fatto JIT per Java!», trovo sempre che sia divertente. Prima di questo, mi sono occupato di un clone di PostScript, il linguaggio che Apple utilizzava una volta per le sue stampanti laser. E prima ancora, ho realizzato un'implementazione del linguaggio Forth. Penso che il tema centrale per me sia lo sviluppo di strumenti. Ho passato la mia vita a creare strumenti che aiutano altre persone a scrivere i loro fantastici programmi. Ma mi sono anche dedicato allo sviluppo di sistemi operativi, driver, debugger a livello di kernel, e linguaggi per l'implementazione di sistemi operativi, che iniziavano in modo semplice ma diventavano sempre più complessi. Ma il focus principale rimane, comunque, lo sviluppo di strumenti. Un ampio periodo della mia vita è trascorso tra Azul e Sun, ed era tutto incentrato su Java. Ma quando mi sono dedicato a Big Data e Machine Learning, ho di nuovo indossato il mio cappello da festa e ho detto: «Oh, adesso abbiamo un problema non banale, ed è in corso un sacco di cose interessanti e persone che stanno facendo qualcosa». È un ottimo percorso di sviluppo da seguire.

Sì, amo molto il calcolo distribuito. Il mio primo lavoro è stato durante gli studi, in C, su un progetto pubblicitario. Era un calcolo distribuito su chip Zilog Z80, che raccoglievano dati per il riconoscimento ottico analogo di testi, realizzato da un vero analizzatore analogico. Era un argomento interessante e decisamente originale. Ma c'erano dei problemi, alcune parti non venivano riconosciute correttamente, quindi bisognava estrarre l'immagine e mostrarla a qualcuno che già sapeva leggere, il quale comunicava cosa c'era scritto. Così c'erano lavori con dati, e questi lavori avevano il loro linguaggio. C'era un backend che gestiva tutto questo – Z80 funzionanti in parallelo con terminali vt100 in uso – uno per persona, e c'era un modello di programmazione parallela su Z80. Una certa porzione di memoria comune che veniva condivisa da tutti gli Z80 all'interno di una configurazione a stella; sia il backplane che metà della RAM erano condivisi all'interno della rete, mentre l'altra metà era privata o utilizzata per altro. Una sistema distribuito complesso e significante con memoria condivisa… semi-condivisa. Quando è stato… Non riesco più a ricordare, circa a metà degli anni '80. È passato un bel po' di tempo. 
Sì, consideriamo che 30 anni siano abbastanza tempo. Le sfide legate al calcolo distribuito esistono da molto, e le persone hanno combattuto con esse da tempo, Beowulf-cluster. Questi cluster sembrano… Ad esempio: c'è Ethernet e il tuo veloce x86 è collegato a questo Ethernet, e ora desideri ottenere una memoria condivisa fittizia, perché allora nessuno poteva dedicarsi alla programmazione del calcolo distribuito, era troppo complicato e quindi esisteva una memoria condivisa fittizia con protezione delle pagine di memoria su x86. Se scrivevi in questa pagina, informavamo gli altri processori che, se accedevano alla stessa memoria condivisa, avrebbero dovuto ricaricarla da te, ed è così che è nato un protocollo per supportare la coerenza della cache e il software per questo. Un concetto interessante. Il vero problema, ovviamente, era un altro. Tutto questo funzionava, ma si evidenziavano rapidamente problemi di performance, poiché nessuno comprendeva il modello di performance a un livello sufficientemente buono: quali fossero i modelli di accesso alla memoria, come evitare che i nodi pingassero all'infinito tra di loro, e così via.

In H2O ho concepito quanto segue: gli stessi sviluppatori sono responsabili di identificare dove si nasconde il parallelismo e dove non c'è. Ho ideato un modello di codifica che rende la scrittura di codice ad alte prestazioni facile e semplice. Scrivere codice poco performante, invece, è difficile e risulta poco gradevole. È necessario fare uno sforzo significativo per scrivere codice lento, dovendo ricorrere a metodi non convenzionali. Il codice lento è evidente a colpo d'occhio. Di conseguenza, di solito si scrive codice che funziona rapidamente, ma bisogna capire come gestire la memoria condivisa. Tutto è legato a grandi array e il comportamento è simile a quello di grandi array non volatili in Java parallelo. In altre parole, immaginate che due thread scrivano in un array parallelo: uno di essi ha successo, mentre l'altro, ovviamente, no, e non sapete chi è chi. Se non sono volatili, l'ordine può essere qualsiasi – e questo funziona davvero bene. Le persone si prendono cura dell'ordine delle operazioni, posizionano correttamente volatile e, nei posti giusti, si aspettano problemi di prestazioni legati alla memoria. Altrimenti, scriverebbero semplicemente codice in 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 è né Java né Scala, si può considerare come «Java meno meno», se si desidera. È uno stile di programmazione molto chiaro e assomiglia a scrivere codice semplice in C o Java con cicli e array. Ma nel frattempo, la memoria può essere gestita in terabyte. Uso ancora H2O. Di tanto in tanto lo utilizzo in diversi progetti – e resta ancora la cosa più veloce, di decine di volte superiore ai concorrenti. Se state lavorando con Big Data e dati a colonne, è molto difficile superare H2O.

Sfide tecniche

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

Cliff: Stiamo discutendo la parte tecnica o quella non tecnica della questione? Direi che le sfide più grandi sono quelle non tecniche. 
Per quanto riguarda le sfide tecniche, le ho semplicemente vinte. Non so nemmeno quale sia stata la più grande, ma ci sono stati diversi problemi abbastanza interessanti che hanno richiesto molto tempo e sforzo mentale. Quando sono andato a Sun, ero sicuro di poter realizzare un compilatore veloce, ma molti senior mi dicevano che non ce l'avrei mai fatta. Eppure ho intrapreso questa strada, scrivendo un compilatore fino all'allocatore di registri, e sorprendentemente veloce. Era veloce quanto un moderno C1, ma all'epoca l'allocatore era molto più lento; ripensandoci, era un problema di una grande struttura dati. Ne avevo bisogno per scrivere un allocatore grafico di registri e non capivo la diatriba tra l'espressività del codice e la velocità che esisteva all'epoca e che era molto importante. Si è rivelato che la struttura dati superava generalmente la dimensione della cache sui sistemi x86 di quel tempo e quindi, se inizialmente avevo ipotizzato che l'allocatore di registri avrebbe assorbito il 5-10% dell'intero tempo di JIT, in realtà si è rivelato essere il 50%.

Il tempo passava, il compilatore diventava sempre più chiaro e performante, smetteva di generare codice inguardabile in un numero sempre maggiore di casi, e le prestazioni cominciavano ad assomigliare a quelle del compilatore C. A meno che, ovviamente, tu non stia scrivendo qualche schifezza che neanche C riesce ad accelerare. Se scrivi codice come se fossi su C, ottieni anche performance simili a C in un numero maggiore di casi. E più andavo avanti, più frequentemente il codice risultava asintoticamente in linea con il livello C; l'allocatore di registri iniziava a sembrare qualcosa di compiuto... indipendentemente dal fatto che il tuo codice fosse veloce o lento. Continuavo a lavorare sull'allocatore per ottenere allocazioni migliori. Diventava sempre più lento, ma offriva prestazioni sempre più eccellenti nei casi in cui nessun altro riusciva. Potevo immergermi nell'allocatore di registri, dedicare un mese di lavoro, e improvvisamente tutto il codice iniziava a girare il 5% più velocemente. Questo accadeva ripetutamente e l'allocatore di registri diventava una sorta di opera d'arte: tutti lo amavano o lo odiavano, e le persone dell'accademia si ponevano domande su "perché tutto venisse fatto in questo modo", perché non... scansione lineare, e qual è la differenza. La risposta è sempre la stessa: un allocatore basato su grafi colorati più un'attenta gestione del codice buffer è uguale a uno strumento vincente, la migliore combinazione che nessuno può battere. Ed è piuttosto un aspetto non ovvio. Tutto il resto che fa il compilatore è piuttosto studiato, anche se portato a un livello di arte. Ho sempre realizzato cose che dovevano trasformare il compilatore in un'opera d'arte. Ma nulla di tutto ciò era eccezionale — tranne per l'allocatore di registri. Il trucco è che bisogna fare attenzione a tagliare sotto carico e, se ciò accade (posso spiegare meglio, se ti interessa), significa che si può inlinare in modo più aggressivo, senza il rischio di superare il punto di rottura nella curva delle prestazioni. A quei tempi c'erano molte compilazioni complete, ricche di accessori e suoni, che avevano allocatori di registri, ma nessuno è riuscito più a fare così.

Il problema è che quando aggiungi metodi suscettibili di inlining, aumentando sempre di più l'area di inlining, l'insieme dei valori utilizzati supera istantaneamente il numero di registri disponibili, e si deve eliminare. Il livello critico di solito si raggiunge quando l'allocatore cede, e uno dei buoni candidati per lo spillage vale più di un altro, ed è lì che si sprecano valori completamente assurdi. Il valore dell'inlining sta nel fatto 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 spills più di quanto necessario, perdi subito. Pertanto, la maggior parte degli allocatori ha un problema: quando l'inlining supera una certa soglia, inizia a spillarsi tutto e la performance può andare in malora. Coloro che sviluppano compilatori aggiungono alcune euristiche: per esempio, per fermare l'inlining una volta raggiunta una certa dimensione sufficiente, poiché le allocazioni rovinerebbero tutto. Così si forma una rottura nel grafico delle performance: in inline, inline, le performance crescono lentamente — e poi boom! — crollano a picco, perché hai inline troppa roba. Così è sempre andata fino all'arrivo di Java. Java richiede molto più inlining, quindi ho dovuto rendere il mio allocatore molto più aggressivo, affinché si mantenesse su un buon livello e non scendesse; e se hai inline troppo, inizia a spillare, ma c'è comunque un momento in cui non ci sono più spillaggi. Questa è un'osservazione interessante ed è arrivata a me da un'illuminazione, non ovvia, ma ben ripagata. Ho iniziato a fare inlining aggressivo e questo mi ha portato in posizioni in cui le performance di Java e C sono affiancate. Sono davvero vicine: posso scrivere codice in Java che risulta significativamente più veloce del codice C e simili, ma in media, nella grande visione delle cose, sono grossolanamente comparabili. Credo che parte di questo merito vada all'allocatore di registri, che mi consente di fare inlining in modo praticamente a tappeto. In effetti, faccio inlining per tutto ciò che vedo. La questione è se l'allocatore funzioni bene, e se il codice risultante sia ragionevolmente funzionante. Questa è stata una grande sfida: capire tutto questo e farlo funzionare.

Un po' su allocazione dei registri e multithreading

Vladimir: Problemi come l'allocazione dei registri sembrano un tema infinito. È interessante sapere se qualche idea sembrava promettente, ma poi è fallita nella pratica?

Cliff: Certo! L'allocazione dei registri è un'area in cui, per risolvere un problema NP-completo, cerchi di trovare alcune euristiche. E non potrai mai raggiungere la soluzione ideale, giusto? È semplicemente impossibile. Guarda, la compilazione Ahead of Time - funziona male anche essa. Qui si parla di casi medi. Di prestazioni tipiche, quindi puoi andare e misurare qualcosa che consideri una buona prestazione tipica - dopo tutto, stai lavorando per migliorarla! L'allocazione dei registri è un tema interamente dedicato alle prestazioni. Una volta che hai il primo prototipo, che funziona e rende ciò che serve, inizia il lavoro sulle performance. Devi imparare a misurare bene. Perché è importante? Se ci sono dati chiari, puoi guardare a diverse aree e vedere: ah, questo ha aiutato qui, ma lì è andato tutto male! Emergeranno alcune buone idee, aggiungi una nuova euristica e improvvisamente tutto inizia a funzionare mediamente un po' meglio. O non inizia. Ho avuto molti casi in cui abbiamo lottato per cinque punti percentuali di prestazioni che distingevano il nostro sviluppo dagli allocatori precedenti. E ogni volta appare così: hai vinto da qualche parte, hai perso da qualche altra parte. Se hai buoni strumenti di analisi delle prestazioni, puoi trovare idee perdenti e capire perché stanno perdendo. Forse vale la pena lasciare tutto com'è, o forse è il caso di fare una seria messa a punto, o andare a sistemare qualcos'altro. Questo è un intero insieme di cose! Ho fatto questo fantastico hack, ma ho bisogno anche di questo, di questo e di quest'altro – e la loro combinazione complessiva porta a dei miglioramenti. Ma gli hack singoli possono fallire. Questa è la natura del lavoro sulle prestazioni dei problemi NP-completi.

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

Cliff: Non è risolta in quanto tale. Devi trasformarla in "risolta". Ci sono compiti difficili e vanno affrontati. Quando ciò è fatto, arriva il momento di lavorare sulle prestazioni. Questa fase deve essere presa sul serio: eseguire benchmark, raccogliere metriche, spiegare situazioni in cui, tornando a una versione precedente, il tuo vecchio hack funziona di nuovo (o viceversa, smette di funzionare). E non fermarti finché non ottieni risultati. Come ho già detto, ci sono idee interessanti che non hanno avuto successo, ma nel campo dell'allocazione dei registri le idee sono praticamente infinite. Puoi, ad esempio, leggere pubblicazioni scientifiche. Sebbene ora questo campo si muova più lentamente e sia diventato più chiaro rispetto ai tempi della sua giovinezza. Tuttavia, in questo settore lavorano un'infinità di persone e tutte le loro idee meritano di essere provate, tutte aspettano il loro momento. E non puoi sapere quanto siano buone se non le provi. Quanto bene si integrano con tutto il resto nel tuo allocatore, perché l'allocatore fa molte cose, e alcune idee nel tuo specifico allocatore potrebbero non funzionare, mentre in un altro allocatore funzionerebbero senza problemi. Il principale modo per vincere per un allocatore è estrarre le cose lente dal percorso principale e forzare la suddivisione lungo i confini dei percorsi lenti. Quindi, se vuoi avviare la GC, seguire il percorso lento, deottimizzare, generare un'eccezione, e così via – sai che queste situazioni sono relativamente rare. E sono davvero rare, l'ho verificato. Effettui un lavoro aggiuntivo e grazie a ciò molti vincoli su questi percorsi lenti scompaiono, ma questo non è molto importante, poiché sono lenti e ci si passa raramente. Ad esempio, un puntatore nullo – non accade mai, giusto? Devi avere più percorsi per diverse cose, ma non devono intralciarsi nel principale. 

Vladimir: Cosa pensi della multi-core quando ci sono migliaia di core? È una cosa utile?

Cliff: Il successo della GPU dimostra che è abbastanza utile!

Vladimir: Sono piuttosto specializzati. E per quanto riguarda i processori generali?

Cliff: Beh, questo era il modello di business di Azul. La risposta arrivò ancora nell'era in cui la gente amava molto le performance prevedibili. All'epoca era difficile scrivere codice parallelo. Il modello di codifica H2O è molto scalabile, ma non è un modello generale. A meno che non sia leggermente più generale rispetto all'uso di GPU. Stiamo parlando della complessità di sviluppo di una cosa del genere o della complessità del suo utilizzo? Ad esempio, Azul mi ha insegnato una lezione interessante, piuttosto inaspettata: piccoli cache vanno bene. 

La sfida più grande della vita

Vladimir: E riguardo alle sfide non tecniche?

Cliff: La sfida più grande è stata non essere... gentile e buono con le persone. E di conseguenza, mi sono sempre ritrovato in situazioni estremamente conflittuali. In queste situazioni sapevo che tutto stava andando male, ma non sapevo come procedere nella risoluzione di quei problemi, e non riuscivo a gestirli. Molti problemi di lunga durata, che durano decenni, sono emersi proprio in questo modo. Il fatto che in Java ci siano i compilatori C1 e C2 è una diretta conseguenza di ciò. Il fatto che in Java non ci sia stata per un decennio la compilazione multilivello è anch'essa una diretta conseguenza. È ovvio che avevamo bisogno di un sistema del genere, ma non è chiaro 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 avuto un'opinione su tutto. E credevo fosse la verità che si potesse semplicemente prendere questa verità e dirla senza mezzi termini. Soprattutto perché avevo ragione in modo sconcertante gran parte del tempo. E se questo approccio non ti piace... soprattutto se sei ovviamente in errore e stai facendo sciocchezze... Insomma, poche persone riuscivano a tollerare un tale stile comunicativo. Anche se alcune sì, come me. Ho costruito tutta la mia vita su principi meritocratici. Se mi mostri qualcosa di sbagliato, mi volto immediatamente e dico: hai detto una sciocchezza. Ovviamente mi scuso e tutto il resto, riconosco i meriti, se ci sono, e compio gli altri passi corretti. D'altra parte, ho ragione in modo sconcertante in un'impressionante percentuale di tempo. E questo non funziona molto bene nelle relazioni con le persone. Non cerco nemmeno di essere gentile, ma metto la questione sul tavolo. 'Questo non funzionerà mai, perché uno, due e tre'. E loro sono tipo: 'Oh!'. Ci sono stati anche altri effetti, che sarebbe meglio forse saltare: per esempio, quelli che hanno portato al divorzio da mia moglie e a dieci anni di depressione dopo.

La sfida è una lotta contro le persone, contro la loro percezione di ciò che puoi o non puoi fare, di ciò che è importante e di ciò che non lo è. Ci sono stati molti challenge sullo stile di codifica. Scrivo ancora molto codice, e in quei tempi ho dovuto persino rallentare perché stavo facendo troppe cose in parallelo e non le facevo bene, invece di concentrarmi su una sola cosa. Ripensando, ho scritto metà del codice del team Java JIT, del team C2. Il prossimo programmatore in velocità scriveva la metà, l'altro ancora la metà, ed era una caduta esponenziale. Il settimo in quella fila era molto, molto lento — succede sempre così! Ho toccato un sacco di codice. Ho osservato chi scriveva cosa, senza eccezioni, fissavo il loro codice, revisionavo ciascuno di loro, e continuavo comunque a scrivere più di chiunque altro. Con le persone, questo approccio non funziona molto bene. Alcuni non lo gradiscono. E quando non riescono a gestirlo, iniziano tutte le sorta di lamentele. Ad esempio, un giorno mi hanno detto di smettere di scrivere codice, perché stavo scrivendo troppo codice e questo metteva in pericolo il team, e per me tutto ciò suonava come uno scherzo: amico, se tutto il resto del team scompare e io continuo a scrivere codice, perderai solo metà del team. D'altra parte, se continuo a scrivere codice e perdi metà del team – suona come una gestione molto scadente. Non ci ho mai pensato molto, non ne ho mai parlato, ma era comunque da qualche parte nella mia testa. Nelle retrovie della mia coscienza girava 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 per programmatori per un lungo periodo, e ora dico chiaramente alle persone: sai, io sono come sono, e dovrai affrontarmi – va bene se rimango qui? E quando hanno iniziato a gestirlo, tutto ha funzionato. In realtà non sono né buono né cattivo, non ho alcuna cattiva intenzione o obiettivi egoistici, è semplicemente la mia essenza, e bisogna trovare un modo per vivere con esse.

Andrey: Recentemente, tutti hanno iniziato a parlare di auto-consapevolezza 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 tratto dal mio divorzio con mia moglie. Ciò che ho appreso dal divorzio è stata una comprensione di me stesso. In questo modo ho cominciato a capire gli altri. Capire come funziona questa interazione. Questo ha portato a scoperte una dopo l'altra. È arrivata la consapevolezza su chi sono e cosa rappresento. Cosa faccio: o sono impegnato in un compito, o evito il conflitto, o qualcos'altro – e questo livello di autoconsapevolezza davvero aiuta a mantenere la calma. Dopo di che tutto diventa molto più semplice. Una cosa che ho scoperto non solo in me stesso, ma anche in altri programmatori è l'impossibilità di verbalizzare i pensieri quando sei sotto stress emotivo. Ad esempio, ti trovi a scrivere codice, sei in uno stato di flusso, e all'improvviso qualcuno corre da te gridando in preda al panico che qualcosa è andato storto e che ora verranno adottate misure estreme. E non riesci a dire una parola, perché sei in uno stato di stress emotivo. Le conoscenze acquisite ti permettono di prepararti a quel momento, affrontarlo e passare a un piano di fuga, dopo il quale puoi fare qualcosa. Quindi sì, quando inizi a renderti conto di come funziona tutto questo – è un enorme evento che cambia la vita. 
Non sono riuscito a trovare le parole giuste, ma ho memorizzato la sequenza delle azioni. La sostanza è che questa reazione è tanto fisica quanto verbale, e hai bisogno di spazio. Uno spazio del genere, in senso zen. È proprio questo che deve essere spiegato, e poi è necessario allontanarsi fisicamente. Quando rimango in silenzio, posso elaborare la situazione dal punto di vista emotivo. Man mano che l'adrenalina raggiunge il cervello e ti mette in modalità 'attacca o fuggi', non riesci più a parlare: ora sei un idiota, un ingegnere da colpire, incapace di fornire una risposta adeguata o di fermare l'attacco, e l'attaccante può colpire liberamente ancora e ancora. Prima di tutto, devi tornare a essere te stesso, riprendere il controllo, uscire dalla modalità 'attacca o fuggi'.

Ecco, per questo è necessario uno spazio verbale. Semplicemente uno spazio libero. Se si deve parlare di qualcosa, si può dichiarare proprio questo e poi andare a cercare realmente il proprio "spazio": fare una passeggiata nel parco, chiudersi in doccia – non importa. L'importante è disconnettersi temporaneamente dalla situazione. Non appena ti disconnetti anche solo per qualche secondo, il controllo ritorna e inizi a pensare in modo lucido. "Bene, non sono un idiota, non faccio cose stupide, sono una persona piuttosto utile." Una volta che riesci a convincere te stesso, è tempo di passare alla fase successiva: capire cosa è successo. Sei stato attaccato, l'attacco è arrivato da dove non te lo aspettavi, è stata una trappola vile e sleale. Questo è male. Il passo successivo è capire perché l'aggressore ha fatto questo. Davvero, perché? Forse perché è arrabbiato lui stesso? Perché è arrabbiato? Ad esempio, perché ha fallito e non può accettare la responsabilità? In questo modo bisogna elaborare con attenzione l'intera situazione. Ma per farlo serve uno spazio di manovra, uno spazio verbale. Il primo passo è interrompere il contatto verbale. Allontanarsi dalla discussione verbale. Annullarla, andarsene il prima possibile. Se è una telefonata – basta riattaccare – è un'abilità che ho acquisito comunicando con la mia ex moglie. Se la conversazione non porta a nulla di buono, basta dire "arrivederci" e riattaccare. Dall'altra parte del telefono: "bla-bla-bla", tu rispondi: "ok, ciao!" e riattacchi. Interrompi semplicemente la conversazione. Cinque minuti dopo, quando la tua capacità di pensare lucidamente torna, ti sei un po' raffreddato, è possibile riflettere su cosa sia effettivamente successo e su cosa ci sarà dopo. E iniziare a formulare una risposta ponderata, non semplicemente reagire emotivamente. Per me, una svolta nella consapevolezza di sé è stata proprio il fatto che in caso di stress emotivo non riesco a parlare. Uscire da questo stato, riflettere e pianificare come rispondere e affrontare i problemi – questi sono i passaggi corretti nel caso in cui non si possa parlare. Il modo più semplice è fuggire dalla situazione in cui si manifesta lo stress emotivo e semplicemente smettere di partecipare a questo stress. Dopo di ciò, riacquisti la capacità di pensare, quando puoi pensare, hai la possibilità di parlare, e così via.

A proposito, in tribunale l'avvocato della parte avversa cerca di farlo con te – adesso è chiaro perché. Perché ha la possibilità di schiacciarti fino a un punto in cui non riuscirai nemmeno a pronunciare il tuo nome, ad esempio. Nel senso più diretto, non riuscirai a parlare. Se ti trovi in questa situazione e se sai di dover affrontare un ambiente dove si svolgono battaglie verbali, come in tribunale, è utile portare con te il tuo avvocato. L'avvocato si farà carico di tutelarti e di fermare l'attacco verbale, e lo farà in modo del tutto legittimo, permettendoti di recuperare il tuo spazio di zen perduto. Ad esempio, una volta o due ho dovuto chiamare la mia famiglia, e il giudice ha reagito in modo piuttosto amichevole, ma l'avvocato della parte opposta continuava a urlare e urlare verso di me, e non riuscivo nemmeno a inserire una parola. In tali casi, per me funziona meglio utilizzare un mediatore. Il mediatore interrompe tutta questa pressione che ti colpisce come un flusso incessante, e ti permette di ritrovare il tuo necessario spazio di zen, insieme al quale torna la capacità di parlare. È un'intera area di conoscenze, in cui c'è molto da studiare e molto da scoprire dentro di sé, e tutto questo si traduce in decisioni strategiche di alto livello, diverse per persone diverse. Alcuni non hanno problemi descritti sopra; di solito, non li hanno le persone che lavorano nelle vendite. Tutti questi individui che guadagnano da vivere con le parole – cantanti famosi, poeti, leader religiosi e politici – hanno sempre qualcosa da dire. Non hanno questi problemi, mentre io sì.

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

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

Fonte: habr.com

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