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

Bellissima intervista con Cliff Click, il padre della compilazione JIT in JavaClic sulla scogliera — CTO di Cratus (sensori IoT per il miglioramento dei processi), fondatore e co-fondatore 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)! È conosciuto soprattutto per il suo lavoro su C2 in Java (il Mare dei Nodi IR). Questo compilatore ha mostrato al mondo che JIT poteva produrre codice di alta qualità, che è stato uno dei fattori nell'emergere di Java come una delle principali piattaforme software moderne. Quindi Cliff ha aiutato Azul Systems a costruire un mainframe da 864 core con software Java puro che supportava le pause GC su un heap da 500 gigabyte entro 10 millisecondi. In generale, Cliff è riuscito a lavorare su tutti gli aspetti della JVM.

 
Questo habrapost è un'ottima intervista con Cliff. Parleremo dei seguenti argomenti:

  • Transizione alle ottimizzazioni di basso livello
  • Come eseguire un grande refactoring
  • Modello di costo
  • Formazione sull'ottimizzazione di basso livello
  • Esempi pratici di miglioramento delle prestazioni
  • Perché creare il tuo linguaggio di programmazione
  • Carriera di ingegnere delle prestazioni
  • Sfide tecniche
  • Qualcosa sull'allocazione dei registri e sui multi-core
  • La sfida più grande della vita

Le interviste sono condotte da:

  • Andrej Satarin da Amazon Web Services. Nella sua carriera, è riuscito a lavorare su progetti completamente diversi: ha testato il database distribuito NewSQL in Yandex, un sistema di rilevamento cloud in Kaspersky Lab, un gioco multiplayer in Mail.ru e un servizio per il calcolo dei prezzi dei cambi in Deutsche Bank. Interessato a testare backend e sistemi distribuiti su larga scala.
  • Vladimir Sitnikov da Netcracker. Dieci anni di lavoro sulle prestazioni e sulla scalabilità di NetCracker OS, il software utilizzato dagli operatori di telecomunicazioni per automatizzare i processi di gestione delle reti e degli apparati di rete. Interessato ai problemi di prestazioni di Java e Oracle Database. Autore di oltre una dozzina di miglioramenti delle prestazioni nel driver JDBC PostgreSQL ufficiale.

Transizione alle ottimizzazioni di basso livello

Andrew: Sei un grande nome nel mondo della compilazione JIT, di Java e del lavoro performativo in generale, giusto? 

scogliera: È come questo!

Andrew: Cominciamo con alcune domande generali sul lavoro performativo. Cosa ne pensi della scelta tra ottimizzazioni di alto e basso livello come lavorare a livello di CPU?

scogliera: Sì, qui è tutto semplice. Il codice più veloce è quello che non viene mai eseguito. Pertanto, è sempre necessario iniziare da un livello elevato, lavorare sugli algoritmi. Una notazione O migliore batterà una notazione O peggiore, a meno che non intervengano alcune costanti sufficientemente grandi. Le cose di basso livello vanno per ultime. In genere, se hai ottimizzato abbastanza bene il resto del tuo stack e sono rimaste ancora alcune cose interessanti, il livello è basso. Ma come iniziare da un livello elevato? Come fai a sapere che è stato svolto un lavoro di alto livello sufficiente? Beh... assolutamente no. Non esistono ricette già pronte. Devi capire il problema, decidere cosa fare (in modo da non fare passi inutili in futuro) e poi puoi scoprire il profiler, che può dire qualcosa di utile. Ad un certo punto, ti rendi conto di esserti sbarazzato di cose inutili ed è ora di fare qualche messa a punto di basso livello. Questo è sicuramente un tipo speciale di arte. Ci sono molte persone che fanno cose inutili, ma si muovono così velocemente che non hanno tempo di preoccuparsi della produttività. Ma questo finché la domanda non si pone senza mezzi termini. Di solito il 99% delle volte a nessuno interessa quello che faccio, fino al momento in cui sul percorso critico arriva una cosa importante di cui a nessuno importa. E qui tutti iniziano a tormentarti sul “perché non ha funzionato perfettamente fin dall’inizio”. In generale, c'è sempre qualcosa da migliorare nelle prestazioni. Ma il 99% delle volte non hai contatti! Stai solo cercando di far funzionare qualcosa e nel processo capisci cosa è importante. Non puoi mai sapere in anticipo che questo pezzo deve essere perfetto, quindi, in effetti, devi essere perfetto in tutto. Ma questo è impossibile e tu non lo fai. Ci sono sempre molte cose da sistemare, e questo è del tutto normale.

Come eseguire un grande refactoring

Andrew: Come lavori su una performance? Questo è un problema trasversale. Ad esempio, ti è mai capitato di dover lavorare su problemi che nascono dall'intersezione di molte funzionalità esistenti?

scogliera: Cerco di evitarlo. Se so che le prestazioni saranno un problema, ci penso prima di iniziare a scrivere codice, soprattutto con le strutture dati. Ma spesso tutto questo lo scopri molto tardi. E poi devi ricorrere a misure estreme e fare quello che io chiamo “riscrivere e conquistare”: devi prendere un pezzo abbastanza grande. Parte del codice dovrà ancora essere riscritta a causa di problemi di prestazioni o qualcos'altro. Qualunque sia la ragione per riscrivere il codice, è quasi sempre meglio riscrivere un pezzo più grande che uno più piccolo. In questo momento, tutti iniziano a tremare di paura: “oh mio Dio, non puoi toccare così tanto codice!” Ma in realtà questo approccio funziona quasi sempre molto meglio. Devi affrontare immediatamente un grosso problema, disegnarci un grande cerchio attorno e dire: riscriverò tutto all'interno del cerchio. Il bordo è molto più piccolo del contenuto al suo interno che deve essere sostituito. E se tale delimitazione dei confini ti consente di svolgere perfettamente il lavoro all'interno, le tue mani sono libere, fai quello che vuoi. Una volta compreso il problema, il processo di riscrittura sarà molto più semplice, quindi dai un grosso morso!
Allo stesso tempo, quando esegui una riscrittura di grandi dimensioni e ti rendi conto che le prestazioni saranno un problema, puoi immediatamente iniziare a preoccupartene. Questo di solito si traduce in cose semplici come “non copiare i dati, gestirli nel modo più semplice possibile, renderli piccoli”. Nelle riscritture di grandi dimensioni, esistono modi standard per migliorare le prestazioni. E ruotano quasi sempre attorno ai dati.

Modello di costo

Andrew: In uno dei podcast hai parlato di modelli di costo nel contesto della produttività. Puoi spiegare cosa intendevi con questo?

scogliera: Certamente. Sono nato in un'epoca in cui le prestazioni del processore erano estremamente importanti. E quest'epoca ritorna di nuovo: il destino non è privo di ironia. Ho iniziato a vivere ai tempi delle macchine a otto bit; il mio primo computer funzionava con 256 byte. Esattamente byte. Tutto era molto piccolo. Le istruzioni dovevano essere contate e, man mano che iniziavamo a salire nello stack dei linguaggi di programmazione, i linguaggi ne assumevano sempre di più. C'era l'Assembler, poi il Basic, poi il C, e il C si occupava di molti dettagli, come l'allocazione dei registri e la selezione delle istruzioni. Ma lì tutto era abbastanza chiaro, e se avessi creato un puntatore a un'istanza di una variabile, mi sarei caricato e il costo di questa istruzione sarebbe noto. L'hardware produce un certo numero di cicli macchina, quindi la velocità di esecuzione di cose diverse può essere calcolata semplicemente sommando tutte le istruzioni che eseguirai. Ogni confronto/test/diramazione/chiamata/caricamento/negozio potrebbe essere sommato e dire: questo è il tempo di esecuzione per te. Quando lavori per migliorare le prestazioni, presterai sicuramente attenzione a quali numeri corrispondono a piccoli cicli caldi. 
Ma non appena passi a Java, Python e cose simili, ti allontani molto rapidamente dall'hardware di basso livello. Qual è il costo di chiamare un getter in Java? Se JIT in HotSpot è corretto inline, verrà caricato, ma se non lo ha fatto, sarà una chiamata di funzione. Poiché la chiamata si trova in un ciclo attivo, sovrascriverà tutte le altre ottimizzazioni in quel ciclo. Pertanto, il costo reale sarà molto più alto. E perdi immediatamente la capacità di guardare un pezzo di codice e capire che dovremmo eseguirlo in termini di velocità di clock del processore, memoria e cache utilizzata. Tutto questo diventa interessante solo se ti immergi davvero nella performance.
Ora ci troviamo in una situazione in cui la velocità del processore non è quasi aumentata per un decennio. I vecchi tempi sono tornati! Non puoi più contare su buone prestazioni a thread singolo. Ma se all’improvviso entri nel mondo del calcolo parallelo, è incredibilmente difficile, tutti ti guardano come James Bond. Qui accelerazioni decuplicate di solito si verificano in luoghi in cui qualcuno ha sbagliato qualcosa. La concorrenza richiede molto lavoro. Per ottenere un aumento di XNUMX volte, è necessario comprendere il modello di costo. Cosa e quanto costa? E per fare questo è necessario capire come si adatta la linguetta all'hardware sottostante.
Martin Thompson ha scelto una parola fantastica per il suo blog Simpatia meccanica! È necessario capire cosa farà l'hardware, come lo farà esattamente e perché fa quello che fa in primo luogo. Utilizzando questo, è abbastanza semplice iniziare a contare le istruzioni e capire dove sta andando il tempo di esecuzione. Se non hai la formazione adeguata, stai solo cercando un gatto nero in una stanza buia. Vedo persone che ottimizzano continuamente le prestazioni e non hanno idea di cosa diavolo stanno facendo. Soffrono molto e non fanno molti progressi. E quando prendo lo stesso pezzo di codice, inserisco un paio di piccoli hack e ottengo una velocità cinque o dieci volte superiore, loro dicono: beh, non è giusto, sapevamo già che eri migliore. Sorprendente. Di cosa sto parlando... il modello di costo riguarda il tipo di codice che scrivi e la velocità con cui viene eseguito in media nel quadro generale.

Andrew: E come fai a tenere in testa un volume del genere? Questo si ottiene con più esperienza o? Da dove viene tale esperienza?

scogliera: Beh, non ho ottenuto la mia esperienza nel modo più semplice. Ho programmato in Assembly ai tempi in cui potevi comprendere ogni singola istruzione. Sembra stupido, ma da allora il set di istruzioni Z80 è sempre rimasto nella mia testa, nella mia memoria. Non ricordo i nomi delle persone dopo un minuto di conversazione, ma ricordo il codice scritto 40 anni fa. È divertente, sembra una sindrome"scienziato idiota'.

Formazione sull'ottimizzazione di basso livello

Andrew: C'è un modo più semplice per entrare?

scogliera: Sì e no. L'hardware che tutti utilizziamo non è cambiato molto nel tempo. Tutti utilizzano x86, ad eccezione degli smartphone Arm. Se non stai facendo una sorta di incorporamento hardcore, stai facendo la stessa cosa. Ok, il prossimo. Anche le istruzioni non sono cambiate da secoli. Devi andare a scrivere qualcosa in Assembly. Non molto, ma abbastanza per cominciare a capire. Stai sorridendo, ma sto parlando completamente sul serio. È necessario comprendere la corrispondenza tra linguaggio e hardware. Dopodiché devi andare a scrivere un po' e creare un piccolo compilatore di giocattoli per un piccolo linguaggio giocattolo. Simile a un giocattolo significa che deve essere realizzato in un lasso di tempo ragionevole. Può essere semplicissimo, ma deve generare istruzioni. L'atto di generare un'istruzione ti aiuterà a 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 verrà impressa nel cervello nel momento in cui verrà scritto il compilatore. Anche il compilatore più semplice. Dopodiché, puoi iniziare a guardare Java e il fatto che il suo abisso semantico è molto più profondo ed è molto più difficile costruire ponti su di esso. A Giava è molto più difficile capire se il nostro ponte è andato bene o male, cosa lo farà crollare e cosa no. Ma hai bisogno di una sorta di punto di partenza in cui guardi il codice e capisci: "sì, questo getter dovrebbe essere incorporato ogni volta". E poi si scopre che a volte ciò accade, tranne nella situazione in cui il metodo diventa troppo grande e il JIT inizia a integrare tutto. Il rendimento di tali luoghi può essere previsto immediatamente. Di solito i getter funzionano bene, ma poi guardi i grandi hot loop e ti rendi conto che ci sono alcune chiamate di funzione che fluttuano lì intorno che non sanno cosa stanno facendo. Questo è il problema con l'uso diffuso dei getter, il motivo per cui non sono inline è che non è chiaro se siano un getter. Se hai una base di codice molto piccola, puoi semplicemente ricordarla e poi dire: questo è un getter e questo è un setter. In una grande base di codice, ogni funzione vive la propria storia, che, in generale, non è nota a nessuno. Il profiler dice che abbiamo perso il 24% del tempo su qualche loop e per capire cosa sta facendo questo loop, dobbiamo guardare ogni funzione al suo interno. È impossibile capirlo senza studiarne la funzione, e questo rallenta seriamente il processo di comprensione. Ecco perché non uso getter e setter, ho raggiunto un nuovo livello!
Dove trovare il modello di costo? Beh, puoi leggere qualcosa, ovviamente... Ma penso che il modo migliore sia agire. Creare un piccolo compilatore sarà il modo migliore per comprendere il modello di costo e adattarlo alla tua testa. Un piccolo compilatore adatto alla programmazione di un forno a microonde è un compito per un principiante. Beh, voglio dire, se hai già capacità di programmazione, allora dovrebbe essere sufficiente. Tutte queste cose, come analizzare una stringa che hai come una sorta di espressione algebrica, estrarre da lì istruzioni per operazioni matematiche nell'ordine corretto, prendere i valori corretti dai registri: tutto questo viene fatto in una volta. E mentre lo fai, rimarrà impresso nel tuo cervello. Penso che tutti sappiano cosa fa un compilatore. E questo consentirà di comprendere il modello di costo.

Esempi pratici di miglioramento delle prestazioni

Andrew: A cos'altro dovresti prestare attenzione quando lavori sulla produttività?

scogliera: Strutture dati. A proposito, sì, è da molto tempo che non insegno in questi corsi... Scuola missilistica. È stato divertente, ma ha richiesto molto impegno, e ho anche una vita! OK. Quindi, in una delle lezioni più grandi e interessanti, "Dove va la tua performance", ho fatto un esempio agli studenti: due gigabyte e mezzo di dati fintech sono stati letti da un file CSV e poi hanno dovuto calcolare il numero di prodotti venduti . Dati di mercato dei tick regolari. Pacchetti UDP convertiti in formato testo dagli anni '70. Chicago Mercantile Exchange - ogni genere di cose come burro, mais, soia, cose del genere. Era necessario contare questi prodotti, il numero di transazioni, il volume medio di movimento di fondi e merci, ecc. È una matematica di trading piuttosto semplice: trova il codice del prodotto (che è 1-2 caratteri nella tabella hash), ottieni l'importo, aggiungilo a uno dei set commerciali, aggiungi volume, aggiungi valore e un paio di altre cose. Matematica molto semplice. L'implementazione del giocattolo è stata molto semplice: è tutto in un file, leggo il file e lo sposto, dividendo i singoli record in stringhe Java, cercando in esse le cose necessarie e sommandole secondo la matematica sopra descritta. E funziona a bassa velocità.

Con questo approccio è ovvio cosa sta succedendo e il calcolo parallelo non aiuta, giusto? Si scopre che è possibile ottenere un aumento quintuplicato delle prestazioni semplicemente scegliendo le giuste strutture dati. E questo sorprende anche i programmatori esperti! Nel mio caso particolare, il trucco stava nel non effettuare allocazioni di memoria in un hot loop. Bene, questa non è tutta la verità, ma in generale non dovresti evidenziare "una volta in X" quando X è abbastanza grande. Quando X è due gigabyte e mezzo, non dovresti allocare nulla “una volta per lettera”, o “una volta per riga”, o “una volta per campo”, qualcosa del genere. Qui è dove si trascorre il tempo. Come funziona? Immagina che io faccia una chiamata String.split() o BufferedReader.readLine(). Readline crea una stringa da un insieme di byte arrivati ​​sulla rete, una volta per ogni riga, per ciascuna delle centinaia di milioni di righe. Prendo questa riga, la analizzo e la butto via. Perché lo sto buttando via? beh, l'ho già elaborato, tutto qui. Quindi, per ogni byte letto da questi 2.7G, nella riga verranno scritti due caratteri, cioè già 5.4G, e non mi servono per nient'altro, quindi vengono buttati via. Se guardi la larghezza di banda della memoria, carichiamo 2.7 G che passano attraverso la memoria e il bus di memoria nel processore, quindi il doppio viene inviato alla linea che giace in memoria, e tutto questo viene sfilacciato quando viene creata ogni nuova linea. Ma devo leggerlo, l'hardware lo legge, anche se poi tutto si sfilaccia. E devo scriverlo perché ho creato una riga e le cache sono piene: la cache non può contenere 2.7G. Quindi, per ogni byte che leggo, leggo altri due byte e scrivo altri due byte, e alla fine hanno un rapporto 4:1: in questo rapporto stiamo sprecando larghezza di banda della memoria. E poi si scopre che se lo faccio String.split() – questa non è l'ultima volta che lo faccio, potrebbero esserci altri 6-7 campi all'interno. Quindi il classico codice di lettura CSV e quindi l'analisi delle stringhe si traduce in uno spreco di larghezza di banda della memoria di circa 14:1 rispetto a ciò che vorresti effettivamente avere. Se elimini queste selezioni, puoi ottenere una velocità quintuplicata.

E non è così difficile. Se guardi il codice dalla giusta angolazione, tutto diventa abbastanza semplice una volta che ti rendi conto del problema. Non dovresti smettere del tutto di allocare memoria: l'unico problema è che allochi qualcosa e questo muore immediatamente, e lungo il percorso brucia una risorsa importante, che in questo caso è la larghezza di banda della memoria. E tutto ciò si traduce in un calo della produttività. Su x86 di solito è necessario masterizzare attivamente i cicli del processore, ma qui hai bruciato tutta la memoria molto prima. La soluzione è ridurre la quantità di scarico. 
L'altra parte del problema è che se esegui il profiler quando la memoria si esaurisce, proprio nel momento in cui ciò accade, di solito stai aspettando che la cache ritorni perché è piena di spazzatura che hai appena prodotto, tutte quelle righe. Pertanto, ogni operazione di caricamento o archiviazione diventa lenta, poiché porta a errori di cache: l'intera cache è diventata lenta, in attesa che i rifiuti la lascino. Pertanto, il profiler mostrerà semplicemente un rumore casuale caldo spalmato lungo l'intero ciclo: non ci saranno istruzioni o punti caldi separati nel codice. Solo rumore. E se guardi i cicli GC, sono tutti di giovane generazione e super veloci: microsecondi o millisecondi al massimo. Dopotutto, tutta questa memoria muore all'istante. Assegni miliardi di gigabyte e lui li taglia, li taglia e li taglia ancora. Tutto questo avviene molto rapidamente. Si scopre che ci sono cicli GC economici, rumore caldo lungo l'intero ciclo, ma vogliamo ottenere un aumento di velocità 5 volte. In questo momento, qualcosa dovrebbe chiudersi nella tua testa e suonare: "perché è questo?!" L'overflow della striscia di memoria non viene visualizzato nel debugger classico; è necessario eseguire il debugger del contatore delle prestazioni hardware e vederlo direttamente. Ma questo non può essere sospettato direttamente da questi tre sintomi. Il terzo sintomo è quando guardi cosa evidenzi, chiedi al profiler e lui risponde: “Hai fatto un miliardo di righe, ma il GC ha funzionato gratis”. Non appena ciò accade, ti rendi conto di aver creato troppi oggetti e bruciato l'intero percorso della memoria. C’è un modo per capirlo, ma non è ovvio. 

Il problema è nella struttura dei dati: la struttura nuda che sta alla base di tutto ciò che accade, è troppo grande, è 2.7G su disco, quindi fare una copia di questa cosa è molto indesiderabile: vuoi caricarla immediatamente dal buffer di byte della rete nei registri, in modo da non leggere-scrivere sulla riga avanti e indietro cinque volte. Sfortunatamente, Java non fornisce tale libreria come parte del JDK per impostazione predefinita. Ma questo è banale, vero? Essenzialmente, si tratta di 5-10 righe di codice che verranno utilizzate per implementare il proprio caricatore di stringhe bufferizzato, che ripete il comportamento della classe string, pur essendo un wrapper attorno al buffer di byte sottostante. Di conseguenza, si scopre che stai lavorando quasi come con le stringhe, ma in realtà i puntatori al buffer si spostano lì e i byte grezzi non vengono copiati da nessuna parte, e quindi gli stessi buffer vengono riutilizzati più e più volte, e il sistema operativo è felice di farsi carico delle cose per cui è stato progettato, come il doppio buffer nascosto di questi buffer di byte, e non stai più macinando un flusso infinito di dati non necessari. A proposito, capisci che quando si lavora con GC è garantito che ogni allocazione di memoria non sarà visibile al processore dopo l'ultimo ciclo GC? Pertanto, tutto ciò non può essere nella cache e quindi si verifica un errore garantito al 100%. Quando si lavora con un puntatore, su x86, la sottrazione di un registro dalla memoria richiede 1-2 cicli di clock e non appena ciò accade si paga, si paga, si paga, perché la memoria è tutta accesa NOVE cache – e questo è il costo dell’allocazione della memoria. Valore reale.

In altre parole, le strutture dei dati sono la cosa più difficile da cambiare. E una volta che ti rendi conto di aver scelto la struttura dati sbagliata che in seguito ridurrà le prestazioni, di solito c'è molto lavoro da fare, ma se non lo fai, le cose peggioreranno. Prima di tutto, devi pensare alle strutture dei dati, questo è importante. Il costo principale qui ricade sulle grosse strutture dati, che stanno iniziando ad essere utilizzate nello stile di “Ho copiato la struttura dati X nella struttura dati Y perché mi piace di più la forma di Y”. Ma l'operazione di copia (che sembra economica) in realtà spreca larghezza di banda della memoria ed è lì che viene sepolto tutto il tempo di esecuzione sprecato. Se ho una stringa gigante di JSON e voglio trasformarla in un albero DOM strutturato di POJO o qualcosa del genere, l'operazione di analisi di quella stringa e creazione del POJO, e quindi l'accesso successivo al POJO, comporterà costi inutili: è non economico. Tranne se corri attorno ai POJO molto più spesso di quanto corri attorno a una stringa. A prima vista, puoi invece provare a decrittografare la stringa ed estrarre solo ciò che ti serve da lì, senza trasformarla in alcun POJO. Se tutto ciò accade su un percorso da cui è richiesta la massima prestazione, nessun POJO per te, devi in ​​qualche modo scavare direttamente nella linea.

Perché creare il tuo linguaggio di programmazione

Andrew: Hai detto che per comprendere il modello di costo è necessario scrivere il proprio linguaggio...

scogliera: Non un linguaggio, ma un compilatore. Un linguaggio e un compilatore sono due cose diverse. La differenza più importante è nella tua testa. 

Andrew: A proposito, per quanto ne so, stai sperimentando la creazione dei tuoi linguaggi. Per quello?

scogliera: Perché io posso! Sono semi-pensionato, quindi questo è il mio hobby. Ho implementato le lingue di altre persone per tutta la vita. Ho anche lavorato molto sul mio stile di programmazione. E anche perché vedo problemi in altre lingue. Vedo che ci sono modi migliori per fare cose familiari. E li userei. Sono solo stanco di vedere problemi in me stesso, in Java, in Python, in qualsiasi altro linguaggio. Ora scrivo in React Native, JavaScript ed Elm come hobby che non riguarda la pensione, ma il lavoro attivo. Scrivo anche in Python e, molto probabilmente, continuerò a lavorare sull'apprendimento automatico per i backend Java. Esistono molte lingue popolari e tutte hanno caratteristiche interessanti. Ognuno è bravo a modo suo e puoi provare a riunire tutte queste funzionalità. Quindi sto studiando le cose che mi interessano, il comportamento del linguaggio, cercando di trovare una semantica ragionevole. E finora ci sto riuscendo! Al momento sto lottando con la semantica della memoria, perché voglio averla come in C e Java e ottenere un modello di memoria forte e una semantica della memoria per carichi e archivi. Allo stesso tempo, ottieni l'inferenza automatica del tipo come in Haskell. Qui, sto cercando di mescolare l'inferenza di tipo simile a Haskell con il lavoro di memoria sia in C che in Java. Questo è quello che ho fatto negli ultimi 2-3 mesi, per esempio.

Andrew: Se costruisci un linguaggio che prende aspetti migliori da altri linguaggi, pensi che qualcuno farà il contrario: prenderà le tue idee e le userà?

scogliera: Questo è esattamente il modo in cui appaiono le nuove lingue! Perché Java è simile a C? Perché C aveva una buona sintassi che tutti capivano e Java si è ispirato a questa sintassi, aggiungendo l'indipendenza dai tipi, il controllo dei limiti dell'array, GC e hanno anche migliorato alcune cose da C. Hanno aggiunto il proprio. Ma sono stati molto ispirati, giusto? Tutti stanno sulle spalle dei giganti che sono venuti prima di te: è così che si fa il progresso.

Andrew: A quanto ho capito, la tua lingua sarà al sicuro dalla memoria. Hai pensato di implementare qualcosa come un controllo prestiti da Rust? Lo hai guardato, cosa pensi di lui?

scogliera: Beh, scrivo C da anni, con tutto questo malloc e gratis, e gestendo manualmente la durata. Sapete, il 90-95% della vita controllata manualmente ha la stessa struttura. Ed è molto, molto doloroso farlo manualmente. Vorrei che il compilatore ti dicesse semplicemente cosa sta succedendo lì e cosa hai ottenuto con le tue azioni. Per alcune cose, il controllo prestiti lo fa immediatamente. E dovrebbe visualizzare automaticamente le informazioni, comprendere tutto e non caricarmi nemmeno di presentare questa comprensione. Deve eseguire almeno un'analisi di fuga locale e, solo se fallisce, è necessario aggiungere annotazioni di tipo che descrivano la durata - e un tale schema è molto più complesso di un controllo di prestito, o addirittura di qualsiasi controllo di memoria esistente. La scelta tra "va tutto bene" e "non capisco niente" - no, deve esserci qualcosa di meglio. 
Quindi, come persona che ha scritto molto codice in C, penso che avere il supporto per il controllo automatico della durata sia la cosa più importante. Sono anche stufo di quanta memoria Java utilizza e la lamentela principale è il GC. Quando allochi memoria in Java, non recupererai la memoria locale nell'ultimo ciclo GC. Questo non è il caso dei linguaggi con una gestione della memoria più precisa. Se chiami malloc, ottieni immediatamente la memoria che di solito veniva appena utilizzata. Di solito fai alcune cose temporanee con la memoria e le restituisci immediatamente. E ritorna immediatamente nel pool di malloc e il successivo ciclo di malloc lo estrae di nuovo. Pertanto, l'utilizzo effettivo della memoria è ridotto all'insieme di oggetti viventi in un dato momento, più le perdite. E se tutto non viene trapelato in modo del tutto indecente, la maggior parte della memoria finisce nelle cache e nel processore e funziona rapidamente. Ma richiede molta gestione manuale della memoria con malloc e chiamate gratuite nell'ordine giusto, nel posto giusto. Rust è in grado di gestirlo correttamente da solo, e in molti casi offre prestazioni ancora migliori, poiché il consumo di memoria è limitato solo al calcolo corrente, invece di attendere il successivo ciclo GC per liberare memoria. Di conseguenza, abbiamo ottenuto un modo molto interessante per migliorare le prestazioni. E abbastanza potente: voglio dire, ho fatto cose del genere durante l'elaborazione dei dati per il fintech, e questo mi ha permesso di ottenere un'accelerazione di circa cinque volte. Si tratta di un notevole vantaggio, soprattutto in un mondo in cui i processori non stanno diventando più veloci e stiamo ancora aspettando miglioramenti.

Carriera di ingegnere delle prestazioni

Andrew: Vorrei anche chiedere in giro riguardo alle carriere in generale. Sei salito alla ribalta con il tuo lavoro JIT presso HotSpot e poi ti sei trasferito ad Azul, che è anche una società JVM. Ma stavamo già lavorando più sull’hardware che sul software. E poi improvvisamente sono passati ai Big Data e al Machine Learning, e poi al rilevamento delle frodi. Come è successo? Si tratta di aree di sviluppo molto diverse.

scogliera: Programmavo da parecchio tempo e sono riuscito a frequentare molti corsi diversi. E quando la gente dice: “oh, sei tu quello che ha fatto JIT per Java!”, è sempre divertente. Ma prima stavo lavorando su un clone di PostScript, il linguaggio che una volta Apple utilizzava per le sue stampanti laser. E prima ancora ho implementato il linguaggio Forth. Penso che il tema comune per me sia lo sviluppo degli strumenti. Per tutta la vita ho creato strumenti con cui altre persone scrivono i loro fantastici programmi. Ma mi sono occupato anche dello sviluppo di sistemi operativi, driver, debugger a livello kernel, linguaggi per lo sviluppo del sistema operativo, che all'inizio era banale, ma col tempo è diventato sempre più complesso. Ma il tema principale resta lo sviluppo degli strumenti. Gran parte della mia vita è passata tra Azul e Sun, e riguardava Java. Ma quando mi sono interessato ai Big Data e al Machine Learning, mi sono rimesso il cappello alla moda e ho detto: "Oh, ora abbiamo un problema non banale, e ci sono molte cose interessanti in corso e persone che fanno cose". Questo è un ottimo percorso di sviluppo da intraprendere.

Sì, adoro davvero il calcolo distribuito. Il mio primo lavoro è stato da studente in C, su un progetto pubblicitario. Si trattava di elaborazione distribuita su chip Zilog Z80 che raccoglievano dati per l'OCR analogico, prodotti da un vero analizzatore analogico. Era un argomento interessante e completamente folle. Ma c'erano dei problemi, qualche parte non veniva riconosciuta correttamente, quindi dovevi togliere una foto e mostrarla a una persona che sapeva già leggere con i suoi occhi e riportare quello che diceva, e quindi c'erano lavori con dati, e questi lavori avevano la loro lingua. C'era un backend che elaborava tutto questo - gli Z80 funzionavano in parallelo con i terminali vt100 in esecuzione - uno per persona, e c'era un modello di programmazione parallela sullo Z80. Qualche pezzo di memoria comune condiviso da tutti gli Z80 all'interno di una configurazione a stella; Anche il backplane era condiviso e metà della RAM era condivisa all'interno della rete e l'altra metà era privata o veniva destinata a qualcos'altro. Un sistema distribuito parallelo significativamente complesso con memoria condivisa... semi-condivisa. Quando è successo... non riesco nemmeno a ricordarlo, da qualche parte a metà degli anni '80. Molto tempo fa. 
Sì, supponiamo che 30 anni siano un periodo piuttosto lungo. I problemi legati al calcolo distribuito esistono da molto tempo; le persone sono da tempo in guerra con La leggenda di Beowulf-cluster. Tali cluster assomigliano a... Ad esempio: c'è Ethernet e il tuo x86 veloce è connesso a questa Ethernet, e ora vuoi ottenere una falsa memoria condivisa, perché nessuno poteva fare codifica informatica distribuita allora, era troppo difficile e quindi lì era una falsa memoria condivisa con pagine di memoria di protezione su x86, e se scrivevi a questa pagina, allora dicevamo agli altri processori che se accedevano alla stessa memoria condivisa, avrebbe dovuto essere caricata da te, e quindi qualcosa come un protocollo per supportare è apparsa la coerenza della cache e il software per questo. Concetto interessante. Il vero problema, ovviamente, era un altro. Tutto ciò ha funzionato, ma si sono verificati rapidamente problemi di prestazioni, perché nessuno comprendeva i modelli di prestazioni a un livello sufficientemente buono: quali modelli di accesso alla memoria erano presenti, come assicurarsi che i nodi non si pingassero all'infinito e così via.

Ciò che mi è venuto in mente in H2O è che sono gli sviluppatori stessi a essere responsabili di determinare dove è nascosto il parallelismo e dove no. Ho ideato un modello di codifica che rendesse la scrittura di codice ad alte prestazioni facile e semplice. Ma scrivere codice lento è difficile, sembrerà brutto. Devi provare seriamente a scrivere codice lento, dovrai utilizzare metodi non standard. Il codice di frenatura è visibile a prima vista. Di conseguenza, di solito scrivi codice che gira velocemente, ma devi capire cosa fare in caso di memoria condivisa. Tutto questo è legato a array di grandi dimensioni e il comportamento è simile a quello degli array di grandi dimensioni non volatili in Java parallelo. Voglio dire, immagina che due thread scrivano su un array parallelo, uno di loro vince e l'altro, di conseguenza, perde, e non sai quale è quale. Se non sono volatili, l'ordine può essere quello che desideri e funziona davvero bene. Le persone si preoccupano davvero dell'ordine delle operazioni, mettono i volatili nei posti giusti e si aspettano problemi di prestazioni legati alla memoria nei posti giusti. Altrimenti, scriverebbero semplicemente il codice sotto forma di cicli da 1 a N, dove N è qualche trilione, nella speranza che tutti i casi complessi diventino automaticamente paralleli - e lì non funziona. Ma in H2O questo non è né Java né Scala; puoi considerarlo “Java meno meno” se vuoi. Questo è uno stile di programmazione molto chiaro ed è simile alla scrittura di semplice codice C o Java con loop e array. Ma allo stesso tempo, la memoria può essere elaborata in terabyte. Uso ancora H2O. Lo uso di tanto in tanto in diversi progetti ed è ancora il più veloce, decine di volte più veloce dei suoi concorrenti. Se stai facendo Big Data con dati colonnari, è molto difficile battere H2O.

Sfide tecniche

Andrew: Qual è stata la sfida più grande in tutta la tua carriera?

scogliera: Stiamo discutendo la parte tecnica o non tecnica del problema? Direi che le sfide più grandi non sono quelle tecniche. 
Per quanto riguarda le sfide tecniche. Li ho semplicemente sconfitti. Non so nemmeno quale sia stato il più grande, ma ce ne sono stati alcuni piuttosto interessanti che hanno richiesto un po' di tempo e fatica mentale. Quando sono andato alla Sun, ero sicuro che avrei realizzato un compilatore veloce e un gruppo di anziani ha detto in risposta che non ci sarei mai riuscito. Ma ho seguito questo percorso, ho scritto un compilatore fino all'allocatore di registri ed è stato abbastanza veloce. Era veloce quanto il moderno C1, ma all'epoca l'allocatore era molto più lento e, col senno di poi, si trattava di un grosso problema di struttura dei dati. Ne avevo bisogno per scrivere un allocatore di registri grafico e non capivo il dilemma tra espressività del codice e velocità, che esisteva in quell'epoca ed era molto importante. Si è scoperto che la struttura dei dati di solito supera la dimensione della cache sugli x86 di quel tempo e quindi, se inizialmente supponevo che l'allocatore di registri avrebbe funzionato per il 5-10 percento del tempo di jitter totale, in realtà si è rivelato essere 50 percento.

Col passare del tempo, il compilatore è diventato più pulito ed efficiente, ha smesso di generare codice terribile in più casi e le prestazioni hanno cominciato ad assomigliare sempre più a ciò che produce un compilatore C. A meno che, ovviamente, non si scriva qualche schifezza che nemmeno il C accelera. . Se scrivi codice come C, otterrai prestazioni come C in più casi. E più andavi avanti, più spesso ottenevi un codice che coincideva asintoticamente con il livello C, l'allocatore di registri cominciava a sembrare qualcosa di completo... indipendentemente dal fatto che il tuo codice funzionasse velocemente o lentamente. Ho continuato a lavorare sull'allocatore per fargli effettuare selezioni migliori. È diventato sempre più lento, ma ha dato prestazioni sempre migliori nei casi in cui nessun altro poteva farcela. Potrei tuffarmi in un allocatore di registri, seppellire lì un mese di lavoro e improvvisamente l'intero codice inizierebbe ad essere eseguito il 5% più velocemente. Questo è successo di volta in volta e il registratore di cassa è diventato una sorta di opera d'arte: tutti lo hanno amato o odiato, e le persone dell'accademia hanno posto domande sull'argomento "perché è fatto tutto in questo modo", perché no scansione della linea, e qual è la differenza. La risposta è sempre la stessa: un allocatore basato sulla colorazione del grafico più un lavoro molto attento con il codice del buffer equivale a un'arma di vittoria, la migliore combinazione che nessuno può sconfiggere. E questa è una cosa piuttosto non ovvia. Tutto il resto di ciò che fa il compilatore sono cose abbastanza ben studiate, sebbene siano state anche portate al livello dell'arte. Ho sempre fatto cose che avrebbero dovuto trasformare il compilatore in un'opera d'arte. Ma niente di tutto questo era straordinario, fatta eccezione per l'allocatore del registro. Il trucco è stare attenti tagliare sotto carico e, se ciò accade (posso spiegarlo più in dettaglio se interessato), significa che è possibile eseguire l'inline in modo più aggressivo, senza il rischio di cadere in un intoppo nel programma delle prestazioni. A quei tempi, c'erano un mucchio di compilatori su vasta scala, ricoperti di ninnoli e fischietti, dotati di allocatori di registri, ma nessun altro poteva farlo.

Il problema è che se aggiungi metodi soggetti a inlining, aumentando e aumentando l'area di inlining, l'insieme dei valori utilizzati supera immediatamente il numero di registri e devi tagliarli. Il livello critico di solito arriva quando l'allocatore si arrende e un buon candidato per una fuoriuscita vale un altro, venderai alcune cose generalmente selvagge. Il valore dell'inlining qui è che perdi parte del sovraccarico, sovraccarico per le chiamate e il salvataggio, puoi vedere i valori all'interno e ottimizzarli ulteriormente. Il costo dell'inlining è che si formano un gran numero di valori live e se il tuo allocatore di registro brucia più del necessario, perdi immediatamente. Pertanto, la maggior parte degli allocatori ha un problema: quando l’inlining supera una certa linea, tutto nel mondo inizia a ridursi e la produttività può essere buttata nel cesso. Coloro che implementano il compilatore aggiungono alcune euristiche: ad esempio, smettere di inlining, iniziando con una dimensione sufficientemente grande, poiché le allocazioni rovinerebbero tutto. Ecco come si forma un nodo nel grafico delle prestazioni: tu in linea, in linea, le prestazioni crescono lentamente - e poi boom! – cade come un martinetto veloce perché hai allineato troppo. Ecco come funzionava tutto prima dell'avvento di Java. Java richiede molto più inlining, quindi ho dovuto rendere il mio allocatore molto più aggressivo in modo che si livellasse piuttosto che bloccarsi, e se lo inlinei troppo, inizia a fuoriuscire, ma poi arriva comunque il momento di "niente più fuoriuscite". Questa è un'osservazione interessante e mi è venuta dal nulla, non ovvia, ma ha dato i suoi frutti. Ho intrapreso l'inlining aggressivo e mi ha portato in luoghi in cui le prestazioni Java e C lavorano fianco a fianco. Sono molto simili: posso scrivere codice Java che è significativamente più veloce del codice C e cose del genere, ma in media, nel quadro generale delle cose, sono più o meno paragonabili. Penso che parte di questo merito sia l'allocatore del registro, che mi permette di inline nel modo più stupido possibile. Metto semplicemente in linea tutto ciò che vedo. La domanda qui è se l'allocatore funziona bene, se il risultato è un codice che funziona in modo intelligente. Questa è stata una grande sfida: capire tutto questo e farlo funzionare.

Qualcosa sull'allocazione dei registri e sui multi-core

Vladimir: Problemi come l'assegnazione dei registri sembrano una sorta di argomento eterno e senza fine. Mi chiedo se sia mai esistita un'idea che sembrava promettente e poi è fallita nella pratica?

scogliera: Certamente! L'allocazione dei registri è un'area in cui si tenta di trovare alcune euristiche per risolvere un problema NP-completo. E non puoi mai raggiungere una soluzione perfetta, giusto? Questo è semplicemente impossibile. Guarda, la compilazione Ahead of Time: funziona anche male. La conversazione qui riguarda alcuni casi medi. Per quanto riguarda le prestazioni tipiche, puoi andare a misurare qualcosa che ritieni sia una buona prestazione tipica: dopo tutto, stai lavorando per migliorarla! L'allocazione dei registri è un argomento incentrato sulle prestazioni. Una volta che hai il primo prototipo, funziona e dipinge ciò che serve, inizia il lavoro di performance. Devi imparare a misurare bene. Perché è importante? Se disponi di dati chiari, puoi esaminare diverse aree e vedere: sì, qui ha aiutato, ma è lì che tutto si è rotto! Vengono fuori alcune buone idee, aggiungi nuove euristiche e all'improvviso tutto inizia a funzionare in media un po' meglio. Oppure non parte. Ho avuto un sacco di casi in cui stavamo lottando per la performance del XNUMX% che differenziava il nostro sviluppo dal precedente allocatore. E ogni volta sembra così: da qualche parte vinci, da qualche parte perdi. Se disponi di buoni strumenti di analisi delle prestazioni, puoi trovare le idee perdenti e capire perché falliscono. Forse vale la pena lasciare tutto così com'è, o forse adottare un approccio più serio alla messa a punto, o uscire e sistemare qualcos'altro. È un sacco di cose! Ho realizzato questo fantastico trucco, ma mi serve anche questo, e questo, e questo - e la loro combinazione totale dà alcuni miglioramenti. E i solitari possono fallire. Questa è la natura del lavoro prestazionale su problemi NP-completi.

Vladimir: Si ha la sensazione che cose come la verniciatura dei ripartitori siano un problema già risolto. Bene, è deciso per te, a giudicare da quello che dici, quindi ne vale la pena...

scogliera: Non è risolto come tale. Sei tu che devi trasformarlo in “risolto”. Ci sono problemi difficili e devono essere risolti. Fatto ciò, è il momento di lavorare sulla produttività. Devi affrontare questo lavoro di conseguenza: eseguire benchmark, raccogliere metriche, spiegare situazioni in cui, quando sei tornato a una versione precedente, il tuo vecchio hack ha iniziato a funzionare di nuovo (o viceversa, si è fermato). E non arrenderti finché non raggiungi qualcosa. Come ho già detto, ci sono idee interessanti che non hanno funzionato, ma nel campo della distribuzione dei registri di idee ce ne sono approssimativamente infinite. Puoi, ad esempio, leggere pubblicazioni scientifiche. Anche se ora quest'area ha iniziato a muoversi molto più lentamente ed è diventata più chiara rispetto alla sua giovinezza. Tuttavia, ci sono innumerevoli persone che lavorano in questo campo e tutte le loro idee meritano di essere provate, sono tutte in attesa dietro le quinte. E non puoi dire quanto siano buoni se non li provi. Quanto bene si integrano con tutto il resto nel tuo allocatore, perché un allocatore fa molte cose e alcune idee nel tuo allocatore specifico non funzioneranno, ma in un altro allocatore funzioneranno facilmente. Il modo principale per vincere per l’allocatore è tirare le cose lente fuori dal percorso principale e forzarle a dividersi lungo i confini dei percorsi lenti. Quindi, se vuoi eseguire un GC, prendere il percorso lento, deottimizzare, lanciare un'eccezione, tutte quelle cose, sai che queste cose sono relativamente rare. E sono davvero rari, ho controllato. Fai un lavoro extra e questo rimuove molte restrizioni su questi percorsi lenti, ma non ha molta importanza perché sono lenti e raramente percorribili. Ad esempio, un puntatore nullo: non succede mai, giusto? È necessario disporre di diversi percorsi per cose diverse, ma non dovrebbero interferire con quello principale. 

Vladimir: Cosa pensi dei multi-core, quando ci sono migliaia di core contemporaneamente? E' una cosa utile?

scogliera: Il successo della GPU dimostra che è piuttosto utile!

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

scogliera: Beh, quello era il modello di business di Azul. La risposta arrivò in un’epoca in cui le persone amavano davvero le prestazioni prevedibili. Allora era difficile scrivere codice parallelo. Il modello di codifica H2O è altamente scalabile, ma non è un modello di uso generale. Forse un po' più generale rispetto a quando si utilizza una GPU. Stiamo parlando della complessità di sviluppare una cosa del genere o della complessità di usarla? Azul, ad esempio, mi ha insegnato una lezione interessante, ma non ovvia: le piccole cache sono normali. 

La sfida più grande della vita

Vladimir: E le sfide non tecniche?

scogliera: La sfida più grande non era essere... gentile e gentile con le persone. E di conseguenza, mi sono trovato costantemente in situazioni estremamente conflittuali. Quelli in cui sapevo che le cose stavano andando male, ma non sapevo come andare avanti con quei problemi e non riuscivo a gestirli. Molti problemi a lungo termine, durati decenni, sorsero in questo modo. Il fatto che Java abbia compilatori C1 e C2 è una diretta conseguenza di ciò. Anche il fatto che in Java non sia stata effettuata la compilazione multilivello per dieci anni consecutivi è una conseguenza diretta. È ovvio che avevamo bisogno di un sistema del genere, ma non è chiaro il motivo per cui non esisteva. Ho avuto problemi con un ingegnere... o con un gruppo di ingegneri. C'era una volta, quando ho iniziato a lavorare alla Sun, ero... Ok, non solo allora, generalmente ho sempre la mia opinione su tutto. E pensavo che fosse vero che potevi semplicemente prendere questa tua verità e raccontarla a testa alta. Soprattutto perché avevo sorprendentemente ragione per la maggior parte del tempo. E se non ti piace questo approccio... soprattutto se hai chiaramente torto e fai delle sciocchezze... In generale, poche persone potrebbero tollerare questa forma di comunicazione. Anche se alcuni potrebbero, come me. Ho costruito tutta la mia vita su principi meritocratici. Se mi mostri qualcosa di sbagliato, mi giro subito e dico: hai detto una sciocchezza. Allo stesso tempo, ovviamente, mi scuso e tutto il resto, prenderò nota dei meriti, se presenti, e intraprenderò altre azioni corrette. D'altra parte, ho sorprendentemente ragione riguardo ad una percentuale incredibilmente elevata del tempo totale. E non funziona molto bene nei rapporti con le persone. Non sto cercando di essere gentile, ma sto ponendo la domanda senza mezzi termini. "Questo non funzionerà mai, perché uno, due e tre." E loro dicevano: "Oh!" Ci sono state altre conseguenze che probabilmente era meglio ignorare: ad esempio, quelle che hanno portato al divorzio da mia moglie e ai successivi dieci anni di depressione.

La sfida è una lotta con le persone, con la loro percezione di cosa si può o non si può fare, cosa è importante e cosa no. C'erano molte sfide riguardo allo stile di codifica. Scrivo ancora molto codice e a quei tempi dovevo persino rallentare perché svolgevo troppe attività parallele e le eseguivo male, invece di concentrarmi su una. Guardando indietro, ho scritto metà del codice per il comando JIT Java, il comando C2. Il successivo programmatore più veloce scriveva lentamente la metà, il successivo metà più lento, e si trattava di un declino esponenziale. La settima persona della fila era molto, molto lenta: succede sempre! Ho toccato un sacco di codice. Ho guardato chi ha scritto cosa, senza eccezioni, ho fissato il loro codice, li ho rivisti ciascuno e ho continuato a scrivere più io stesso che chiunque di loro. Questo approccio non funziona molto bene con le persone. Ad alcune persone questo non piace. E quando non riescono a gestirlo, iniziano tutti i tipi di lamentele. Ad esempio, una volta mi è stato detto di smettere di scrivere codice perché stavo scrivendo troppo codice e stavo mettendo a repentaglio il team, e mi sembrava tutto uno scherzo: amico, se il resto del team scompare e io continuo a scrivere codice, tu perderò solo metà delle squadre. D'altra parte, se continuo a scrivere codice e perdi metà del team, sembra una pessima gestione. Non ci ho mai pensato davvero, non ne ho mai parlato, ma era ancora da qualche parte nella mia testa. Il pensiero girava nella mia mente: "Mi state prendendo in giro?" Quindi il problema più grande ero io e i miei rapporti con le persone. Ora mi capisco molto meglio, sono stato a lungo caposquadra di programmatori e ora dico direttamente alla gente: sai, sono quello che sono e dovrai occuparti di me - va bene se sto in piedi Qui? E quando hanno iniziato ad affrontarlo, tutto ha funzionato. In effetti, non sono né buono né cattivo, non ho cattive intenzioni o aspirazioni egoistiche, è solo la mia essenza e devo conviverci in qualche modo.

Andrew: Proprio di recente tutti hanno iniziato a parlare di autoconsapevolezza per gli introversi e di competenze trasversali in generale. Cosa puoi dire a riguardo?

scogliera: Sì, questa è stata l'intuizione e la lezione che ho imparato dal divorzio da mia moglie. Ciò che ho imparato dal divorzio è stato capire me stesso. È così che ho cominciato a capire le altre persone. Comprendi come funziona questa interazione. Ciò ha portato a scoperte una dopo l'altra. C’era la consapevolezza di chi sono e cosa rappresento. Cosa sto facendo: o sono preoccupato per il compito, o evito il conflitto, o qualcos'altro - e questo livello di autoconsapevolezza aiuta davvero a mantenere il controllo. Dopodiché tutto diventa molto più semplice. Una cosa che ho scoperto non solo in me stesso, ma anche in altri programmatori è l'incapacità di verbalizzare i pensieri quando si è in uno stato di stress emotivo. Ad esempio, sei seduto lì a programmare, in uno stato di flusso, e poi corrono da te e iniziano a urlare isterici che qualcosa è rotto e che ora verranno prese misure estreme contro di te. E non puoi dire una parola perché sei in uno stato di stress emotivo. La conoscenza acquisita ti consente di prepararti per questo momento, sopravvivere e passare a un piano di ritiro, dopodiché puoi fare qualcosa. Quindi sì, quando inizi a capire come funziona il tutto, è un evento che cambia enormemente la vita. 
Io stesso non sono riuscito a trovare le parole giuste, ma ho ricordato la sequenza delle azioni. Il punto è che questa reazione è tanto fisica quanto verbale e hai bisogno di spazio. Tale spazio, nel senso Zen. Questo è esattamente ciò che deve essere spiegato, e poi farsi immediatamente da parte - allontanarsi puramente fisicamente. Quando rimango in silenzio verbalmente, posso elaborare la situazione emotivamente. Non appena l'adrenalina raggiunge il tuo cervello, ti mette in modalità lotta o fuga, non puoi più dire nulla, no - ora sei un idiota, un ingegnere frustante, incapace di una risposta decente o addirittura di fermare l'attacco, e l'aggressore è libero attaccare ancora e ancora. Devi prima diventare di nuovo te stesso, riprendere il controllo, uscire dalla modalità “lotta o fuga”.

E per questo abbiamo bisogno dello spazio verbale. Solo spazio libero. Se dici qualcosa, allora puoi dire esattamente quello, e poi andare a trovare davvero "spazio" per te stesso: vai a fare una passeggiata nel parco, chiuditi nella doccia - non importa. La cosa principale è disconnettersi temporaneamente da quella situazione. Non appena ti spegni per almeno qualche secondo, il controllo ritorna, inizi a pensare in modo sobrio. "Okay, non sono una specie di idiota, non faccio cose stupide, sono una persona piuttosto utile." Una volta che sei riuscito a convincerti, è il momento di passare alla fase successiva: capire cosa è successo. Sei stato aggredito, l’attacco è arrivato da dove non te lo aspettavi, è stato un agguato disonesto, vile. Questo non va bene. Il passo successivo è capire perché l’aggressore ne avesse bisogno. Davvero perchè? Forse perché lui stesso è furioso? Perchè è arrabbiato? Ad esempio, perché ha commesso un errore e non può assumersi la responsabilità? Questo è il modo di gestire attentamente l’intera situazione. Ma questo richiede spazio di manovra, spazio verbale. Il primo passo è interrompere il contatto verbale. Evitare discussioni con le parole. Annullatelo, andatevene il più velocemente possibile. Se si tratta di una conversazione telefonica, riaggancia e basta: questa è un'abilità che ho imparato comunicando con la mia ex moglie. Se la conversazione non sta andando bene, dì semplicemente "arrivederci" e riattacca. Dall’altra parte del telefono: “blah blah blah”, tu rispondi: “sì, ciao!” e riattaccare. Concludi semplicemente la conversazione. Cinque minuti dopo, quando ti ritorna la capacità di pensare in modo sensato, ti sei calmato un po', diventa possibile pensare a tutto, a cosa è successo e cosa succederà dopo. E inizia a formulare una risposta ponderata, piuttosto che reagire semplicemente spinto dall’emozione. Per me la svolta nella consapevolezza di me è stata proprio il fatto che in caso di stress emotivo non posso parlare. Uscire da questo stato, pensare e pianificare come rispondere e compensare i problemi: questi sono i passi giusti nel caso in cui 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. Dopodiché diventi capace di pensare, quando puoi pensare, diventi capace di parlare, e così via.

A proposito, in tribunale l'avvocato avversario cerca di farti questo - ora è chiaro il motivo. Perché ha la capacità di sopprimerti a tal punto che non puoi nemmeno pronunciare il tuo nome, per esempio. In un senso molto reale, non sarai in grado di parlare. Se questo ti accade, e se sai che ti ritroverai in un luogo dove infuriano le battaglie verbali, in un luogo come un tribunale, allora puoi venire con il tuo avvocato. L'avvocato ti difenderà e fermerà l'attacco verbale, e lo farà in modo completamente legale, e lo spazio Zen perduto ti ritornerà. Ad esempio, ho dovuto chiamare la mia famiglia un paio di volte, il giudice è stato molto gentile a riguardo, ma l'avvocato avversario mi ha urlato e urlato contro, non sono riuscito nemmeno a dire una parola. In questi casi, l’utilizzo di un mediatore funziona meglio per me. Il mediatore ferma tutta questa pressione che si riversa su di te in un flusso continuo, ritrovi lo spazio Zen necessario e con esso ritorna la capacità di parlare. Questo è un intero campo della conoscenza in cui c'è molto da studiare, molto da scoprire dentro di sé, e tutto ciò si trasforma in decisioni strategiche di alto livello, diverse per persone diverse. Alcune persone non hanno i problemi sopra descritti; di solito, le persone che lavorano come venditori professionisti non li hanno. Tutte queste persone che si guadagnano da vivere con le parole - cantanti famosi, poeti, leader religiosi e politici, hanno sempre qualcosa da dire. Loro non hanno questi problemi, ma io sì.

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

Puoi continuare la tua conversazione con Cliff alla conferenza Hydra 2019, che si terrà l'11 e il 12 luglio 2019 a San Pietroburgo. Verrà con un rapporto "L'esperienza della memoria transazionale dell'hardware Azul". I biglietti possono essere acquistati sul sito ufficiale.

Fonte: habr.com

Aggiungi un commento