Cliff Click — CTO van Cratus (IoT-sensoren ter verbetering van processen), oprichter en mede-oprichter van verschillende startups (waaronder Rocket Realtime School, Neurensic en H2O.ai) met meerdere succesvolle exits. Cliff schreef zijn eerste compiler op 15-jarige leeftijd (Pascal voor TRS Z-80)! Het meest bekend geworden door zijn werk aan C2 in Java (de Sea of Nodes IR). Deze compiler toonde de wereld aan dat JIT kwalitatieve code kan genereren, wat een van de factoren was die Java vestigde als een van de belangrijkste moderne softwareplatforms. Vervolgens hielp Cliff Azul Systems met het bouwen van een 864-core mainframe met software in pure Java, dat GC-pauzes op een 500-gigabyte heap binnen 10 milliseconden ondersteunde. Over het algemeen heeft Cliff aan alle aspecten van de JVM gewerkt.
Deze hub-post is een uitgebreid interview met Cliff. We zullen de volgende onderwerpen bespreken:
- Overgang naar low-level optimalisaties
- Hoe een grote refactoring uit te voeren
- Kostenmodel
- Leren over low-level optimalisaties
- Praktische voorbeelden van prestatieverbetering
- Waarom je je eigen programmeertaal zou creëren
- Carrière van een performance-engineer
- Technische uitdagingen
- Een beetje over registerallocatie en parallelisme
- De grootste uitdaging in het leven
Het interview wordt geleid door:
- Andrey Satarin van Amazon Web Services. In zijn carrière heeft hij gewerkt aan verschillende projecten: hij heeft een gedistribueerde NewSQL-database getest bij Yandex, een clouddetectiesysteem bij Kaspersky Lab, een multiplayer-game bij Mail.ru en een valutaprijscalculator bij Deutsche Bank. Hij is geïnteresseerd in het testen van grootschalige backend- en gedistribueerde systemen.
- Ik kan een super-inzicht delen dat zelfs de directe deelnemers aan de geschiedenis misschien niet hebben opgemerkt. Eli Gafni sprak op de avond van de eerste dag van de School, en op de volgende dag bleef hij en begon hij Lamport te plagen, en van de buitenkant leek het alsof dit gekkenwerk was en Eli niet in orde was. Alsof het een troll was die het had gemunt op Leslie's verstand. In werkelijkheid blijkt dat ze bijna de beste vrienden zijn, al jaren bevriend, en dit waren gewoon zulke vriendschappelijke plagerijen. Dus de grap werkte — iedereen om hen heen nam het voor waarheid aan. van Netcracker. Hij werkt al tien jaar aan de prestaties en schaalbaarheid van NetCracker OS — software die door telecomoperators wordt gebruikt om netwerkbeheerprocessen en apparatuur te automatiseren. Hij is geïnteresseerd in de prestaties van Java en Oracle Database. Auteur van meer dan tien prestatieverbeteringen in de officiële PostgreSQL JDBC-driver.
Overgang naar low-level optimalisaties
Andrey: U bent een bekend persoon in de wereld van JIT-compilatie, Java en prestaties in het algemeen, toch?
Cliff: Klopt helemaal!
Andrey: Laten we beginnen met algemene vragen over prestatieoptimalisatie. Wat denkt u van de keuze tussen high-level en low-level optimalisaties, zoals werken op CPU-niveau?
Cliff: Het is hier best eenvoudig. De snelste code is code die nooit wordt uitgevoerd. Daarom moet je altijd beginnen op een hoog niveau en werken aan algoritmen. Een betere O-notatie zal een slechtere O-notatie verslaan, tenzij er grote constanten in het spel zijn. Laag-niveau dingen komen als laatste. Gewoonlijk, als je de hele andere stack goed genoeg hebt geoptimaliseerd en er blijft nog iets interessants over - dat is het lage niveau. Maar hoe begin je op hoog niveau? Hoe weet je dat je genoeg werk op hoog niveau hebt geleverd? Nou... dat weet je niet. Er zijn geen kant-en-klare recepten. Je moet het probleem begrijpen, beslissen wat je gaat doen (zodat je niet onnodige stappen zet) en pas dan kun je de profiler gebruiken die iets nuttigs kan zeggen. Op een gegeven moment begrijp je zelf dat je van de onnodige dingen af bent en dat het tijd is om fijne afstemming op laag niveau te doen. Het is zeker een bijzondere vorm van kunst. Veel mensen doen onnodige dingen, maar bewegen zo snel dat ze geen tijd hebben om zich om de prestaties te bekommeren. Maar dat is totdat de kwestie echt aan de orde komt. Gewoonlijk is het 99% van de tijd voor niemand interessant wat ik doe, totdat er iets belangrijks op het kritiek pad komt waar iemand zich om bekommert. En dan beginnen ze je te vragen: 'Waarom werkte het in eerste instantie niet perfect?'. Kortom, er is altijd iets te verbeteren in de prestaties. Maar 99% van de tijd heb je geen haakjes! Je probeert gewoon iets te laten werken en begrijpt in het proces wat belangrijk is. Je kunt nooit van tevoren weten dat je dit deel perfect moet maken, daarom moet je in wezen perfect zijn in alles. En dat is onmogelijk, en dat doe je ook niet. Er zijn altijd veel dingen om te repareren - en dat is volkomen normaal.
Hoe een grote refactoring uit te voeren
Andrey: Hoe werk je aan de prestaties? Het is immers een alomvattend probleem. Heb je bijvoorbeeld gewerkt aan problemen die voortvloeien uit de interactie van veel bestaande functionaliteit?
Cliff: Ik probeer dit te vermijden. Als ik weet dat de prestaties een probleem zullen worden, denk ik erover na voordat ik begin met coderen, vooral met datatypes. Maar vaak ontdek je dit pas veel later. En dan moet je drastische maatregelen nemen en doen wat ik 'schrijf opnieuw en heers' noem: je moet een groot stuk vastgrijpen. Een deel van de code moet hoe dan ook herschreven worden vanwege prestatieproblemen of om een andere reden. Welke reden er ook voor de herschrijving van de code is, het is bijna altijd beter om een groter stuk te herschrijven dan een kleiner stuk. Op dat moment begint iedereen van angst te trillen: 'Oh mijn God, je kunt zoveel code niet raken!'. Maar in feite werkt deze aanpak bijna altijd veel beter. Je moet meteen een groot probleem aanpakken, er een grote cirkel omheen trekken en zeggen: alles binnen die cirkel herschrijf ik. De grens is immers veel kleiner dan de inhoud die erin vervangen moet worden. En als zo'n afgebakening het mogelijk maakt om het werk binnenin perfect te doen - je hebt de vrije hand, doe wat je wilt. Zodra je het probleem begrijpt, gaat het herschrijven veel gemakkelijker, dus neem een groot stuk!
Tegelijkertijd, wanneer je een grote herschrijving doet en je beseft dat prestaties een probleem zullen worden, kun je je daar meteen zorgen over maken. Vaak verandert dit in simpele dingen zoals 'kopieer geen gegevens, beheer gegevens zo eenvoudig mogelijk, maak ze kleiner'. Bij grote herschrijvingen zijn er standaard manieren om de prestaties te verbeteren. En ze draaien bijna altijd om gegevens.
Kostenmodel
Andrey: In een van de podcasts sprak je over kostmodellen in de context van prestaties. Kun je uitleggen wat je daarmee bedoelt?
Cliff: Natuurlijk. Ik ben geboren in een tijdperk waarin de prestaties van de processor van essentieel belang waren. En dat tijdperk komt weer terug – de ironie van het lot. Ik begon te leven in de tijd van achtbitcomputers, mijn eerste computer werkte met 256 bytes. Inderdaad, bytes. Alles was heel klein. Je moest instructies tellen en zodra we omhoog gingen in de stapel programmeertalen, namen de talen steeds meer op zich. Er was Assemblage, daarna Basic, daarna C, en C nam de taak op zich om met veel details om te gaan, zoals het toewijzen van registers en het optimaliseren van instructies. Maar alles was vrij duidelijk en als ik een pointer naar een instantie van een variabele maakte, dan kreeg ik een load, en de kosten van die instructie zijn bekend. De hardware levert een bekend aantal machinecycli, dus de snelheid van de uitvoering van verschillende taken kun je gewoon berekenen door alle instructies op te tellen die je van plan was uit te voeren. Elke compare/test/branch/call/load/store kon je optellen en zeggen: dit is de tijd voor de uitvoering. Bij het verbeteren van de prestaties zul je zeker opmerken welke getallen overeenkomen met kleine, hete cycli.
Maar zodra je overschakelt naar Java, Python en soortgelijke dingen, afstand je heel snel van de low-level hardware. Wat zijn de kosten van het aanroepen van een getter in Java? Als JIT in HotSpot alles goed , dan zal dit een load zijn, maar als hij dat niet heeft gedaan – dan is het een functieaanroep. Aangezien de aanroep plaatsvindt in een heet cyclus, zal dit alle andere optimalisaties in die cyclus tenietdoen. Daarom zullen de werkelijke kosten veel hoger zijn. En je verliest onmiddellijk de mogelijkheid om naar een stuk code te kijken en te begrijpen wat het ons kost om dit uit te voeren in termen van kloksnelheid van de processor, gebruikt geheugen en cache. Dit wordt interessant alleen als je echt in de prestaties duikt.
We are currently in a situation where processor speeds have hardly increased for a decade. The old days are back! You can no longer rely on good single-threaded performance. But if you suddenly delve into parallel computing – it’s incredibly complex, and everyone looks at you like you’re James Bond. Tenfold speedups typically occur in instances where something was overlooked. Parallelism requires a lot of effort. To achieve that tenfold boost, you need to understand the cost model. What costs what and how much. And for that, you have to understand how the language maps onto the underlying hardware.
Martin Thompson found an excellent word for his blog. ! It is essential to understand what the hardware is going to do, how it will do it, and why it even does what it does. Using this knowledge, you can easily start counting instructions and figuring out where execution time is leaking away. If you don’t have the right background, you’re just searching for a black cat in a dark room. I constantly see people optimizing performance who have no idea what the hell they are doing. They struggle greatly and don't make much progress. And when I take the same piece of code, apply a few minor hacks, and achieve a fivefold or tenfold increase, they say, well, that's not fair, we already knew you were better. Amazing. What was I saying… the cost model is about what code you write and how quickly it generally runs in the big picture.
Andrey: And how does one manage to hold that much in their head? Is it achieved through a lot of experience, or? Where does such experience come from?
Cliff: Well, I gained my experience not in the easiest way. I programmed in Assembly back when it was possible to understand every single instruction. That sounds silly, but since then I've permanently retained a set of Z80 instructions in my mind. I can't remember people’s names a minute after a conversation, but I remember code written 40 years ago. Funny enough, it looks like the "».
Leren over low-level optimalisaties
Andrey": Is there a simpler way to get into this?
Cliff: Ja en nee. De hardware die we allemaal gebruiken, is in deze tijd niet zozeer veranderd. Iedereen gebruikt x86, behalve smartphones die op Arm draaien. Als je je niet bezighoudt met hardcore embedded systemen, heb je nog steeds hetzelfde. Goed, verder. Instructies zijn ook al eeuwen niet veranderd. Je moet gaan en iets schrijven in Assembly. Een klein beetje, maar genoeg om het te begrijpen. Je lacht, maar ik spreek volkomen serieus. Je moet het verband tussen de taal en de hardware begrijpen. Daarna moet je beginnen met het schrijven en een kleine speelgoedcompiler maken voor een kleine speelgoedtaal. 'Speelgoed' betekent dat je het binnen een redelijke tijd moet kunnen maken. Het kan super eenvoudig zijn, maar het moet instructies genereren. Het genereren van instructies zal helpen om het kostenmodel te begrijpen van de brug tussen de hoog-niveau code, waar iedereen in schrijft, en de machinecode die op de hardware draait. Dit verband wordt echt duidelijk wanneer je de compiler schrijft. Zelfs voor de simpelste compiler. Daarna kun je gaan kijken naar Java en beseffen dat haar semantische kloof veel dieper is, en dat het veel moeilijker is om bruggen over die kloof te bouwen. In Java is het veel moeilijker om te begrijpen of onze brug goed of slecht is, wat ervoor zorgt dat deze instort en wat niet. Maar je hebt een soort van vertrekpunt nodig, wanneer je naar de code kijkt en begrijpt: 'ah, deze getter moet elke keer inline gezet worden'. En dan blijkt dat het soms inderdaad zo is, behalve wanneer de methode te groot wordt, en JIT begint alles in te lijn. De prestaties van zulke plekken kun je onmiddellijk voorspellen. Gewoonlijk werken getters goed, maar dan kijk je naar grote warme lussen en realiseert je je dat er vreemde functieaanroepen zijn waar je niet precies weet wat ze doen. Dit is de uitdaging met het alomtegenwoordige gebruik van getters, de reden waarom ze niet inline worden gezet - het is niet duidelijk of het een getter is. Als je een super kleine codebase hebt, kun je alles gewoon onthouden en dan zeggen: 'dit is een getter, en dit is een setter'. In een grote codebase heeft elke functie zijn eigen geschiedenis, die verder niemand bekend is. De profiler zegt dat we 24% van de tijd op een bepaalde lus verloren hebben en om te begrijpen wat die lus doet, moet je elke functie binnenin bekijken. Het is onmogelijk om dat te begrijpen zonder de functie te bestuderen, en dat vertraagt het begripproces aanzienlijk. Daarom gebruik ik geen getters en setters, ik ben naar een nieuw niveau gegaan!
Waar vind je een kostenmodel? Je kunt er natuurlijk wat over lezen... Maar ik denk dat de beste manier is om in actie te komen. Maak een kleine compiler en dat zal de beste manier zijn om het kostenmodel te begrijpen en het in je hoofd te krijgen. Een kleine compiler die geschikt zou zijn voor het programmeren van een magnetron - dat is een taak voor beginners. Nou, ik bedoel, als je al programmeervaardigheden hebt, zou dat voldoende moeten zijn. Al die dingen zoals het parseren van een string die een soort algebraïsch expressie is, het extraheren van instructies voor wiskundige operaties in de juiste volgorde, de juiste waarden uit registers halen - dat gaat heel snel. En terwijl je dit doet, wordt het in je hoofd geprent. Ik denk dat iedereen weet wat een compiler doet. En dit zal een begrip van het kostenmodel opleveren.
Praktische voorbeelden van prestatieverbetering
Andrey: Waar moet je nog meer op letten bij het werken aan prestaties?
Cliff: Gegevensstructuren. Trouwens, ik heb deze lessen al een tijd niet meer gegeven... . Het was leuk, maar het kostte zoveel moeite, en ik heb ook nog een leven! Goed. Dus tijdens een van de grote en interessante lessen, "Waar gaat uw performance heen", gaf ik de studenten een voorbeeld: tweeënhalf gigabyte aan fintech-gegevens werd uit een CSV-bestand gelezen, en vervolgens moest het aantal verkochte producten worden berekend. Gewone tick marktgegevens. UDP-pakketten die zijn omgezet naar tekstformaat, beginnen in de jaren '70. Chicago Mercantile Exchange - dingen zoals olie, maïs, sojabonen, en dat soort dingen. We moesten deze producten tellen, het aantal deals, het gemiddelde bedrag van de geld- en goederenstromen, enz. Dit is vrij eenvoudige handelswiskunde: zoek de productcode (dat zijn 1-2 tekens in een hashtabel), krijg de som, voeg die toe aan een van de transactiesets, voeg het volume toe, voeg de waarde toe, en een paar andere dingen. Erg eenvoudige wiskunde. De speelgoedimplementatie was heel rechttoe rechtaan: alles ligt in een bestand, ik lees het bestand en beweeg er doorheen, splits afzonderlijke records in Java-strings, zoek de nodige dingen eruit en tel ze op volgens de hierboven beschreven wiskunde. En dat werkt met een bepaalde snelheid.
Met deze aanpak is het duidelijk wat er aan de hand is, en parallelle berekeningen zullen hier niet helpen, toch? Het blijkt dat je een vijfvoudige prestatieverbetering kunt behalen door simpelweg de juiste datastructuren te kiezen. Dit verrast zelfs ervaren programmeurs! In mijn specifieke geval lag de focus op het vermijden van geheugenallocaties in een heet lus. Nou, dat is niet de hele waarheid, maar in het algemeen - vermijd het alloceren "één keer per X", wanneer X groot genoeg is. Wanneer X tweeënhalve gigabyte is, moet je niets alloceren "één keer per letter", of "één keer per regel", of "één keer per veld", niets van dat alles. Dit kost tijd. Hoe werkt dit eigenlijk? Stel je voor dat ik een aanroep doe String.split() of BufferedReader.readLine(). Readline maakt een string van een reeks bytes die via het netwerk zijn ontvangen, één keer voor elke regel, voor elk van de honderd miljoen regels. Ik neem deze regel, analyseer deze en gooi deze weg. Waarom gooi ik het weg - nou, ik heb het al verwerkt, dat is het. Dus, voor elke byte die ik lees uit die 2.7G, worden er twee tekens in de string geschreven, dat is al 5.4G, en ik heb ze verder voor niets nodig, dus worden ze weggegooid. Als je kijkt naar de geheugendoorvoer, laden we 2.7G, die door het geheugen en de geheugenbus naar de processor komen, en dus worden er twee keer zoveel naar de string in het geheugen gestuurd, en dat alles wordt weer gemengd bij het creëren van elke nieuwe string. Maar ik moet het toch lezen, de hardware leest het, zelfs als het daarna weer gemengd wordt. En ik moet het opslaan, omdat ik een string heb gemaakt en de caches zijn volgelopen - de cache kan geen 2.7G bevatten. Dus, voor elke gelezen byte lees ik nog eens twee extra bytes en schrijf ik twee extra bytes, en daarmee krijgen we een verhouding van 4:1 - zo verspillen we onnodig de geheugendoorvoer. En vervolgens blijkt dat als ik String.split() dit doe, doe ik het zeker niet voor de laatste keer, er binnen kunnen nog 6-7 velden zijn. Daarom leidt klassieke CSV-leescodes met daaropvolgende stringparsing tot verliezen in geheugendoorvoer van rond de 14:1 in vergelijking met wat je eigenlijk zou willen hebben. Als we deze allocaties weggooien, kun je een vijfvoudige versnelling krijgen.
En dat is niet zo moeilijk. Als je naar de code kijkt vanuit de juiste invalshoek, wordt alles behoorlijk eenvoudig zodra je de kern van het probleem begrijpt. Je moet helemaal niet stoppen met het toewijzen van geheugen: het probleem is alleen dat je iets toewijst en het onmiddellijk sterft, terwijl het belangrijke middelen opbrandt, in dit geval de geheugenbandbreedte. Dit leidt tot een slechte prestatie. Op x86 moet je doorgaans actief de klokcycli van de processor verbruiken, maar hier heb je al het geheugen veel eerder opgebrand. De oplossing is om het aantal toewijzingen te verminderen.
Een ander deel van het probleem is dat, als je de profiler start wanneer de geheugenbandbreedte op is, precies op het moment dat dit gebeurt, je doorgaans hoopt dat de cache terugkeert, omdat deze vol staat met de rommel die je net hebt geproduceerd, al deze regels. Daarom wordt elke load- of store-bewerking traag, omdat ze resulteren in cache-misses – de hele cache is traag geworden, terwijl hij wacht tot de rommel eruit gaat. Daarom geeft de profiler alleen maar een warme willekeurige ruis weer, verspreid over de hele cyclus – er zal geen enkele aparte hot instructie of plaats in de code zijn. Alleen maar ruis. En als je naar de GC-cycli kijkt, zullen ze allemaal van de Young Generation zijn en supersnel – microseconden of milliseconden maximaal. Al dat geheugen sterft immers onmiddellijk. Je wijst miljarden gigabytes toe, en hij snijdt ze af, en snijdt ze af, en snijdt ze weer af. Dit gebeurt heel snel. Dus er zijn goedkope GC-cycli, warme ruis over de hele cyclus, maar we willen een versnelling van vijf keer. Op dat moment zou er iets in je hoofd moeten klikken en klinken: 'waarom is dat zo?!'. Een overloop van de geheugenbandbreedte wordt niet weergegeven in de klassieke debugger; je moet de hardware performance counters debugger starten en het zelf en direct zien. Indirect kun je dit vermoeden uit deze drie symptomen. Het derde symptoom is wanneer je kijkt naar wat je toewijst, de profiler vraagt, en hij antwoordt: 'Je hebt een miljard regels gedaan, maar de GC werkte gratis'. Zodra dit gebeurt, begrijp je dat je te veel objecten hebt geproduceerd en de hele geheugenbandbreedte hebt verbrand. Er is een manier om dit te begrijpen, maar die is niet voor de hand liggend.
Probleem met de datastructuur: de kale structuur die aan alles ten grondslag ligt, is te groot, met 2,7 GB op de schijf, dus het is zeer ongewenst om een kopie van dit ding te maken - je wilt het meteen vanuit de netwerke bytes-buffer naar de registers laden, zodat je niet vijf keer daarheen-en-weer hoeft te lezen en schrijven. Helaas biedt Java standaard zo'n bibliotheek niet in de JDK. Maar dit is triviaal, toch? Eigenlijk gaat het om 5-10 regels code die zullen worden gebruikt voor de implementatie van een eigen gebufferde regel-lader, die het gedrag van de string klasse herhaalt, terwijl het tegelijkertijd een wrapper is rond de onderliggende bytes-buffer. Als resultaat lijkt het alsof je bijna met strings werkt, maar in werkelijkheid bewegen er zich pointers naar de buffer, en worden rauwe bytes nergens gekopieerd, waardoor dezelfde buffers keer op keer worden hergebruikt, terwijl het besturingssysteem gelukkig de taken op zich neemt waarvoor het bedoeld is, zoals verborgen dubbele buffering van deze bytes-buffers, en jijzelf maalt niet langer een eindeloze stroom ongewenste gegevens. Trouwens, jullie begrijpen toch dat bij het werken met GC wordt gegarandeerd dat elke geheugenallocatie niet zichtbaar is voor de processor na de laatste GC-cyclus? Daarom kan dit helemaal niet in de cache zitten, en gebeurt er een gegarandeerde miss van 100%. Bij het werken met een pointer, op x86, kost het 1-2 cycli om een register uit het geheugen te lezen, en zodra dat gebeurt, betaal je, betaal je, betaal je, omdat al het geheugen op – en dit is de werkelijke kosten van geheugenallocatie. De echte kosten.
Met andere woorden, databastructuren zijn het moeilijkst te veranderen. En zodra je je realiseert dat je de verkeerde databastructuur hebt gekozen, die de prestaties in de toekomst zal verpesten, is er meestal substantieel werk nodig om dit te verhelpen. Maar als je dat niet doet, wordt het alleen maar erger. Het is vooral belangrijk om na te denken over databastructuren. De grootste kosten komen voort uit onhandige databastructuren, waarbij men in de stijl van 'ik heb databastructuur X gekopieerd naar databastructuur Y, omdat ik Y esthetisch aantrekkelijker vind' handelt. Maar de kopieeroperatie (die goedkoop lijkt) verbruikt eigenlijk geheugenbandbreedte, en daar ligt alle verloren uitvoertijd. Als ik een gigantische JSON-string heb en ik wil deze omzetten naar een gestructureerde DOM-boom van POJO of iets dergelijks, dan zal het parsen van deze string en het opbouwen van POJO, en vervolgens opnieuw toegang krijgen tot de POJO later extra kosten met zich meebrengen – dat is niet goedkoop. Tenzij je veel vaker door de POJO wilt lopen dan door de string. Uit de losse pols kun je in plaats daarvan proberen de string te decoderen en alleen wat nodig is eruit te trekken, zonder het om te zetten naar POJO. Als dit alles gebeurt op een pad waar maximale prestaties vereist zijn, dan is er helemaal geen POJO nodig – dan moet je direct in de string werken.
Waarom je je eigen programmeertaal zou creëren
Andrey: Je zei dat om het prijsmodel te begrijpen, je je eigen kleine taal moet schrijven…
Cliff: Geen taal, maar een compiler. Taal en compiler zijn verschillende dingen. Het belangrijkste verschil ligt in je hoofd.
Andrey: Trouwens, voor zover ik weet, experimenteren jullie met het maken van jullie eigen talen. Waarom?
Cliff: Omdat ik het kan! Ik ben half met pensioen, dus dit is mijn hobby. Mijn hele leven heb ik andermans talen gerealiseerd. Daarnaast heb ik veel gewerkt aan de stijl van coderen. Ook omdat ik problemen zie in andere talen. Ik zie dat er betere manieren zijn om gebruikelijke dingen te doen. En ik zou die gebruiken. Ik ben gewoon moe om problemen in mezelf, in Java, in Python, of in een andere taal te zien. Momenteel schrijf ik in React Native, JavaScript en Elm als een hobby die niet over pensioen gaat, maar over actief werk. En ik schrijf ook in Python en waarschijnlijk blijf ik werken aan machine learning voor Java-backends. Er zijn veel populaire talen en ze hebben allemaal interessante kenmerken. Ieder is op zijn eigen manier goed en je kunt proberen al deze functies samen te brengen. Dus, ik verdiep me in dingen die mij interesseren, het gedrag van de taal, ik probeer een redelijke semantiek te bedenken. En tot nu toe lukt dat! Op dit moment worstel ik met de semantiek van geheugen, omdat ik het wil hebben zoals in C en Java, en een sterke geheugenmodel en semantiek voor loads en stores wil krijgen. Tegelijkertijd wil ik automatische type-inferring zoals in Haskell. Kijk, ik probeer Haskell-achtige type-inference te mengen met geheugen dat werkt zoals in C en Java. Daar ben ik de laatste 2-3 maanden mee bezig, bijvoorbeeld.
Andrey: Als je een taal bouwt die de beste aspecten van andere talen overneemt, heb je erover nagedacht dat iemand het omgekeerde zou doen: jouw ideeën zou nemen en ze voor zichzelf gebruiken?
Cliff: Zo ontstaan nieuwe talen! Waarom lijkt Java op C? Omdat C een goede syntaxis had die iedereen begreep, en Java was geïnspireerd door die syntaxis, met typeveiligheid, bounds checking voor arrays, GC, en ze hebben ook enkele zaken uit C verbeterd. Ze hebben hun eigen dingen toegevoegd. Maar ze waren behoorlijk geïnspireerd, nietwaar? Iedereen staat op de schouders van reuzen die voor jou kwamen – zo wordt vooruitgang geboekt.
Andrey: Zoals ik het begrijp, zal jouw taal veilig zijn met betrekking tot geheugen. Heb je nagedacht over het implementeren van iets als borrow checker uit Rust? Heb je ernaar gekeken, wat vind je ervan?
Cliff: Nou, ik programmeer al een eeuwigheid in C, met al die malloc en free, en ik beheer handmatig de tijdslevensduur. Weet je, 90-95% van de handmatig beheerde levensduur heeft dezelfde structuur. En het is heel, heel pijnlijk om dit handmatig te doen. Ik zou willen dat de compiler gewoon kon aangeven wat er aan de hand is en wat je hebt bereikt met je acties. Voor sommige dingen doet de borrow checker dit uit de doos. Bovendien zou hij automatisch informatie moeten afleiden, alles moeten begrijpen en me niet belasten met het verwoorden van dat begrip. Hij moet minimaal een lokale escape-analyse uitvoeren, en alleen als hem dat niet lukt, moeten er typeannotaties worden toegevoegd die de levensduur beschrijven - en zo'n schema is veel ingewikkelder dan de borrow checker, of zelfs maar enige bestaande geheugenchecker. De keuze tussen 'alles is in orde' en 'ik begrijp er niets van' - nee, er moet iets beters zijn.
Dus, als iemand die veel code in C heeft geschreven, vind ik dat het hebben van ondersteuning voor automatische lifetimesbeheer de belangrijkste zaak is. En ik ben ook moe van hoe Java geheugengebruik toespitst. Mijn grootste klacht is over de garbage collector. Bij het toewijzen van geheugen in Java krijg je het geheugen dat lokaal was in de laatste garbage collector-cyclus niet terug. In talen met meer nauwkeurige geheugensystemen is dat niet zo. Als je malloc aanroept, ontvang je onmiddellijk geheugen dat meestal net is gebruikt. Gewoonlijk doe je iets tijdelijk met dat geheugen en geef je het meteen weer terug. Het keert direct terug naar de malloc-pool en de volgende malloc-cyclus haalt het er opnieuw uit. Daarom vermindert het werkelijke geheugengebruik tot een set van levende objecten op een bepaald moment, plus lekken. En als je niet op een volledig ongepaste manier lekt, blijft het grootste deel van het geheugen in caches en de processor, en dat werkt snel. Maar het vereist veel handmatige geheugenbeheertaken met malloc en free, die op de juiste manier en op de juiste plek moeten worden aangeroepen. Rust kan dit op een goede manier zelf afhandelen en kan in veel gevallen zelfs betere prestaties bieden, omdat het geheugengebruik zich beperkt tot de huidige berekeningen – in tegenstelling tot het wachten op de volgende garbage collector-cyclus om het geheugen vrij te geven. Uiteindelijk hebben we een zeer interessante manier gevonden om de prestaties te verbeteren. En behoorlijk krachtig – ik heb met dergelijke dingen gewerkt tijdens dataverwerking voor fintech, en dit stelde ons in staat om een snelheidstoename van ongeveer vijf keer te realiseren. Dat is een behoorlijke boost, vooral in een wereld waarin processors niet sneller worden en we nog steeds wachten op verbeteringen.
Carrière van een performance-engineer
Andrey: Ik wil ook nog wat vragen stellen over de carrière in het algemeen. U bent beroemd geworden door uw werk aan JIT in HotSpot, en vervolgens verhuisd naar Azul – en dat is ook een JVM-bedrijf. Maar u heeft nu meer met hardware gewerkt dan met software. En toen ineens schakelde u over naar Big Data en Machine Learning, en daarna naar fraudedetectie. Hoe is dat gebeurd? Dit zijn zeer verschillende ontwikkelingsgebieden.
Cliff: Ik ben al een behoorlijke tijd bezig met programmeren en heb me op verschillende gebieden onderscheiden. En wanneer mensen zeggen: "oh, jij bent degene die JIT voor Java heeft gemaakt!", is dat altijd grappig. Maar daarvoor werkte ik aan een kloon van PostScript — die taal die Apple ooit gebruikte voor zijn laserprinters. En daarvoor implementeerde ik de taal Forth. Ik denk dat het gemeenschappelijke thema voor mij het ontwikkelen van tools is. Mijn hele leven maak ik tools waarmee andere mensen hun geweldige programma's kunnen schrijven. Maar ik heb ook gewerkt aan de ontwikkeling van besturingssystemen, stuurprogramma's op kernniveau, debuggers, talen voor het ontwikkelen van besturingssystemen, die aanvankelijk eenvoudig waren, maar na verloop van tijd steeds gecompliceerder werden. Maar het belangrijkste thema blijft toch het ontwikkelen van tools. Een groot deel van mijn leven is verstreken tussen Azul en Sun, en dat ging over Java. Maar toen ik me ging bezighouden met Big Data en Machine Learning, zette ik mijn feesthoed weer op en zei: "Hé, nu hebben we een niet-triviaal probleem, en hier gebeuren ontzettend veel interessante dingen en mensen die iets doen." Dit is een geweldige weg voor ontwikkeling die het waard is om te bewandelen.
Ja, ik hou echt van gedistribueerde berekeningen. Mijn eerste baan was tijdens mijn studententijd met C, op een reclameproject. Dit waren gedistribueerde berekeningen op Zilog Z80-chips die gegevens verzamelden voor analoge optische tekstanalyse, uitgevoerd door een echte analoge analyzer. Het was een cool en volkomen ongewoon onderwerp. Maar er waren problemen, een deel werd niet goed herkend, dus je moest de afbeelding ophalen en aan een persoon tonen die het al met het blote oog had gelezen en kon vertellen wat er stond. Daarom waren er taken met gegevens, en deze taken hadden hun eigen taal. Er was een back-end die alles verwerkte - parallel werkende Z80's met draaiende vt100-terminaal, één per persoon, en er was een model voor parallel programmeren op Z80. Een soort gedeeld geheugen, dat door alle Z80's binnen een sterconfiguratie werd gedeeld; de backplane werd ook gedeeld, en de helft van het RAM werd binnen het netwerk gedeeld, terwijl de andere helft privé was of voor iets anders ging. Een betekenisvol complexe parallelle gedistribueerde systeem met gedeeld... semi-gedeeld geheugen. Wanneer was dit... Het is al zo lang geleden, ergens halverwege de jaren '80. Best lang geleden.
Ja, laten we zeggen dat 30 jaar lang genoeg geleden is. Taken die verband houden met gedistribueerde berekeningen bestaan al een lange tijd, mensen hebben altijd gestreden met -clusters. Dergelijke clusters zien eruit als… Bijvoorbeeld: je hebt Ethernet en je snelle x86 is verbonden met deze Ethernet, en nu wil je valse gedeelde geheugen verkrijgen, omdat niemand toen kon coderen voor gedistribueerd rekenen, dat was te ingewikkeld en daarom was er valse gedeelde geheugen met pagina-beveiliging op x86, en als je op deze pagina schreef, zeiden we tegen de andere processors dat als ze toegang kregen tot hetzelfde gedeelde geheugen, het opnieuw van jou moest worden geladen, en zo ontstond er iets zoals een protocol voor het ondersteunen van cache-coherentie en software daarvoor. Een interessante concept. Het werkelijke probleem lag natuurlijk ergens anders. Dit alles werkte, maar je kreeg snel prestatieproblemen, omdat niemand het prestatiemodel op een goed genoeg niveau begreep – wat de toegangspatronen tot het geheugen waren, hoe ervoor te zorgen dat de nodes elkaar niet eindeloos pingen, enzovoort.
In H2O heb ik het volgende bedacht: de ontwikkelaars zijn verantwoordelijk voor het bepalen waar parallelisme aanwezig is en waar niet. Ik heb een coderingsmodel bedacht waardoor het gemakkelijk en eenvoudig is geworden om hoogwaardige code te schrijven. Aan de andere kant is het moeilijk om trage code te schrijven; deze zal er slecht uitzien. Je moet flink je best doen om trage code te schrijven, en dat vereist ongebruikelijke methoden. Trage code is met één blik te zien. Als gevolg daarvan schrijven de meesten code die snel werkt, maar je moet wel begrijpen wat je moet doen in het geval van gedeeld geheugen. Dit is allemaal verbonden met grote arrays en het gedrag in deze context lijkt op niet-volatiele grote arrays in parallel Java. Stel je voor dat twee threads in een parallelle array schrijven: de een wint, de ander verliest, en je weet niet wie wie is. Als ze niet-volatieel zijn, kan de volgorde willekeurig zijn – en dat werkt echt goed. Mensen maken zich echt zorgen over de volgorde van operaties; ze plaatsen de volatile correct en verwachten op de juiste plekken prestatieproblemen gerelateerd aan geheugen. Anders zouden ze gewoon code schrijven in de vorm van lussen van 1 tot N, waarbij N enkele triljoenen is, in de hoop dat alle moeilijke gevallen automatisch parallel worden – en daar werkt het niet. Maar H2O is niet Java en niet Scala; je zou het als 'Java min-min' kunnen beschouwen als je dat wilt. Het is een zeer begrijpelijke programmeer stijl en lijkt op het schrijven van eenvoudige code in C of Java met loops en arrays. Tegelijkertijd kan geheugen in terabytes worden verwerkt. Ik gebruik H2O nog steeds. Af en toe gebruik ik het in verschillende projecten – en het is nog steeds het snelste, tientallen keren sneller dan de concurrentie. Als je Big Data doet met kolomdata, is het erg moeilijk om H2O te overtreffen.
Technische uitdagingen
Andrey: Wat is de grootste uitdaging die u in uw carrière heeft gehad?
Cliff: Bespreken we de technische of niet-technische kant van de kwestie? Ik zou zeggen dat de grootste uitdagingen niet-technisch zijn.
Wat betreft technische uitdagingen. Ik heb ze gewoon overwonnen. Ik weet zelfs niet wat de grootste was, maar er waren verschillende best interessante, die veel tijd en mentale strijd kostten. Toen ik naar Sun ging, was ik ervan overtuigd dat ik een snelle compiler zou maken, terwijl veel senioren antwoordden dat ik nooit iets zou bereiken. Maar ik ging die weg op, schreef een compiler tot aan de registerallocator, en hij was behoorlijk snel. Hij was net zo snel als de moderne C1, maar toen was de allocator veel trager. Achteraf gezien was het een probleem van een grote datastructuur. Ik had het nodig om een grafische registerallocator te schrijven en begreep de dilemma's tussen expressiviteit van de code en snelheid die in die tijd bestonden en heel belangrijk waren. Het bleek dat de datastructuren meestal de cachegrootte op de x86 van die tijd overschreden, en dus, als ik aanvankelijk veronderstelde dat de registerallocator 5-10 procent van de tijd tijdens het JIT'en zou vergen, bleek dat in werkelijkheid 50 procent te zijn.
De tijd verstreek, de compiler werd steeds helderder en efficiënter, stopte met het genereren van vreselijke code in meer gevallen, en de prestaties kwamen steeds dichter in de buurt van wat je van een C-compiler zou verwachten. Tenzij je natuurlijk wat rommel schrijft dat zelfs C niet kan versnellen. Als je code schrijft zoals in C, krijg je in meer gevallen net zo veel prestaties als in C. En hoe verder we gingen, hoe vaker de code asymptotisch overeenkwam met het niveau van C, de registerallocator begon meer op een voltooide zaak te lijken… ongeacht of je code snel of langzaam werkte. Ik bleef aan de allocator werken zodat hij betere toewijzingen deed. Hij werd steeds trager, maar leverde steeds betere prestaties in situaties waarin niemand het nog kon bijbenen. Ik kon de registerallocator induiken, daar een maand aan werken, en plotseling begon de hele code 5% sneller te draaien. Dit gebeurde keer op keer en de registerallocator werd iets als een kunstwerk – iedereen hield ervan of haatte het, en mensen van de academie stelden vragen als "waarom wordt alles op deze manier gedaan", waarom niet. , en wat is het verschil. Het antwoord blijft hetzelfde: een op graf kleuren gebaseerde allocator plus zeer zorgvuldige verwerking van de buffercode is gelijk aan een wapen van overwinning, de beste combinatie die niemand kan verslaan. En dit is een vrij onopvallend ding. Alles wat de compiler daar verder doet, zijn vrij bestudeerde zaken, hoewel ze ook tot een kunst zijn ontwikkeld. Ik heb altijd dingen gedaan die de compiler in een kunstwerk moesten veranderen. Maar niets daarvan was buitengewoon — met uitzondering van de registerallocator. De focus ligt op het zorgvuldig onder belasting en, als dat gebeurt (ik kan het verder uitleggen als dat interessant is), betekent het dat je agressiever kunt inlineen, zonder het risico dat je over de piek van de prestatiecurve heen gaat. In die tijd waren er tal van volwaardige compilers, vol met frutsels en toeters, die registerallocators bevatten, maar niemand kon dat ooit weer.
Het probleem is dat als je methoden toevoegt die aan inlining onderhevig zijn, en je de inlining area steeds verder vergroot, de set gebruikte waarden onmiddellijk het aantal registers overtreft, waardoor je moet spoelen. Het kritieke niveau wordt meestal bereikt wanneer de allocator faalt, en een goede kandidaat voor spoelen levert een andere op, je spoelt echt bizarre dingen. De waarde van inlining ligt hierin dat je een deel van de overhead, de overhead van de aanroep en opslag, verliest; je kunt waarden binnenin zien en je kunt ze verder optimaliseren. De kosten van inlining zijn dat er een groot aantal live waarden ontstaat, en als je allocator meer spoelt dan nodig, verlies je direct. Daarom hebben de meeste allocators een probleem: wanneer inlining een bepaald niveau bereikt, begint alles te spoelen en kun je de prestaties in de afvoer gooien. Degenen die de compiler implementeren, voegen bepaalde heuristieken toe: bijvoorbeeld om inlining te stoppen bij een bepaalde grootte, omdat allocaties alles verpesten. Dit leidt tot een knik in de prestatiecurve - je inline, je inline, de prestaties stijgen gestaag - en dan buh! - het zakt als een stenen blok, omdat je te veel hebt ingelined. Dit werkte zo tot de komst van Java. Java vereist veel meer inlining, dus moest ik mijn allocator veel agressiever maken, zodat hij niet zou falen, en als je te veel ingelined hebt - begint hij te spoelen, maar dan komt toch het moment van "geen meer spoeling". Dit is een interessante observatie en het kwam gewoon uit het niets, niet voor de hand liggend, maar het heeft zich goed terugbetaald. Ik heb me toegelegd op agressieve inlining en dat heeft me op plekken gebracht waar de prestaties van Java en C zij aan zij staan. Ze zijn echt dichtbij - ik kan Java-code schrijven die aanzienlijk sneller is dan C-code en dergelijke, maar gemiddeld, in het grote geheel, zijn ze ongeveer vergelijkbaar. Ik denk dat een deel van deze eer toekomt aan de registerallocator, die me in staat stelt om zo dom mogelijk in te linden. Ik inline gewoon alles wat ik zie. De vraag hierbij is of de allocator goed werkt en of de resulterende code redelijk is. Dat was een grote uitdaging: dit alles begrijpen en het werkend krijgen.
Een beetje over registerallocatie en parallelisme
Vladimir: Problemen zoals registerallocatie lijken een eeuwigdurende kwestie te zijn. Ik vraag me af of er ooit een idee was dat veelbelovend leek, maar in de praktijk faalde.
Cliff: Zeker! Registerallocatie is een gebied waar je, om een NP-volledig probleem op te lossen, probeert enkele heuristieken te vinden. En je zult nooit een perfect resultaat kunnen behalen, klopt dat? Dat is gewoon onmogelijk. Kijk, Ahead of Time compilatie – die werkt ook slecht. Het gaat hier om gemiddelde gevallen. Over typische prestaties, zodat je iets kunt nemen en meten wat je als goede typische prestaties beschouwt – tenslotte werk je eraan om dit te verbeteren! Registerallocatie is een onderwerp dat volledig gewijd is aan prestaties. Zodra je een eerste prototype hebt, dat werkt en doet wat het moet doen, begint het werk aan de prestaties. Je moet leren goed te meten. Waarom is dat belangrijk? Als er duidelijke gegevens zijn, kun je naar verschillende delen kijken en zien: aha, dit hielp hier, maar daar ging alles mis! Er komen goede ideeën naar voren, je voegt een nieuwe heuristiek toe en plotseling begint alles gemiddeld iets beter te werken. Of niet. Ik heb talloze gevallen gehad waarin we streden voor vijf procent prestatie die ons ontwerp onderscheidde van de vorige allocator. En elke keer ziet het er zo uit: ergens gewonnen, ergens verloren. Als je goede tools voor prestatieanalyse hebt, kun je de verliezende ideeën vinden en begrijpen waarom ze verliezen. Misschien is het beter om alles zoals het is te laten, of misschien moet je serieuzer aan de fijne afstelling werken, of iets anders repareren. Het is een hele reeks dingen! Ik heb deze coole hack gemaakt, maar ik heb dit, dit en dit ook nodig – en de combinatie ervan levert enkele verbeteringen op. En afzonderlijke dingen kunnen falen. Zo is de natuur van het werken aan de prestaties van NP-volledige problemen.
Vladimir: Het lijkt erop dat dingen zoals kleuren in allocators al een opgelost probleem zijn. Nou, opgelost voor jou, gezien wat je vertelt, dus moet het dan echt…
Cliff: Het is op zich niet opgelost. Jij moet het omzetten in een 'opgeloste' situatie. Er zijn zware taken en die moeten worden aangepakt. Zodra dat is gedaan, is het tijd om aan de prestaties te werken. Je moet deze taak serieus nemen – benchmarks uitvoeren, metrics verzamelen, situaties uitleggen waarbij je teruggaat naar een eerdere versie en je oude hack weer begon te werken (of omgekeerd, niet meer werkte). En niet opgeven totdat je iets bereikt. Zoals ik al zei, als het gaat om coole ideeën die niet hebben gewerkt, zijn er in het domein van registertoewijzing ongeveer oneindig veel ideeën. Je kunt bijvoorbeeld wetenschappelijke publicaties lezen. Hoewel dit gebied nu veel langzamer vooruitgaat en duidelijker is geworden dan in de begintijd. Desondanks werken er een eindeloze hoeveelheid mensen in dit domein en het is de moeite waard om al hun ideeën uit te proberen, ze wachten allemaal op hun kans. En je kunt niet zeggen hoe goed ze zijn als je het niet uitprobeert. Hoe goed ze integreren met de rest in jouw allocator, want de allocator doet veel dingen, en sommige ideeën werken misschien niet in jouw specifieke allocator, maar in een andere allocator absoluut wel. De belangrijkste manier waarop een allocator kan winnen, is door de trage zaken buiten het belangrijkste pad te halen en gedwongen splitsingen te maken langs de grenzen van de trage paden. Dus, als je GC wilt starten, het trage pad wilt volgen, de-optimaliseren, een uitzondering wilt gooien, dat soort dingen – je weet dat deze dingen relatief zeldzaam zijn. En ze zijn inderdaad zeldzaam, dat heb ik gecontroleerd. Je doet extra werk en daardoor verdwijnen veel beperkingen op die trage paden, maar dat is niet zo belangrijk omdat ze traag zijn en er zelden gebruik van wordt gemaakt. Bijvoorbeeld, een null-pointer – dat gebeurt nooit, toch? Je moet verschillende paden hebben voor verschillende dingen, maar ze moeten niet in de weg zitten van het hoofdpad.
Vladimir: Wat denk je van veelkernigheid, wanneer er duizenden kernen tegelijk zijn? Is dat een nuttige zaak?
Cliff: Het succes van GPU's laat zien dat het behoorlijk nuttig is!
Vladimir: Ze zijn vrij gespecialiseerd. En hoe zit het met algemene processoren?
Cliff: Dat was het businessmodel van Azul. Het antwoord kwam in een tijd waarin mensen zeer hielden van voorspelbare prestaties. Het was toen lastig om parallelle code te schrijven. Het H2O-coderingsmodel schaalt goed, maar het is geen algemene model. Het is misschien iets meer algemeen dan bij het gebruik van een GPU. Gaan we het hebben over de complexiteit van het ontwikkelen van zoiets of de complexiteit van het gebruik ervan? Bijvoorbeeld, een interessante les die Azul me leerde, was dat kleine caches prima zijn.
De grootste uitdaging in het leven
Vladimir: Wat betreft de niet-technische uitdagingen?
Cliff: De grootste uitdaging was om niet… vriendelijk en aardig tegen mensen te zijn. En als gevolg daarvan bevond ik me constant in extreem conflictueuze situaties. Situaties waarin ik wist dat alles verkeerd ging, maar niet wist hoe verder te gaan met het oplossen van die problemen en niet in staat was om ze aan te pakken. Vele langdurige problemen, die tientallen jaren duurden, zijn op deze manier ontstaan. Het feit dat er in Java C1- en C2-compilers zijn, is daar een direct gevolg van. Het feit dat er in Java tien jaar lang geen multilayer-compilatie was, is ook een direct gevolg. Het is duidelijk dat we zo'n systeem nodig hadden, maar niet duidelijk waarom het er niet was. Ik had problemen met één ingenieur… of een groep ingenieurs. Lang geleden, toen ik begon te werken bij Sun, was ik… Nou, niet alleen toen, ik heb altijd mijn eigen mening over alles. En ik hield vol dat je gewoon je waarheid kunt zeggen. Vooral omdat ik meestal schokkend gelijk had. En als je zo'n aanpak niet leuk vindt… vooral als je duidelijk fout bent en onzin zegt… Over het algemeen konden maar weinig mensen zo'n communicatiestijl verduren. Hoewel sommige mensen dat konden, bijvoorbeeld ik. Ik heb mijn hele leven opgebouwd op meritocratische principes. Als je me iets verkeerds laat zien, draai ik me meteen om en zeg: je zegt onzin. Daarbij bied ik natuurlijk mijn excuses aan, en dat soort dingen, erken ik verdiensten, als die er überhaupt zijn, en doe ik andere goede dingen. Aan de andere kant heb ik schokkend vaak schokkend gelijk. En dat werkt niet erg goed in relaties met mensen. Ik probeer niet aardig te zijn, maar ik stel de kwestie rechtstreeks aan de orde. 'Dit gaat nooit werken, omdat één, twee en drie.' En zij reageren met: 'Oh!'. Er waren ook andere gevolgen, die ik waarschijnlijk beter kan overslaan: bijvoorbeeld, die hebben geleid tot een scheiding van mijn vrouw en een decennium van depressie daarna.
Een uitdaging is de strijd met mensen, met hun perceptie van wat je wel of niet kunt doen, wat belangrijk is en wat niet. Er zijn veel uitdagingen geweest over coderingsstijl. Ik schrijf nog steeds veel code, en in die tijd moest ik zelfs vertragen omdat ik te veel parallelle taken deed en ze slecht uitvoerde, in plaats van me op één te concentreren. Terugkijkend, heb ik de helft van de code geschreven voor het Java JIT-team, het C2-team. De volgende snelste coder schreef de helft langzamer, de volgende nog eens de helft langzamer, en dat was een exponentiële daling. De zevende persoon in deze rij was heel, heel traag – dat gebeurt altijd! Ik heb veel code bekeken. Ik keek naar wat iedereen schreef, zonder uitzonderingen, ik tuurde in hun code, beoordeelde ze allemaal, en schreef nog steeds meer dan een van hen. De aanpak met mensen werkt niet altijd goed. Sommigen houden daar niet van. En wanneer ze daar niet mee om kunnen gaan, beginnen allerlei klachten. Bijvoorbeeld, een keer werd me verteld te stoppen met coderen, omdat ik te veel code schreef, en dat dit het team in gevaar bracht; dit klonk voor mij als een grap: man, als de hele rest van het team verdwijnt en ik blijf code schrijven, verlies je slechts de helft van het team. Aan de andere kant, als ik blijf coderen en jij verliest de helft van het team – dat klinkt als zeer slecht management. Ik heb hier nooit echt over nagedacht, nooit erover gepraat, maar het was toch ergens in mijn hoofd. Achter in mijn bewustzijn draaide de gedachte rond: 'Gaan jullie serieus?' Dus, het grootste probleem was ik en mijn relaties met mensen. Nu begrijp ik mezelf veel beter, ik heb lang teamleider geweest van programmeurs, en nu zeg ik rechtstreeks tegen mensen: weet je, ik ben zoals ik ben, en jullie moeten met mij omgaan – is het oké als ik hier even sta? En toen ze daarmee omgingen, werkte alles. Want in feite ben ik niet slecht of goed, ik heb geen slechte bedoelingen of egoïstische verlangens, het is gewoon mijn aard, en daar moet je op een of andere manier mee leven.
Andrey: Onlangs ging iedereen praten over zelfbewustzijn voor introverten en over soft skills. Wat kunnen we hierover zeggen?
CliffJa, dat was het inzicht en de les die ik uit mijn scheiding met mijn vrouw heb gehaald. Wat ik uit de scheiding heb geleerd, is zelfinzicht. Zo begon ik anderen beter te begrijpen. Begrijpen hoe de interactie werkt. Dit leidde tot opeenvolgende ontdekkingen. Er ontstond een bewustzijn van wie ik ben en wat ik vertegenwoordig. Wat ik doe: of ik ben gefocust op de taak, of ik vermijd conflict, of iets anders – dat niveau van zelfbewustzijn helpt me echt om mezelf onder controle te houden. Daarna gaat alles veel eenvoudiger. Eén ding dat ik niet alleen bij mezelf, maar ook bij andere programmeurs ontdekte, is de onmogelijkheid om gedachten te verwoorden wanneer je onder emotionele stress staat. Bijvoorbeeld, je zit te coderen, je bent in een flow, en dan komen ze naar je toe en beginnen hysterisch te schreeuwen dat er iets kapot is gegaan en dat er extreme maatregelen zullen worden genomen. En je kunt geen woord zeggen, omdat je in emotionele stress bent. De opgedane kennis helpt je om je voor te bereiden op dat moment, het te doorstaan en naar een terugtrekplan over te schakelen, waarna je iets kunt doen. Dus ja, wanneer je begint te beseffen hoe dit allemaal werkt – dat is een enorm levenveranderend moment.
Zelf kon ik de juiste woorden niet vinden, maar ik herinnerde me de volgorde van acties. Het punt is dat deze reactie net zo fysiek is als verbaal, en je hebt ruimte nodig. Zo'n ruimte, in de zen-zin. Dat is wat je moet uitleggen, en dan onmiddellijk opzij stappen – gewoon fysiek opzij stappen. Wanneer ik stil ben in woorden, kan ik de situatie emotioneel verwerken. Zodra de adrenaline je hersenen bereikt en je in de 'vecht of vlucht'-modus schakelt, kun je niets meer zeggen, nee – nu ben je een idioot, een doelwit, niet in staat om een waardige reactie te geven of om in ieder geval de aanval te stoppen, en de aanvaller kan vrijelijk opnieuw aanvallen. Eerst moet je weer jezelf worden, de controle terugkrijgen en uit de 'vecht of vlucht'-modus komen.
En dit vereist een verbaal ruimte. Gewoon vrije ruimte. Als je überhaupt iets wilt zeggen, kun je precies dat zeggen en dan echt op zoek gaan naar je "ruimte": een wandeling maken in het park, je opsluiten onder de douche - het maakt niet uit. Het belangrijkste is om tijdelijk los te koppelen van die situatie. Zodra je zelfs maar enkele seconden loskoppelt, komt de controle terug en begin je helder te denken. "Goed, ik ben geen idioot, ik doe geen domme dingen, ik ben een redelijk nuttig persoon." Zodra je jezelf ervan hebt overtuigd, is het tijd om naar de volgende fase te gaan: begrijpen wat er is gebeurd. Je werd aangevallen, de aanval kwam van een onverwachte hoek, het was een oneerlijke, gemene hinderlaag. Dat is slecht. De volgende stap is te begrijpen waarom de aanvaller dit nodig had. Inderdaad, waarom? Misschien omdat hij zelf in woede verkeert? Waarom is hij woedend? Bijvoorbeeld omdat hij zelf heeft gefaald en geen verantwoordelijkheid kan nemen? Op deze manier moet je de hele situatie zorgvuldig verwerken. Maar daarvoor heb je manoeuvreerruimte nodig, verbaal ruimte. De allereerste stap is de verbale communicatie te verbreken. Weggaan van de verbale discussie. Het annuleren, zo snel mogelijk weggaan. Als het een telefoongesprek is – leg gewoon de hoorn op, dat is een vaardigheid die ik heb opgedaan uit gesprekken met mijn ex-vrouw. Als het gesprek nergens goed toe leidt, zeg gewoon "doei" en hang op. Aan de andere kant van de lijn: "bla-bla-bla", jij antwoordt: "aha, doei!" en hangt op. Je onderbreekt het gesprek gewoon. Vijf minuten later, wanneer je weer in staat bent om helder te denken, ben je een beetje afgekoeld, kun je nadenken over wat er daadwerkelijk is gebeurd en wat er nu gaat gebeuren. En begin een doordacht antwoord te formuleren in plaats van alleen maar emotioneel te reageren. Voor mij was de doorbraak in zelfbewustzijn precies dat ik in geval van emotionele stress niet kan praten. Uit deze staat komen, nadenken en plannen hoe te antwoorden en compensaties te bieden voor de problemen - dat zijn de juiste stappen als je niet kunt praten. De eenvoudigste manier is om uit de situatie te vluchten waarin emotionele stress zich manifesteert en gewoon niet meer aan die stress deel te nemen. Daarna krijg je weer de mogelijkheid om na te denken, wanneer je kunt nadenken, komt de mogelijkheid om te praten, en ga zo maar door.
Overigens, in de rechtbank probeert de advocaat van de tegenpartij dit met jou te doen - nu is het duidelijk waarom. Omdat hij in staat is jou te onderdrukken tot op het punt dat je zelfs je eigen naam niet kunt uitspreken, bijvoorbeeld. In de meest letterlijke zin, je kunt niet praten. Als dit met jou gebeurt en je weet dat je je in een omgeving bevindt waar verbale gevechten plaatsvinden, zoals een rechtbank, dan kun je met je advocaat komen. De advocaat zal voor je opkomen en de verbale aanval staken, en dat zal op een heel wettelijke manier gebeuren, waardoor je de verloren zen-ruimte terugkrijgt. Bijvoorbeeld, ik moest een paar keer mijn familie bellen, de rechter was daar heel vriendelijk over, maar de advocaat van de tegenpartij schreeuwde en schreeuwde naar me, ik kon zelfs geen woord inbrengen. In zulke gevallen werkt het voor mij het beste om een bemiddelaar te gebruiken. De bemiddelaar stopt al die druk die als een constante stroom op je afkomt, je ontdekt de benodigde zen-ruimte, en daarmee komt het vermogen om te praten terug. Dit is een heel domein van kennis waarin je veel moet leren, veel binnen jezelf moet ontdekken, en dit alles transformeert in hoge strategische oplossingen die voor verschillende mensen verschillend zijn. Sommigen hebben de hierboven beschreven problemen niet, meestal hebben mensen in de verkoop dat niet. Al deze mensen die hun leven met woorden verdienen - bekende zangers, dichters, religieuze leiders en politici, zij hebben altijd iets te zeggen. Zij hebben niet zulke problemen, maar ik heb ze wel.
Andrey: Dat was... onverwacht. Geweldig, we hebben al behoorlijk wat gesproken en het is tijd om dit interview af te ronden. We zullen elkaar zeker ontmoeten op de conferentie en dit gesprek kunnen voortzetten. Tot ziens op Hydra!
Je kunt de communicatie met Cliff voortzetten op de Hydra 2019-conferentie, die op 11-12 juli 2019 in St. Petersburg zal plaatsvinden. Hij komt met een presentatie . Je kunt tickets kopen .
Bron: habr.com
