Interviu amplu cu Cliff Click — părintele compilării JIT în Java

Interviu amplu cu Cliff Click — părintele compilării JIT în JavaCliff Click — CTO al companiei Cratus (senzori IoT pentru îmbunătățirea proceselor), fondator și cofondator al mai multor startup-uri (inclusiv Rocket Realtime School, Neurensic și H2O.ai) cu mai multe ieșiri de succes. Cliff a scris primul său compilator la 15 ani (Pascal pentru TRS Z-80)! Este cel mai cunoscut pentru munca sa asupra C2 în Java (the Sea of Nodes IR). Acest compilator a arătat lumii că JIT poate produce cod de calitate, ceea ce a fost unul dintre factorii care au contribuit la emergența Java ca una dintre principalele platforme software moderne. Apoi, Cliff a ajutat compania Azul Systems să construiască un mainframe cu 864 de nuclee cu software în Java pur, care suportea pauze GC pe un heap de 500 de gigaocteți în cadrul a 10 milisecunde. De fapt, Cliff a reușit să lucreze la toate aspectele JVM.

 
Acest post Habr este un interviu amplu cu Cliff. Vom discuta următoarele subiecte:

  • Trecerea la optimizări de nivel scăzut
  • Cum să faci un refactoring mare
  • Modelul de costuri
  • Învățare a optimizărilor de nivel scăzut
  • Exemple practice de îmbunătățire a performanței
  • De ce să creezi propriul limbaj de programare
  • Cariera unui inginer de performanță
  • Provocările tehnice
  • Puțin despre alocarea registrelor și multicoerență
  • Cea mai mare provocare din viața mea

Interviul este condus de:

  • Andrei Satarin de la Amazon Web Services. În cariera sa, a reușit să colaboreze la proiecte complet diferite: a testat o bază de date distribuită NewSQL la Yandex, un sistem de detectare în cloud la Laboratorul Kaspersky, un joc multiplayer la Mail.ru și un serviciu de calcul al prețurilor valutare la Deutsche Bank. Este interesat de testarea sistemelor backend la scară mare și de sistemele distribuite.
  • Vladimir Sitnikov de la Netcracker. Lucrează de zece ani la performanța și scalabilitatea NetCracker OS — software utilizat de operatorii de telecomunicații pentru automatizarea proceselor de gestionare a rețelei și a echipamentului de rețea. Este pasionat de problemele de performanță ale Java și Oracle Database. Autor al mai multor îmbunătățiri de performanță în driverul oficial PostgreSQL JDBC.

Trecerea la optimizări de nivel scăzut

Andrei: Ești o persoană cunoscută în lumea JIT-compilerii, în Java și în ceea ce privește performanța în general, nu-i așa? 

Cliff: Așa este!

Andrei: Să începem cu întrebări generale despre munca pe performanță. Ce părere ai despre alegerea între optimizări de nivel înalt și cele de nivel scăzut, cum ar fi lucrul la nivel de CPU?

Cliff: Aici totul este simplu. Cea mai rapidă linie de cod este aceea care nu este niciodată executată. Prin urmare, trebuie să începi întotdeauna de la un nivel înalt, lucrând la algoritmi. O notare O mai bună va învinge o notare O mai slabă, cu excepția cazului în care intervin unele constante destul de mari. Lucrurile de nivel scăzut vin întotdeauna ultimele. De obicei, dacă ai optimizat suficient restul stivei și mai rămâne ceva interesant – aceasta este esența nivelului scăzut. Dar cum începi de la un nivel înalt? Cum știi că ai făcut suficientă muncă la un nivel înalt? Ei bine… nu există. Nu există rețete gata făcute. Trebuie să înțelegi problema, să decizi ce vrei să faci (pentru a nu face pași nefolositori ulterior) și atunci poți scoate profilerul, care poate spune ceva util. La un moment dat, îți dai seama singur că ai scăpat de lucrurile inutile și că a sosit momentul să te ocupi de ajustările fine de nivel scăzut. Cu siguranță, aceasta este o formă specială de artă. Mulți oameni fac lucruri inutile, dar se mișcă atât de repede, încât nu au timp să se preocupe de performanță. Dar asta este până când problema devine critică. De obicei, 99% din timp nimănui nu-i pasă de ce fac, până în momentul în care pe drumul critic apare ceva important, care devine de interes pentru cineva. Și atunci toți încep să te întrebe „de ce nu a funcționat perfect de la început”. În general, întotdeauna există ceva de îmbunătățit în performanță. Dar 99% din timp nu ai indicii! Încerci pur și simplu să faci ceva să funcționeze și descoperi în acest proces ce este important. Nu poți ști niciodată dinainte că acest fragment trebuie să fie perfect, așa că, în esență, trebuie să fii perfect în tot. Și aceasta este imposibil și nu faci așa. Întotdeauna există o mulțime de lucruri de reparat – și asta este perfect normal.

Cum să faci un refactoring mare

Andrei: Cum lucrați la performanță? Este o problemă transversală. De exemplu, a trebuit vreodată să lucrezi la probleme care apar în urma suprapunerii unui număr mare de funcționalități existente?

Cliff: Evit acest lucru. Dacă știu că performanța va fi o problemă, mă gândesc la asta înainte de a începe să codez, mai ales la structurile de date. Dar adesea descoperi toate acestea mult mai târziu. Și atunci trebuie să recurgi la măsuri extreme și să faci ceea ce numesc „rescrie și domină”: trebuie să te apuci de o bucată destul de mare. O parte din cod tot va trebui rescrisă din cauza problemelor de performanță sau din alte motive. Indiferent de motivul pentru care trebuie să rescrii codul, aproape întotdeauna este mai bine să rescrii o bucată mai mare decât una mai mică. În acel moment, toată lumea începe să tremure de frică: „oh, Doamne, nu poți să atingi atât de mult cod!”. Dar, de fapt, această abordare funcționează aproape întotdeauna mult mai bine. Trebuie să te apuci din start de o problemă mare, să conturezi un cerc mare în jurul ei și să spui: tot ce e în interiorul cercului, voi rescrie. Limita este cu mult mai mică decât conținutul care trebuie înlocuit din interior. Și dacă această conturare a limitelor permite să faci treaba dinăuntru perfect – ești liber, fă ce vrei. Odată ce ai înțeles problema, procesul de rescriere decurge mult mai ușor, așa că ia o bucată mare!
În același timp, atunci când rescrii o bucată mare și realizezi că performanța va fi o problemă, poți începe imediat să te îngrijorezi în legătură cu aceasta. De obicei, se transformă în lucruri simple precum „nu copia datele, gestionează datele cât mai simplu posibil, fă-le mai mici”. În rescrierile mari, există modalități standard de a îmbunătăți performanța. Și acestea se învârt aproape întotdeauna în jurul datelor.

Modelul de costuri

Andrei: În unul dintre podcasturi ai vorbit despre modelele de costuri în contextul performanței. Poți explica ce ai vrut să spui prin asta?

Cliff: Desigur. M-am născut într-o epocă în care performanța procesorului era extrem de importantă. Și această eră revine din nou – soarta nu e lipsită de ironie. Am început să trăiesc în vremurile mașinilor pe 8 biți, primul meu computer funcționa cu 256 de biți. A fost absolut mic. Trebuia să număr instrucțiunile și, pe măsură ce am început să avansăm în ierarhia limbajelor de programare, limbajele își asumau din ce în ce mai multe responsabilități. A fost Assembler, apoi Basic, apoi C, iar C prelua sarcini legate de multe detalii, cum ar fi alocarea registrelor și selecția instrucțiunilor. Dar acolo totul era destul de clar și, dacă am creat un pointer la o instanță a unei variabile, atunci voi obține un load, iar costul acestei instrucțiuni este cunoscut. Hardware-ul oferă un număr cunoscut de cicluri mașină, astfel că viteza de execuție a diferitelor operații poate fi calculată simplu prin adunarea tuturor instrucțiunilor pe care intenționezi să le execuți. Fiecare compare/test/branch/call/load/store putea fi adunat și putea fi spus: iată, aceasta este timpul de execuție. Atunci când te ocupi cu îmbunătățirea performanței, cu siguranță vei observa ce numere corespund ciclurilor fierbinți de mică amploare. 
Dar de îndată ce te schimbi pe Java, Python și lucruri asemănătoare, te îndepărtezi foarte repede de hardware-ul de nivel scăzut. Care este costul apelului getter în Java? Dacă JIT în HotSpot face totul corect inlined, acesta va fi un load, dar, dacă nu a făcut asta – va fi un apel de funcție. Deoarece apelul se află în ciclul fierbinte, va anula toate celelalte optimizări din acel ciclu. Prin urmare, costul real va fi mult mai mare. Și pierzi imediat capacitatea de a privi un segment de cod și de a înțelege cât de mult ar trebui să-l executăm în termeni de frecvență de ceas a procesorului, memorie utilizată și cache. Totul devine interesant doar dacă te aprofundezi în performanță.
Acum ne aflăm într-o situație în care vitezele procesoarelor aproape că nu au crescut timp de un deceniu. Timpurile vechi revin! Nu te mai poți baza pe o performanță bună pe un singur fir de execuție. Dar dacă te-ai apuca de calcul paralel – este extrem de complicat, toți se uită la tine ca la James Bond. Accelerările de zece ori apar de obicei acolo unde cineva a ratat ceva. Paralelismul necesită multă muncă. Pentru a obține acel accelerare de zece ori, trebuie să înțelegi modelul de cost. Ce costă și cât costă. Iar pentru asta trebuie să înțelegi cum se potrivește limbajul pe hardware-ul de bază.
Martin Thompson a găsit un cuvânt minunat pentru blogul său Simpatia Mechanical! Este necesar să înțelegi ce va face hardware-ul, cum anume va face asta și de ce face ceea ce face. Folosind aceste informații, este destul de simplu să începi să număreți instrucțiunile și să afli unde se duce timpul de execuție. Dacă nu ai pregătirea corespunzătoare, cauți doar o pisică neagră într-o cameră întunecată. Văd constant oameni care optimizează performanța, fără să aibă cele mai mici idei despre ce naiba fac de fapt. Se chinuie foarte mult și nu avansează deloc. Și când iau același fragment de cod, adaug câteva trucuri simple și obțin o accelerare de cinci sau zece ori, ei spun: „Eh, asta nu este cinstit, știm deja că ești mai bun.” Uimitor. Despre ce vorbeam… modelul de cost – este despre ce cod scrii și cât de repede funcționează acesta în imaginea de ansamblu.

Andrei: Și cum reții un astfel de volum în minte? Se atinge prin multă experiență, nu-i așa? De unde se dobândește astfel de experiență?

Cliff: Ei bine, experiența mea nu a venit pe cel mai simplu drum. Am programat în Assembler în vremurile când putea să înțelegi fiecare instrucțiune în parte. Sună prostie, dar de atunci am un set de instrucțiuni Z80 în cap, în memorie, pentru totdeauna. Nu-mi amintesc numele oamenilor la un minut după o discuție, dar îmi amintesc codul scris acum 40 de ani. Amuzant, arată ca un sindrom de „geniu idiot».

Învățare a optimizărilor de nivel scăzut

Andrei: Există o modalitate mai simplă de a intra în domeniu?

Cliff: Da și nu. Echipamentele pe care le folosim nu s-au schimbat atât de mult în această perioadă. Toată lumea folosește x86, cu excepția smartphone-urilor care funcționează pe Arm. Dacă nu te ocupi de ceva foarte avansat în embeddeding, ai aceleași lucruri. Bine, să continuăm. Instrucțiunile nu s-au schimbat de secole. Trebuie să mergi și să scrii ceva în Assembler. Puțin, dar suficient pentru a începe să înțelegi. Tu râzi, dar eu vorbesc serios. Trebuie să înțelegi corespondența dintre limbaj și hardware. După aceea, trebuie să mergi, să scrii puțin și să creezi un mic compilator de joc pentru un mic limbaj de joc. „Joc” înseamnă că trebuie să-l faci într-un timp rezonabil. Poate fi super simplu, dar trebuie să genereze instrucțiuni. Actul generării instrucțiunilor va permite să înțelegi modelul de cost pentru puntea între codul de nivel înalt, pe care toată lumea îl scrie, și codul mașină care rulează pe hardware. Această corespondență se va întipări în minte în momentul în care scrii compilatorul. Chiar și cel mai simplu compilator. După aceea, poți începe să te uiți la Java și să vezi că există o prăpastie semantică mult mai profundă, iar construirea de punți peste aceasta este mult mai complicată. În Java, este mult mai greu să înțelegi dacă puntea noastră este bună sau proastă, ce o va face să se prăbușească și ce nu. Dar ai nevoie de un punct de plecare, când te uiți la cod și înțelegi: „aha, acest getter ar trebui să fie inlinat de fiecare dată”. Și se dovedește că uneori se și întâmplă, cu excepția cazului în care metoda devine prea mare și JIT începe să inlinieze tot. Performanța acestor locuri poate fi prezisă instantaneu. De obicei, gettere funcționează bine, dar apoi te uiți la buclele mari și fierbinți și înțelegi că există apeluri de funcții care nu este clar ce fac. Aici este problema cu utilizarea pe scară largă a getterelor, motivul pentru care nu se inliniază – nu este clar dacă este un getter. Dacă ai o bază de cod foarte mică, poți pur și simplu să o memorezi și apoi să spui: acesta este un getter, iar acesta este un setter. Într-o bază de cod mare, fiecare funcție are propria sa poveste, care nu este cunoscută nimănui. Profilatorul spune că am pierdut 24% din timp pe un anumit ciclu și, pentru a înțelege ce face acest ciclu, trebuie să te uiți la fiecare funcție din interior. Nu poți să înțelegi asta fără a studia funcția, și acest lucru încetinește serios procesul de înțelegere. De aceea nu folosesc gettere și settere, am trecut la un nou nivel!
De unde pot obține un model de costuri? Ei bine, pot citi ceva, desigur... Dar cred că cea mai bună modalitate este să acționez. Să fac un mic compilator și acesta va fi cel mai bun mod de a înțelege modelul de costuri și de a-l încorpora în propria mea gândire. Un mic compilator care ar putea fi folosit pentru programarea unui cuptor cu microunde – aceasta este o sarcină pentru începători. Adică, dacă deja ai abilități de programare, ar trebui să fie suficient. Toate aceste lucruri, cum ar fi parcurgerea unei stringa care va fi o expresie algebraică, scoaterea instrucțiunilor operațiunilor matematice în ordinea corectă, obținerea valorilor corecte din registre – toate acestea se fac rapid. Și în timp ce faci asta, se va imprima în minte. Cred că toată lumea știe ce face un compilator. Și aceasta va oferi o înțelegere a modelului de costuri.

Exemple practice de îmbunătățire a performanței

Andrei: La ce altceva ar trebui să fiu atent când lucrez la performanță?

Cliff: Structuri de date. Apropo, da, nu am mai susținut aceste cursuri de mult timp... Rocket School. A fost amuzant, dar necesita atât de mult efort, iar eu mai am și viața mea! Bine. Așadar, într-una din lecțiile mari și interesante, „Unde se duce performanța ta”, le-am dat studenților un exemplu: două giga și jumătate de date fintech erau citite dintr-un fișier CSV și apoi trebuia să calculez numărul produselor vândute. Datele pieței de tip tic. Pachete UDP, transformate în format text, începând cu anii 70. Chicago Mercantile Exchange – tot felul de lucruri cum ar fi uleiul, porumbul, soia și așa mai departe. Trebuia să număr aceste produse, numărul tranzacțiilor, volumul mediu al mișcărilor de fonduri și bunuri etc. Este matematică de tranzacționare destul de simplă: găsește codul produsului (acesta este 1-2 caractere în tabela de hash), obține suma, adaugă-o într-unul din seturile de tranzacții, adaugă volumul, adaugă costul și câteva alte lucruri. O matematică foarte simplă. Implementarea ludică a fost foarte directă: totul se află într-un fișier, citesc fișierul și mă mișc pe el, separând înregistrările individuale în stringuri Java, caut în ele lucrurile necesare și le adun conform matematicii descrise mai sus. Și acest lucru funcționează cu o viteză oarecare.

Cu această abordare, totul este evident, ceea ce se întâmplă, și computațiile paralele nu vor ajuta aici, nu? Se pare că o creștere a performanței de cinci ori poate fi obținută doar prin alegerea structurilor de date potrivite. Și acest lucru îi surprinde chiar și pe programatorii experimentați! În cazul meu specific, accentul a fost pe faptul că nu ar trebui să fac alocări de memorie în buclele frecvente. Ei bine, aceasta nu este toată adevărul, dar în general – nu este bine să aloci „odată la X”, când X este suficient de mare. Când X este de două ori și jumătate de gigabyte, nu ar trebui să aloci nimic „odată pentru literă”, sau „odată pentru linie”, sau „odată pentru câmp”, nimic de acest fel. Aici se pierde timpul. Cum funcționează acest lucru? Imaginează-ți că fac o apelare String.split() sau BufferedReader.readLine(). Readline face un șir dintr-un set de octeți care vin prin rețea, o dată pentru fiecare linie, pentru fiecare dintre sutele de milioane de linii. Eu iau acest șir, îl analizez și îl arunc. De ce îl arunc – ei bine, eu l-am procesat deja, tot. Așadar, pentru fiecare octet citit din acele 2.7G, vor fi scrise două caractere în șir, adică deja 5.4G, iar acestea nu îmi mai sunt necesare, așa că sunt aruncate. Dacă ne uităm la lățimea de bandă a memoriei, încărcăm 2.7G, care trec prin memorie și magistrala de memorie în procesor, și mai departe se trimit de două ori mai multe în șirul aflat în memorie, și toate acestea sunt procesate la crearea fiecărui nou șir. Dar trebuie să îl citesc, hardware-ul îl citește, chiar dacă după aceea totul va fi procesat. Și trebuie să îl scriu, pentru că am creat un șir și cache-urile au fost umplute – cache-ul nu poate reține 2.7G. În total, pentru fiecare octet citit, citesc încă două octeți suplimentari și scriu doi octeți suplimentari, și în cele din urmă, au un raport de 4:1 – într-un astfel de raport, risipim lățimea de bandă a memoriei. Apoi se dovedește că dacă fac String.split() – atunci o fac departe de ultima dată, acolo pot fi încă 6-7 câmpuri. Așadar, codul clasic de citire CSV cu parsarea ulterioară a șirurilor duce la pierderi de lățime de bandă a memoriei de aproximativ 14:1 față de ceea ce ai dori, de fapt, să ai. Dacă elimini aceste alocări, poți obține o accelerare de cinci ori.

Și nu este foarte complicat. Dacă privești codul din unghiul corect, totul devine destul de simplu imediat ce ai înțeles esența problemei. Nu trebuie să încetezi să aloci memorie: problema este că aloci ceva și imediat se pierde, iar pe parcurs consumă o resursă importantă, care în acest caz este lățimea de bandă a memoriei. Și totul se traduce într-o scădere a performanței. Pe x86, de obicei, trebuie să utilizezi activ ciclurile procesorului, dar aici ai ars toată memoria mult mai devreme. Soluția este să reduci numărul de alocări. 
O altă parte a problemei este că, dacă pornești un profiler atunci când lățimea de bandă a memoriei s-a epuizat, exact în momentul în care se întâmplă acest lucru, aștepți de obicei să revină cache-ul, deoarece este plin de zgomotul pe care tocmai l-ai generat, cu toate acele stringuri. Prin urmare, fiecare operațiune load sau store devine lentă, deoarece duc la erori în cache – întregul cache a devenit lent, așteptând să fie eliberat de zgomot. De aceea, profilerul va arăta doar o zgomot aleatoriu cald, întins pe tot ciclul – nu va exista nicio instrucțiune sau loc în cod care să fie fierbinte. Numai zgomot. Și dacă te uiți la ciclurile GC, toate vor fi pe Young Generation și foarte rapide – microsecunde sau milisecunde maxim. Deoarece toată această memorie moare instantaneu. Aloci miliarde de gigaocteți, și el le elimină, și le elimină, și din nou le elimină. Totul se întâmplă foarte rapid. Așadar, ai cicluri GC ieftine, zgomot cald pe parcursul întregului ciclu, dar ne dorim o accelerare de 5 ori. În acel moment, ar trebui să îți facă ceva click în minte și să sune: „de ce așa?!”. Supraîncărcarea lățimii de bandă a memoriei nu se reflectă în debugger-ul standard, trebuie să pornești un debugger pentru contoarele de performanță hardware și să vezi asta singur și direct. Iar indirect, acesta poate fi suspectat din aceste trei simptome. Al treilea simptom este atunci când te uiți ce aloci, întrebi profiler-ul, iar el răspunde: „Ai făcut miliarde de stringuri, dar GC a funcționat gratuit”. Odată ce s-a întâmplat asta, înțelegi că ai generat prea multe obiecte și ai ars întreaga lățime de bandă a memoriei. Există o modalitate de a înțelege asta, dar nu este evidentă. 

Problema se află în structura datelor: structura pură, care stă la baza a tot ce se întâmplă, este prea mare, are 2.7G pe disc, așa că este foarte nedorit să faci o copie a acestei lucruri — preferi să o încarci direct din bufferul de bytes de rețea în registre, pentru a nu citi-scrie în șir de mai multe ori. Din păcate, Java nu îți oferă o astfel de bibliotecă în JDK. Dar este un lucru trivial, nu-i așa? Practic, sunt 5-10 linii de cod care vor implementa un loader de șiruri cu buffer, care reproduce comportamentul clasei şir, acționând ca un wrapper în jurul bufferului de bytes de bază. Ca rezultat, ajungi să lucrezi aproape ca și cum ai lucra cu șiruri, dar, de fapt, acolo se mișcă pointeri către buffer, iar bytes brut nu sunt copiate nicăieri, astfel reutilizând mereu aceleași buffere, de nenumărate ori, iar sistemul de operare este fericit să preia lucrurile pentru care este destinat, precum dublarea ascunsă a acestor buffere de bytes, iar tu nu mai procesezi un flux infinit de date inutile. Apropo, cu siguranță știți că, atunci când lucrezi cu GC, este garantat că fiecare alocare de memorie nu va fi vizibilă procesorului după ultimul ciclu de GC? Așa că, tot acest lucru nu poate fi în cache, iar mai departe se întâmplă un miss garantat de 100%. Când lucrezi cu un pointer, pe x86, citirea unui registru din memorie durează 1-2 cicluri, și imediat ce se întâmplă acest lucru, plătești, plătești, plătești, pentru că toată memoria NINE cache-uri – și aceasta este costul alocării memoriei. Costul real.

Pe scurt, structurile de date sunt cele mai greu de schimbat. Odată ce realizezi că ai ales o structură de date greșită care îți va afecta performanța pe viitor, de obicei, este necesar să faci un efort considerabil, dar dacă nu o faci, lucrurile vor deveni mai dificile. În primul rând, trebuie să te gândești la structurile de date, acest aspect este important. Principalul cost provine din structuri de date elaborate, folosite în stilul „am copiat structura de date X în structura de date Y pentru că Y îmi place mai mult ca aspect”. Totuși, operația de copiere (care pare ieftină) consumă de fapt lățimea de bandă a memoriei și aici se află tot timpul pierdut. Dacă am un șir gigantic de JSON și vreau să-l transform într-un arbore DOM structurat din POJO sau ceva similar, operația de parsare a acestui șir și construirea POJO, iar apoi accesarea nouă a POJO va conduce la costuri suplimentare - lucru care nu este ieftin. Cu excepția cazului în care vei accesa POJO mult mai des decât șirul. La o adică, poți încerca să decodifici șirul și să extragi doar ceea ce ai nevoie, fără a-l transforma în POJO. Dacă toate acestea se întâmplă într-un context în care se cere performanță maximă, evită POJO - trebuie să interacționezi direct cu șirul.

De ce să creezi propriul limbaj de programare

Andrei: Ai spus că pentru a înțelege modelul de cost, trebuie să îți scrii un mic limbaj...

Cliff: Nu un limbaj, ci un compilator. Limbajul și compilatorul sunt lucruri diferite. Cea mai importantă distincție este în mintea ta. 

Andrei: Apropo, din câte știu, experimentezi cu crearea propriilor limbaje. De ce?

Cliff: Pentru că pot! Sunt pe jumătate pensionat, așa că acesta este hobby-ul meu. Întreaga mea viață am implementat limbajele altora. De asemenea, am lucrat mult la stilul de codare. Și pentru că văd probleme în alte limbaje. Observ că există modalități mai bune de a face lucrurile obișnuite. Și aș profita de ele. M-am săturat să văd probleme în mine, în Java, în Python, în orice alt limbaj. Acum scriu în React Native, JavaScript și Elm ca un hobby care nu este legat de pensionare, ci de o muncă activă. Scriu și în Python și, cel mai probabil, voi continua să lucrez la învățarea automată pentru backend-urile Java. Există multe limbaje populare și fiecare are trăsături interesante. Fiecare este bun în felul său și poți încerca să le unifici toate aceste trăsături. Așa că mă dedic studiului lucrurilor interesante pentru mine, comportamentul limbajului, încercând să inventez o semantică rezonabilă. Și până acum reușesc!În acest moment, mă confrunt cu semantică memoriei, deoarece vreau să o am precum în C și Java și să obțin un model puternic de memorie și semantică pentru loaduri și store-uri. În același timp, vreau să am un tip de inferență automată precum în Haskell. Așadar, încerc să combin inferența tipurilor asemănătoare cu Haskell cu o memorie care funcționează ca în C și Java. Acest lucru mă ocup de 2-3 luni, de exemplu.

Andrei: Dacă construiești un limbaj care preia cele mai bune aspecte din alte limbaje, te-ai gândit că cineva ar face invers: ar prelua ideile tale și le-ar folosi la el?

Cliff: Exact așa apar noile limbaje! De ce Java este similar cu C? Pentru că C avea o sintaxă bună pe care toată lumea o înțelegea, iar Java s-a inspirat din această sintaxă, adăugând acolo siguranța tipurilor, verificări ale limitelor tabloului, GC, și de asemenea au îmbunătățit anumite lucruri din C. Au adăugat ale lor. Dar s-au inspirat destul de mult, nu-i așa? Toți stăm pe umerii gigantilor care au fost înaintea ta – așa se face progresul.

Andrei: Din câte înțeleg, limbajul tău va fi sigur în legătură cu utilizarea memoriei. Te-ai gândit să implementezi ceva asemănător cu borrow checker din Rust? Te-ai uitat la el, ce părere ai?

Cliff: Ei bine, scriu în C de o veșnicie, cu toate aceste malloc și free, și gestionez manual durata de viață. Știți, 90-95% din durata de viață gestionată manual are aceeași structură. Și este foarte, foarte dureros să te ocupi de asta manual. Mi-ar plăcea ca compilatorul să spună pur și simplu ce se întâmplă și ce ai obținut prin acțiunile tale. Pentru unele lucruri, borrow checker face asta din cutie. De asemenea, ar trebui să extragă automat informații, să înțeleagă totul și să nu mă încarce cu faptul de a expune această înțelegere. Ar trebui să facă cel puțin o analiză locală a scăpării, și abia dacă nu reușește, atunci ar trebui să adauge anotații de tipuri care să descrie durata de viață – și o astfel de schemă este mult mai complexă decât borrow checker-ul, sau orice alt checker de memorie existent. Alegerea între «totul e în regulă» și «nu am înțeles nimic» — nu, ar trebui să existe ceva mai bun. 
Așadar, ca o persoană care a scris mult cod în C, consider că suportul pentru gestionarea automată a duratei de viață este cel mai important aspect. De asemenea, m-a frustrat foarte mult cât de multă memorie folosește Java, iar principala plângere este că în GC (garbage collection). Când aloci memorie în Java, nu primești înapoi memoria care a fost locală în ultimul ciclu de GC. În limbajele cu o gestionare a memoriei mai precisă, acest lucru nu se întâmplă. Dacă apelezi malloc, primești imediat memoria care, de obicei, a fost utilizată recent. De obicei, faci cu memoria diverse lucruri temporare și o returnezi imediat. Și aceasta revine imediat în pool-ul malloc, iar următorul ciclu malloc o extrage din nou. Prin urmare, utilizarea reală a memoriei se reduce la un set de obiecte active într-un anumit moment, plus eventualele scurgeri. Și dacă nu ai scurgeri într-un mod complet inacceptabil, cea mai mare parte a memoriei rămâne în cache-uri și în procesor, iar aceasta funcționează rapid. Dar necesită mult control manual al memoriei cu malloc și free, apelate în ordinea și locul corecte. Rust se poate ocupa de acest lucru corect și în multe cazuri oferă chiar o performanță mai mare, deoarece consumul de memorie se restrânge doar la calculele curente - spre deosebire de așteptarea următorului ciclu de GC care va elibera memorie. Ca rezultat, am obținut o modalitate foarte interesantă de a îmbunătăți performanța. Și este destul de puternică - în sensul că m-am ocupat de astfel de lucruri în prelucrarea datelor pentru fintech, iar aceasta a permis obținerea unui spor de viteză de aproximativ cinci ori. Acesta este un spor considerabil, mai ales într-o lume în care procesoarele nu devin mai rapide, iar noi continuăm să așteptăm îmbunătățiri.

Cariera unui inginer de performanță

Andrei: De asemenea, aș vrea să întreb despre carieră în ansamblu. Ați devenit celebru datorită muncii pe JIT în HotSpot și apoi v-ați mutat la Azul - și aceasta este o companie JVM. Dar v-ați ocupat mai mult de hardware decât de software. Apoi, dintr-o dată, v-ați concentrat pe Big Data și Machine Learning, iar apoi pe detectarea fraudelor. Cum s-a întâmplat acest lucru? Acestea sunt domenii foarte diferite de dezvoltare.

Cliff: Am practicat programarea de ceva vreme și am reușit să mă implic în activități foarte diverse. Și când oamenii spun: „oh, tu ești cel care a făcut JIT pentru Java!”, este întotdeauna amuzant. Până atunci, am lucrat la un clon al PostScript – limbajul pe care Apple l-a folosit cândva pentru imprimantele sale laser. Și înainte de aceasta, am realizat o implementare a limbajului Forth. Cred că tema comună pentru mine este dezvoltarea de instrumente. Toată viața am creat instrumente prin care alți oameni își scriu programele lejere. Dar am lucrat și la dezvoltarea sistemelor de operare, drivere, debuggere la nivel de kernel, limbaje pentru dezvoltarea sistemelor de operare, care au început simplu, dar au devenit tot mai complexe. Dar tema principală rămâne, totuși, dezvoltarea instrumentelor. O mare parte din viața mea a trecut între Azul și Sun, și a fost despre Java. Dar când m-am apucat de Big Data și Machine Learning, mi-am pus din nou pălăria de ceremonie și am spus: „Oh, acum avem o problemă non-trivială și aici se întâmplă o grămadă de lucruri interesante și oameni care fac ceva”. Este un traseu excelent de dezvoltare, pe care merită să-l parcurgi.

Da, îmi plac foarte mult calculele distribuite. Primul meu loc de muncă a fost în studenție, lucrând cu C, la un proiect publicitar. Erau calcule distribuite pe cipuri Zilog Z80, care colectau date pentru recunoașterea analogică a textului, realizată de un adevărat analizor analogic. A fost un subiect foarte interesant și complet neobișnuit. Însă au fost probleme, o parte din date nu erau recunoscute corect, așa că trebuia să scoatem imaginea și să o arătăm unei persoane care o citise cu ochii și ne comunica ce anume era acolo, așa că erau joburi cu date, iar aceste joburi aveau propriul lor limbaj. Era un backend care gestiona totul – Z80 care lucrau în paralel cu terminale vt100 – câte unul pentru fiecare persoană, iar modelul de programare paralelă pe Z80 era utilizat. Un anumit bloc de memorie comun, împărțit de toate Z80 în configurația tip „stea”; bckplane-ul era și el împărțit, iar jumătate din RAM era împărțită în rețea, iar cealaltă jumătate era privată sau folosită pentru altceva. O sistemă paralelă distribuită complexă, cu memorie împărtășită… parțial împărtășită. Când a fost asta… deja nu-mi amintesc, undeva prin mijlocul anilor '80. Destul de demult. 
Da, să zicem că 30 de ani este destul de mult. Sarcinile legate de calculele distribuite există de mult timp, oamenii s-au luptat cu... Beowulf-clustere. Aceste clustere arată ca... De exemplu: există Ethernet și procesorul tău x86 rapid este conectat la acest Ethernet, și acum vrei să obții memorie partajată falsă, pentru că nimeni nu putea în acel moment să se ocupe de programarea calculelor distribuite, era mult prea complicat și de aceea a fost creată memoria partajată falsă cu protecția paginilor de memorie pe x86, și dacă scriai în această pagină, le spuneam celorlalte procesoare că, dacă au acces la aceeași memorie partajată, trebuie să o încarce de la tine, și astfel a apărut ceva asemănător cu un protocol de suport pentru coerența cache-urilor și software-ul pentru aceasta. O conceptie interesantă. Problema adevărată, desigur, era alta. Totul funcționa, dar aveai rapid probleme de performanță, pentru că nimeni nu înțelegea modelul de performanță la un nivel suficient de bun – care erau modelele de acces la memorie, cum să faci astfel încât nodurile să nu se pinguiască reciproc la nesfârșit, și așa mai departe.

La H2O am avut această idee: dezvoltatorii înșiși sunt responsabili pentru a determina unde se află paralelismul și unde nu. Am conceput un model de codare astfel încât să devină ușor și simplu să scriu cod de înaltă performanță. În schimb, să scrii cod care rulează încet este complicat, iar acesta va arăta prost. Trebuie să te străduiești serios pentru a scrie cod lent, va trebui să folosești metode neobișnuite. Codul care încetinește este vizibil de la prima privire. Ca urmare, de obicei se scrie cod care funcționează rapid, dar trebuie să te descurci cu ce să faci în cazul memoriei partajate. Toate acestea sunt legate de mari matrice, iar comportamentul acestora seamănă cu matricele mari nevolatibile din Java paralel. Gândește-te că două fire scriu într-o matrice paralelă, unul dintre ele câștigă, iar celălalt, în mod corespunzător, pierde, și nu știi care dintre ele este care. Dacă nu sunt volatibile, ordinea poate fi oricum – și asta funcționează cu adevărat bine. Oamenii chiar se îngrijesc de ordinea operațiunilor, plasează corect volatile și în locurile potrivite se așteaptă la probleme de performanță legate de memorie. Altfel, ar scrie cod sub formă de bucle de la 1 la N, unde N sunt câteva trilioane, sperând că toate cazurile complexe vor deveni automat paralele – și acolo nu funcționează. Dar în H2O, aceasta nu este Java și nici Scala, o poți considera „Java minus minus”, dacă dorești. Este un stil de programare foarte clar și seamănă cu scrierea codului simplu în C sau Java cu bucle și matrice. Dar în același timp, memoria poate fi procesată în terabyte. Încă folosesc H2O. Din când în când, o folosesc în diferite proiecte – și este în continuare cel mai rapid lucru, de zeci de ori superior competitorilor. Dacă faci Big Data cu date columnare, este foarte greu să depășești H2O.

Provocările tehnice

Andrei: Care a fost cea mai mare provocare din întreaga ta carieră?

Cliff: Discutăm despre partea tehnică sau despre cea non-tehnică a întrebării? Aș spune că cele mai mari provocări sunt cele non-tehnice. 
În ceea ce privește provocările tehnice, le-am învins. Nu știu care a fost cea mai mare, dar au fost câteva destul de interesante, la care a trebuit să lucrez mult timp și să fac o mulțime de efort mental. Când am venit la Sun, eram convins că voi face un compilator rapid, dar o mulțime de seniori mi-au spus că nu voi reuși niciodată. Dar am mers pe acest drum, am scris un compilator până la alocarea registrelor, și a fost destul de rapid. Era la fel de rapid ca un C1 modern, dar atunci alocarea era mult mai lentă, și privind înapoi – aceasta a fost o problemă a unei structuri de date mari. Aveam nevoie de ea pentru a scrie un alocator grafic de registre și nu înțelegeam dilema dintre expresivitatea codului și viteză, care exista în acea epocă și era foarte importantă. S-a dovedit că structura de date depășește de obicei dimensiunea cache-ului pe x86-urile de atunci și, prin urmare, dacă la început presupuneam că alocarea registrelor va consuma 5-10 procente din tot timpul de jitare, în realitate s-a transformat în 50%.

Timpul a trecut, compilatorul devenea din ce în ce mai clar și performant, a încetat să genereze cod groaznic în tot mai multe cazuri, iar performanța a început să semene mai mult cu ceea ce produce un compilator C. Dacă, desigur, nu scrii ceva necorespunzător, pe care nici măcar C nu-l accelerează. Dacă scrii cod ca în C, obții performanță similară cu C în mai multe cazuri. Și pe măsură ce mergeam mai departe, codul devenea tot mai frecvent asimptotic la nivelul C, alocarea registrelor a început să semene cu ceva complet... indiferent dacă codul tău rulează rapid sau lent. Am continuat să lucrez la alocator, astfel încât să facă alocări mai bune. A devenit din ce în ce mai lent, dar oferea din ce în ce mai multă performanță în cazurile în care nimeni altcineva nu reușea. Puteam să intru în alocarea registrelor, să îngrop acolo o lună de muncă, și brusc tot codul începea să ruleze cu 5% mai repede. Se întâmpla iar și iar și alocarea registrelor a devenit ceva de genul unei opere de artă – toți o iubeau sau o urau, iar oamenii din academie puneau întrebări despre „de ce se face totul în acest mod”, de ce nu scanare liniară, și care este diferența. Răspunsul este același: un alocător bazat pe colorarea grafului plus o gestionare foarte atentă a codului tampon înseamnă un instrument de câștig, cea mai bună combinație, pe care nimeni nu o poate înfrunta. Și aceasta este o chestiune destul de subtilă. Tot ce face compilatorul acolo sunt lucruri bine cunoscute, deși și acestea au fost duse la nivel de artă. Întotdeauna am făcut lucruri care ar fi trebuit să transforme compilatorul într-o capodoperă. Dar nimic din acestea nu a fost ceva extraordinar – cu excepția alocatoarelor de registre. Secretul este că trebuie să se facă cu atenție tăiați sub sarcină și, dacă se întâmplă acest lucru (pot explica mai în detaliu, dacă este de interes), înseamnă că se poate face inline mai agresiv, fără riscul de a depăși la încrucișarea graficului de performanță. În acele vremuri existau o mulțime de compilatoare cu toate tipurile de accesorii și trinkets, care aveau alocatoare de registre, dar nimeni nu a mai reușit asta.

Problema este că, dacă adaugi metode care pot fi inlinate, mărind și mai mult domeniul inlining-ului, setul de valori utilizate depășește instantaneu numărul de registre și devine necesar să faci spilling. Nivelul critic apare de obicei atunci când alocatorul renunță, iar un bun candidat pentru spilling valorează cât altul, iar tu începi să faci spilling pe unele lucruri complet ciudate. Valoarea inlining-ului consta în faptul că pierzi o parte din overhead, overhead-ul apelului și salvării; poți vedea valorile interne și le poți optimiza în continuare. Costul inlining-ului este că se generează un număr mare de valori viabile și, dacă alocatorul tău de registre face spilling de mai multe ori decât este necesar, pierzi imediat. Prin urmare, majoritatea allocatorilor se confruntă cu problema că, atunci când inlining-ul trece peste o anumită limită, începe să facă spilling pentru tot și performanța poate fi redusă drastic. Cei care implementează compilatorul adaugă unele heuristici: de exemplu, pentru a opri inlining-ul, începând de la o dimensiune suficient de mare, deoarece alocările vor deteriora totul. Astfel se formează un punct de inflexiune în graficul performanței – faci inlining, faci inlining, performanța crește ușor – și apoi bum! – scade brusc, pentru că ai realizat prea mult inlining. Așa a funcționat totul până la apariția Java. Java necesită mult mai mult inlining, așa că a trebuit să fac alocatorul meu mult mai agresiv pentru a se stabiliza, și nu a cădea, iar dacă ai făcut prea mult inlining – el începe să facă spilling, dar apoi totuși ajunge momentul „nu mai este spilling”. Aceasta este o observație interesantă și a venit la mine dintr-o dată, nu era evident, dar s-a dovedit a fi bine răsplătită. Am adoptat inlining-ul agresiv și m-a dus în locuri în care performanța Java și C sunt comparabile. Ele sunt într-adevăr apropiate – pot scrie cod în Java care este semnificativ mai rapid decât codul în C și așa mai departe, dar în medie, în imaginea de ansamblu, sunt aproximativ comparabile. Cred că o parte din această realizare se datorează alocatorului de registre, care îmi permite să fac inlining la capacitate maximă. Eu pur și simplu fac inlining cu tot ceea ce văd. Întrebarea este dacă alocatorul funcționează bine, rezultând un cod care funcționează în mod rezonabil. Acesta a fost un mare provocare: să înțeleg tot acest lucru și să-l fac să funcționeze.

Puțin despre alocarea registrelor și multicoerență

Vladimir: Probleme precum alocarea registrelor par a fi un subiect etern fără sfârșit. Este interesant dacă a existat vreodată o idee care părea promițătoare, dar care a eșuat în practică?

Cliff: Desigur! Alocarea registrelor este un domeniu în care, pentru a rezolva o problemă NP-completă, încerci să găsești anumite euristici. Și niciodată nu vei putea obține o soluție perfectă, nu-i așa? Este pur și simplu imposibil. Uite, compilarea Ahead of Time – nici aceasta nu funcționează bine. Vorbim aici despre cazuri medii. Despre performanța tipică, așa că poți să te duci și să măsori ceva ce consideri a fi o performanță tipică bună – până la urmă, tu lucrezi la îmbunătățirea ei! Alocarea registrelor este un subiect dedicat integral performanței. Odată ce ai un prim prototip, care funcționează și face ceea ce trebuie, urmează munca asupra performanței. Trebuie să înveți să măsori bine. De ce este important? Dacă ai date clare, poți să te uiți la diferite secțiuni și să vezi: aha, asta a ajutat aici, dar acolo a eșuat totul! Apar idei bune, adaugi o nouă euristică și brusc totul începe să funcționeze puțin mai bine în medie. Sau nu începe. Am avut o grămadă de cazuri în care ne-am luptat pentru cinci procente de performanță care ne-au diferențiat dezvoltarea de alocatorul anterior. Și de fiecare dată arată astfel: câteva câteva câștiguri, câteva pierderi. Dacă ai instrumente bune de analiză a performanței, poți găsi ideile pierdute și înțelege de ce au eșuat. Poate că ar trebui să lăsăm totul așa cum este, poate că ar trebui să ne ocupăm mai serios de ajustări fine sau să mergem să reparăm ceva diferit. Este un întreg set de lucruri! Am realizat acest hack interesant, dar am nevoie și de acesta, și de acesta, și de acesta – iar combinația lor totală oferă anumite îmbunătățiri. Iar soluțiile unice pot eșua. Asta este natura muncii asupra problemelor de performanță NP-complete.

Vladimir: Se pare că lucruri precum colorarea în alocatoare sunt o problemă deja rezolvată. Ei bine, pentru voi pare rezolvată, judecând după ceea ce povestiți, așa că merită să...

Cliff: Nu este rezolvată în sine. Tu trebuie să o transformi în „rezolvată”. Există sarcini complexe și trebuie abordate. Odată ce acest lucru este realizat, vine timpul pentru a lucra la performanță. Trebuie să abordezi această muncă în mod corespunzător – să faci benchmark-uri, să colectezi metrice, să explici situațiile în care, când te întorci la o versiune anterioară, vechiul tău hack începe din nou să funcționeze (sau invers, să nu mai funcționeze). Și nu te abate până nu obții un rezultat. Așa cum am mai spus, ideile grozave care nu au funcționat, dar în domeniul alocării registrelor sunt practic nelimitate. Poți, de exemplu, să citești publicații științifice. Deși acum acest domeniu a început să progreseze mult mai lent și a devenit mai clar decât în vremurile sale de început. Totuși, în acest domeniu lucrează o infinitate întreagă de oameni și toate ideile lor merită încercate, toate așteaptă momentul lor. Și nu poți spune cât de bune sunt, dacă nu le încerci. Cât de bine se integrează totul cu restul în alocătorul tău, căci alocătorul face o mulțime de lucruri, și unele idei din alocătorul tău specific nu vor funcționa, dar în alt alocător – cu ușurință. Principala modalitate de a învinge pentru un alocător este de a scoate lucrurile lente din calea principală și a forța divizarea pe marginile căilor lente. Așadar, dacă vrei să lansezi GC, să alegi calea lentă, să te deoptimizzi, să arunci excepții, tot în acest spirit – știi că aceste lucruri sunt relativ rare. Și chiar sunt rare, am verificat. Faci muncă suplimentară și datorită acestui fapt dispar numeroase limitări pe aceste căi lente, dar acesta nu este foarte important, deoarece sunt lente și sunt frecventate rar. De exemplu, un pointer nul – acesta nu se întâmplă niciodată, nu-i așa? Trebuie să ai mai multe căi pentru lucruri diferite, dar nu trebuie să se suprapună în calea principală. 

Vladimir: Ce părere ai despre multicores, când sunt mii de nuclee deodată? Este un lucru util?

Cliff:Succesul GPU arată că este destul de util!

Vladimir:Sunt destul de specializate. Dar ce zici de procesoarele de uz general?

Cliff: Ei bine, acesta a fost modelul de afaceri Azul. Răspunsul a venit încă dintr-o epocă în care oamenii iubeau foarte mult performanța predictibilă. Atunci era destul de dificil să scrii cod paralel. Modelul de codare H2O se scalează bine, dar nu este un model de uz general. Poate doar ușor mai general decât utilizarea GPU-ului. Vorbim despre complexitatea dezvoltării unei astfel de soluții sau despre complexitatea utilizării acesteia? De exemplu, o lecție interesantă pe care mi-a oferit-o Azul, destul de neobișnuită: cache-urile mici sunt în regulă. 

Cea mai mare provocare din viața mea

Vladimir: Ce zici de provocările non-tehnice?

Cliff: Cea mai mare provocare a fost să nu fiu… bun și plăcut cu oamenii. Ca urmare, m-am aflat constant în situații extrem de conflictuale. Cele în care știam că totul merge prost, dar nu știam cum să înaintesc în rezolvarea acestor probleme și nu am reușit să fac față lor. Multe probleme de lungă durată, care au durat zeci de ani, au apărut tocmai în acest fel. Faptul că în Java există compilatoare C1 și C2 este o consecință directă a acestui lucru. De asemenea, faptul că în Java, timp de un deceniu, nu a existat compilare multi-nivel este tot o consecință directă. Este evident că aveam nevoie de un astfel de sistem, dar nu este clar de ce nu a existat. Am avut probleme cu un inginer… sau cu un grup de ingineri. Cu mult timp în urmă, când am început să lucrez la Sun, eram… Bine, nu doar atunci, am avut mereu o opinie proprie despre tot. Și am considerat că este o adevăr absolut că se poate lua această adevăr și spune simplu. Mai ales că de cele mai multe ori aveam dreptate în mod șocant. Iar dacă îți displace această abordare… mai ales dacă te înșeli evident și faci prostii… În general, puțini oameni puteau suporta o astfel de formă de comunicare cu toleranță. Deși unii puteau, de exemplu, eu. Mi-am construit întreaga viață pe principii meritocratice. Dacă îmi arăți ceva greșit, mă voi întoarce imediat și voi spune: ai spus o prostie. Totuși, desigur, mă voi scuza și voi face toate celelalte acțiuni corecte. Pe de altă parte, am fost șocant de corect un procent șocant de mare din timp. Și asta nu funcționează prea bine în relațiile cu oamenii. Nu mă străduiesc să fiu drăguț, ci pun problema pe masa discuției. „Asta nu va funcționa niciodată, pentru că unu, doi și trei”. Și ei sunt: „Oh!”. Au fost și alte consecințe, pe care cel mai bine este probabil să le ocolim: de exemplu, cele care au dus la divorțul de soție și la zece ani de depresie după aceea.

Provocarea este lupta cu percepția oamenilor despre ceea ce poți sau nu poți face, despre ceea ce este important și ceea ce nu este. Au fost multe provocări legate de stilul de codare. Încă mai scriu mult cod și în acele vremuri a trebuit să încetinesc, deoarece făceam prea multe sarcini paralele și le făceam prost, în loc să mă concentrez pe una singură. Privind înapoi, am scris jumătate din codul echipei Java JIT, echipa C2. Următorul coder în viteză scria de două ori mai încet, iar următorul – și mai încet, și a fost o cădere exponențială. Persoana a șaptea din această serie a fost foarte, foarte lentă – așa se întâmplă mereu! Am trecut printr-un morman de cod. M-am uitat cine ce scrie, fără excepții, m-am concentrat pe codul lor, am revizuit pe fiecare dintre ei și încă scriam mai mult decât oricare dintre ei. Cu oamenii, această abordare nu funcționează prea bine. Unii nu îi place acest lucru. Și când nu pot face față, încep diverse reclamații. De exemplu, mi s-a spus odată să încetez să scriu cod, deoarece scriu prea mult cod, iar asta pune în pericol echipa, și pentru mine toate acestea păreau o glumă: omule, dacă întreaga echipă restantă dispare și eu continui să scriu cod, vei pierde doar jumătate din echipă. Pe de altă parte, dacă continui să scriu cod și tu pierzi jumătate din echipă – asta sună ca un management foarte prost. Nu m-am gândit prea mult la asta, nu am vorbit niciodată despre asta, dar totuși a fost undeva în mintea mea. O idee îmi rotea în colțurile conștiinței: „Chiar vă luați în serios?”. Așa că, cea mai mare problemă am fost eu și relațiile mele cu oamenii. Acum mă înțeleg mult mai bine, am fost mult timp team lead la programatori și acum le spun direct oamenilor: știți, sunt așa cum sunt, și va trebui să faceți față – nu-i nimic că stau aici? Și când au avut succes cu asta, totul a început să funcționeze. De fapt, nu sunt nici rău, nici bun, nu am intenții rele sau aspirații egoiste, este pur și simplu esența mea, și trebuie să trăim cu asta.

Andrei: Recent, toată lumea a început să vorbească despre autocunoștință pentru introverți și, în general, despre abilitățile soft. Ce se poate spune despre asta?

Cliff: Da, a fost o înțelegere și o lecție pe care am extrasa din divorțul cu soția. Ceea ce am învățat din divorț este o înțelegere de sine. Astfel, am început să înțeleg alți oameni. Să înțeleg cum funcționează această interacțiune. A dus la descoperiri una după alta. A apărut conștientizarea a cine sunt și ce reprezint. Ce fac: fie că sunt preocupat de sarcini, fie că evit conflictele, sau altceva – și acest nivel de conștientizare de sine mă ajută cu adevărat să mă mențin în frâu. După aceea, totul devine mai simplu. O chestiune pe care am descoperit-o nu doar la mine, ci și la alți programatori – imposibilitatea de a verbaliza gândurile atunci când te afli într-o stare de stres emoțional. De exemplu, stai și codifici, ești într-o stare de flux, și apoi cineva vine și începe să strige disperat că ceva s-a stricat, și că acum vor fi luate măsuri extreme. Și nu poți să spui un cuvânt pentru că te găsești într-o stare de stres emoțional. Cunoștințele acumulate te ajută să te pregătești pentru acel moment, să-l trăiești și să treci la un plan de retragere, după care poți face ceva. Așadar, da, când începi să conștientizezi cum funcționează totul – este un eveniment cu adevărat semnificativ care îți poate schimba viața. 
Eu personal nu am reușit să găsesc cuvintele potrivite, dar am reținut secvența acțiunilor. Esența este că această reacție este la fel de fizică, pe cât este și verbală, și ai nevoie de spațiu. Un astfel de spațiu, în sens zen. Exact asta trebuie explicat, iar apoi imediat să te retragi – pur și simplu să te retragi fizic. Când tac, pot procesa situația din punct de vedere emoțional. Pe măsură ce adrenalina ajunge la creier, te comută în modul „fugi sau luptă”, nu mai poți să vorbești, nu – acum ești un idiot, un inginer de bătaie, incapabil să oferi un răspuns demn sau să oprești atacul, iar atacatorul poate lovi liber din nou și din nou. Trebuie, mai întâi, să devii din nou ceea ce erai, să recâștigi controlul, să ieși din modul „fugi sau luptă”.

Și pentru asta este necesar un spațiu verbal. Pur și simplu un spațiu liber. Dacă totuși trebuie să spui ceva, poți pur și simplu să declari asta și apoi să mergi și să îți găsești „spațiul”: să te plimbi prin parc, să te închizi în duș – nu contează. Important este să te deconectezi temporar de la situația respectivă. Odată ce reușești să te deconectezi măcar pentru câteva secunde, controlul revine, începi să gândești clar. „Bine, nu sunt vreun idiot, nu fac lucruri stupide, sunt o persoană destul de utilă.” Odată ce ai reușit să te convingi pe tine însăți, este timpul să treci la următoarea etapă: să înțelegi ce s-a întâmplat. Ai fost atacat, atacul a venit dintr-o direcție neașteptată, a fost o ambuscadă nedreaptă și ticăloasă. Asta e rău. Pasul următor este să înțelegi de ce atacatorul a simțit nevoia să facă asta. Chiar, de ce? Poate pentru că el însuși este furios? De ce e furios? De exemplu, pentru că a greșit și nu poate accepta responsabilitatea? Așa trebuie să abordezi întreaga situație cu delicatețe. Dar pentru asta ai nevoie de un spațiu de manevră, un spațiu verbal. Primul pas este să întrerupi contactul verbal. Să te îndepărtezi de discuția verbală. Să anulezi asta, să pleci cât mai repede. Dacă este o conversație telefonică – pur și simplu închide telefonul – asta este o abilitate pe care am învățat-o din comunicarea cu fosta mea soție. Dacă discuția nu duce la nimic bun, pur și simplu spune „la revedere” și închide. De cealaltă parte a telefonului: „bla-bla-bla”, tu răspunzi: „aha, pa!” și închizi. Pur și simplu întrerupi conversația. Cinci minute mai târziu, când îți revine capacitatea de a gândi clar, te calmezi puțin, devine posibil să reflectezi asupra a ceea ce s-a întâmplat și ce va urma. Și să începi să formulezi un răspuns bine gândit, nu doar să reacționezi din emoții. Pentru mine, o revelație în auto-conștientizare a fost tocmai aceea că, în cazul stresului emoțional, nu pot vorbi. Să ieși din această stare, să gândești și să planifici cum să răspunzi și să compensezi problemele – acestea sunt pașii corecți atunci când nu poți vorbi. Cel mai simplu mod este să fugi de situația care generează stres emoțional și pur și simplu să încetezi să mai participi în acel stres. După aceea, dobândești capacitatea de a gândi, când poți gândi, apare posibilitatea de a vorbi și așa mai departe.

Între timp, avocatul părții adverse încearcă să facă asta cu tine – acum este clar de ce. Pentru că el are posibilitatea de a te suprima până în așa măsură încât să nu poți nici măcar să-ți spui numele, de exemplu. În cel mai direct sens, nu vei putea vorbi. Dacă ți se întâmplă acest lucru și știi că te vei afla într-un loc unde se desfășoară bătălii verbale, într-un loc precum instanța, poți veni cu avocatul tău. Avocatul se va interpune în apărarea ta și va opri atacul verbal, și va face asta în mod legal, iar ție ți se va reda spațiul zen pierdut. De exemplu, a trebuit să sun de câteva ori familia, iar judecătorul a fost destul de prietenos cu asta, dar avocatul părții adverse a strigat și a strigat la mine, nu am putut să intervin cu nicio vorbă. În astfel de cazuri, cel mai bine pentru mine funcționează folosirea unui mediator. Mediatorul oprește toată această presiune care curge peste tine într-un flux continuu, redescoperi spațiul zen necesar, iar împreună cu el revine capacitatea de a vorbi. Este un domeniu întreg de cunoștințe în care trebuie să înveți multe, să descoperi multe în tine, iar toate acestea se transformă în soluții strategice de nivel înalt, diferite pentru diferite persoane. Unii nu au problemele descrise mai sus, de obicei, nu au oameni care se ocupă profesional de vânzări. Toți acești oameni care își câștigă existența din cuvinte – cântăreți celebri, poeți, lideri religioși și politicieni, au întotdeauna ceva de spus. Ei nu au astfel de probleme, dar eu le am.

Andrei: A fost… neașteptat. Perfect, am discutat destul de mult și e timpul să încheiem acest interviu. Ne vom întâlni cu siguranță la conferință și vom putea continua acest dialog. Ne vedem la Hydra!

Poți continua discuția cu Cliff la conferința Hydra 2019, care va avea loc pe 11-12 iulie 2019 în Sankt Petersburg. El va veni cu o prezentare „Experiența Azul Hardware Transactional Memory”. Biletele pot fi achiziționate de pe site-ul oficial.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster