Cliff Click — CTO of Cratus (IoT sensors for process improvement), founder and co-founder of several startups (including Rocket Realtime School, Neurensic, and H2O.ai) with several successful exits. Cliff wrote his first compiler at the age of 15 (Pascal for TRS Z-80)! He is best known for his work on C2 in Java (the Sea of Nodes IR). This compiler showed the world that JIT can produce quality code, which became one of the factors in establishing Java as one of the main modern software platforms. Later, Cliff helped Azul Systems build an 864-core mainframe with software written purely in Java, which supported GC pauses on a 500-gigabyte heap within 10 milliseconds. Overall, Cliff has worked on all aspects of the JVM.
This hub post is a large interview with Cliff. We will discuss the following topics:
- Transitioning to low-level optimizations
- How to perform large refactorings
- Cost model
- Learning low-level optimizations
- Practical examples of performance improvements
- Why create your own programming language
- Career of a performance engineer
- Technical challenges
- A bit about register allocation and multithreading
- The biggest challenge of life
The interviewers are:
- Andrey Sataryn from Amazon Web Services. In his career, he has worked on various projects: testing a distributed NewSQL database at Yandex, a cloud detection system at Kaspersky Lab, a multiplayer game at Mail.ru, and a currency pricing service at Deutsche Bank. He is interested in testing large-scale backend and distributed systems.
- Władimir Sitnikow from Netcracker. He has been working for ten years on performance and scalability of NetCracker OS — software used by telecom operators to automate network management processes and network equipment. He is passionate about Java and Oracle Database performance issues. An author of more than a dozen performance improvements in the official PostgreSQL JDBC driver.
Transitioning to low-level optimizations
Andrey: You are a well-known figure in the world of JIT compilation, Java, and performance work in general, are you not?
Cliff: That's right!
Andrey: Let's start with general questions about performance work. What do you think about the choice between high-level and low-level optimizations like working at the CPU level?
Cliff: To jest proste. Najszybszy kod to ten, który nigdy nie jest uruchamiany. Dlatego zawsze należy zaczynać od wysokiego poziomu, pracować nad algorytmami. Lepsza notacja O pokona gorszą notację O, chyba że interweniują jakieś wystarczająco duże stałe. Rzeczy niskiego poziomu przychodzą na samym końcu. Zwykle, jeśli zoptymalizujesz pozostałą część stosu wystarczająco dobrze, a nadal zostaje coś interesującego – to jest właśnie niski poziom. Ale jak zacząć od wysokiego poziomu? Jak dowiedzieć się, że wystarczająco dobrze pracowano na wysokim poziomie? Cóż... nie ma gotowych recept. Trzeba zrozumieć problem, zdecydować, co zamierzasz zrobić (aby nie podejmować niepotrzebnych w późniejszym czasie kroków) i wtedy można już sięgnąć po profiler, który może powiedzieć coś użytecznego. W pewnym momencie sam rozumiesz, że pozbyłeś się niepotrzebnych rzeczy i nastał czas, aby zająć się drobnym dostrajaniem niskiego poziomu. To z pewnością jest szczególnym rodzajem sztuki. Wiele osób robi niepotrzebne rzeczy, ale porusza się tak szybko, że nie mają czasu, aby dbać o wydajność. Ale to do momentu, gdy problem staje się krytyczny. Zwykle 99% czasu nikogo nie interesuje, czym się zajmuję, aż do momentu, gdy na krytycznej drodze pojawi się ważna sprawa, która kogoś obchodzi. I wtedy wszyscy zaczynają dopytywać, dlaczego to od samego początku nie działało idealnie. Generalnie zawsze jest coś do poprawy w wydajności. Ale 99% czasu nie masz wskazówek! Po prostu próbujesz sprawić, aby coś działało, a w trakcie tego dowiadujesz się, co jest ważne. Nigdy nie można z góry wiedzieć, że ten kawałek trzeba zrobić idealnym, dlatego praktycznie musisz być idealnym we wszystkim. A to jest niemożliwe i tak się nie robi. Zawsze jest mnóstwo rzeczy do poprawy – i to jest całkowicie normalne.
How to perform large refactorings
Andrey: Jak pracujesz nad wydajnością? To przecież problem wszechobecny. Na przykład, czy kiedykolwiek musiałeś pracować nad problemami, które pojawiają się w wyniku przecięcia się dużej ilości istniejącej już funkcjonalności?
Cliff: Staram się tego unikać. Jeśli wiem, że wydajność będzie problemem, zastanawiam się nad tym, zanim zacznę kodować, szczególnie nad strukturami danych. Ale często odkrywasz to znacznie później. Wtedy musisz podejmować ostateczne środki i robić to, co nazywam „przepisywaniem i rządzeniem”: musisz uchwycić wystarczająco dużą część. Część kodu i tak będzie musiała zostać przepisana z powodu problemów z wydajnością lub z innych powodów. Niezależnie od tego, jaka przyczyna przepisania kodu się pojawi, prawie zawsze lepiej przerobić większą część niż mniejszą. W tym momencie wszyscy zaczynają drżeć ze strachu: „o rany, nie można dotykać tak dużej ilości kodu!”. Ale w rzeczywistości takie podejście zazwyczaj działa znacznie lepiej. Należy od razu wziąć się za duży problem, otoczyć go dużym kręgiem i powiedzieć: wszystko, co w tym kręgu, przepisuję. Granica jest znacznie mniejsza niż treść wewnątrz niej, którą należy wymienić. A jeśli taki zarys granic pozwoli wykonać pracę wewnątrz doskonale – masz wolną rękę, rób co chcesz. Gdy tylko zrozumiesz problem, proces przepisywania znacznie się upraszcza, więc bierz duży kawałek!
Z drugiej strony, gdy przepisujesz dużą część i zdajesz sobie sprawę, że wydajność może stać się problemem, możesz od razu zacząć się tym martwić. Zazwyczaj sprowadza się to do prostych rzeczy, takich jak „nie kopiuj danych, zarządzaj danymi tak prosto, jak to możliwe, spraw, aby były mniejsze”. W dużych przepisywaniach są standardowe sposoby na poprawę wydajności. I zazwyczaj kręcą się wokół danych.
Cost model
Andrey: W jednym z podcastów wspominaliście o modelach kosztów w kontekście wydajności. Czy możecie wyjaśnić, co przez to mieliście na myśli?
Cliff: Oczywiście. Urodziłem się w czasach, gdy wydajność procesora była niezwykle ważna. I ta era znów powraca – los nie jest pozbawiony ironii. Zacząłem żyć w czasach maszyn ośmiobitowych, mój pierwszy komputer działał na 256 bajtach. Właśnie bajtach. Wszystko było bardzo małe. Musiałem liczyć instrukcje i gdy tylko zaczęliśmy przemieszczać się w górę stosu języków programowania, języki brały na siebie coraz więcej. Był Assember, potem Basic, potem C, a C zajmował się wieloma szczegółami, takimi jak przydzielanie rejestrów i dobór instrukcji. Ale wszystko było dosyć zrozumiałe i jeśli zrobiłem wskaźnik do instancji zmiennej, to otrzymałem load, a koszt tej instrukcji jest znany. Sprzęt wydaje znaną ilość cykli maszynowych, więc prędkość wykonywania różnych rzeczy można obliczyć, sumując wszystkie instrukcje, które zamierzasz uruchomić. Każde porównanie/próba/gałąź/wywołanie/ładunek/przechowywanie można zsumować i powiedzieć: oto czas wykonania. Zajmując się poprawą wydajności, na pewno zwrócisz uwagę na to, jakie liczby odpowiadają małym gorącym cyklom.
Ale jak tylko przełączysz się na Javę, Pythona i podobne rzeczy, szybko oddalasz się od niskopoziomowego sprzętu. Jaki jest koszt wywołania gettera w Javie? Jeśli JIT w HotSpot wszystko dobrze , to będzie load, ale jeśli tego nie zrobił – to będzie wywołanie funkcji. Ponieważ wywołanie leży w gorącym cyklu, unieważni wszystkie inne optymalizacje w tym cyklu. Dlatego rzeczywisty koszt będzie znacznie większy. I tracisz od razu zdolność do spojrzenia na fragment kodu i zrozumienia, co kosztuje jego wykonanie w terminach cykli zegara procesora, używanej pamięci i cache. Wszystko to staje się interesujące tylko wtedy, gdy naprawdę zagłębisz się w wydajność.
Obecnie znajdujemy się w sytuacji, w której prędkości procesorów od dekady prawie się nie zmieniają. Stare czasy wracają! Już nie możesz liczyć na dobrą wydajność jednowątkową. Ale jeśli nagle zajmiesz się obliczeniami równoległymi – to jest szalenie trudne, wszyscy patrzą na ciebie jak na Jamesa Bonda. Dziesięciokrotne przyspieszenia pojawiają się zwykle w miejscach, gdzie ktoś coś przeoczył. Równoległość wymaga wielu działań. Aby uzyskać to święte dziesięciokrotne przyspieszenie, musisz zrozumieć model kosztów. Co i ile kosztuje. A do tego potrzebujesz zrozumieć, jak język współpracuje z leżącym poniżej sprzętem.
Martin Thompson dobrał świetne słowo na swojego bloga ! Należy rozumieć, co zamierza zrobić sprzęt, jak dokładnie to zrobi i dlaczego w ogóle to robi. Korzystając z tego, dość łatwo zacząć liczyć instrukcje i ustalić, gdzie uciekają czas wykonania. Jeśli nie masz odpowiedniego przygotowania, po prostu szukasz czarnej kota w ciemnym pokoju. Ciągle widzę ludzi, którzy optymalizują wydajność, ale nie mają najmniejszego pojęcia, co w ogóle robią. Mają trudności i nie posuwają się za bardzo naprzód. A kiedy biorę ten sam kawałek kodu, dodaję kilka drobnych hacków i osiągam pięciokrotne lub dziesięciokrotne przyspieszenie, oni mówią: no, to niesprawiedliwe, i tak wiedzieliśmy, że jesteś lepszy. Niesamowite. O czym to ja… model kosztów – to o tym, za jaki kod piszesz i jak szybko działa on przeciętnie w całym obrazie.
Andrey: A jak utrzymać taką ilość w głowie? Osiąga się to dużą ilością doświadczenia, prawda? Gdzie można zdobyć takie doświadczenie?
Cliff: Cóż, moje doświadczenie zdobyłem nie najprostszą drogą. Programowałem w Assemblerze, gdy można było zrozumieć każdą pojedynczą instrukcję. Brzmi głupio, ale od tamtej pory na zawsze pozostał mi w pamięci zestaw instrukcji Z80. Nie pamiętam imion ludzi już po minucie rozmowy, ale pamiętam kod napisany 40 lat temu. Ciekawe, to wygląda jak syndrom „».
Learning low-level optimizations
Andrey: Czy istnieje jakiś prostszy sposób, aby się w to wciągnąć?
Cliff: Tak i nie. Sprzęt, którego wszyscy używamy, w tym czasie niewiele się zmienił. Wszyscy korzystają z x86, z wyjątkiem smartfonów na Arm. Jeśli nie zajmujesz się jakimś hardcore’owym embedded, masz to samo. Dobrze, przechodźmy dalej. Instrukcje również przez wieki się nie zmieniły. Musisz pójść i napisać coś w assemblerze. Trochę, ale wystarczająco, aby zacząć rozumieć. Ty się uśmiechasz, ale mówię to zupełnie poważnie. Musisz zrozumieć odpowiedniość między językiem a sprzętem. Po tym musisz pójść, napisać trochę i stworzyć mały zabawkowy kompilator dla małego zabawkowego języka. „Zabawkowy” oznacza, że musisz go zrobić w rozsądnym czasie. Może być super prosty, ale musi generować instrukcje. Akt generowania instrukcji pozwoli zrozumieć model kosztów dla mostu między wysokopoziomowym kodem, w którym wszyscy piszą, a kodem maszynowym, który jest wykonywany na sprzęcie. Ta odpowiedniość wniknie w umysły w momencie pisania kompilatora. Nawet najbardziej podstawowy kompilator. Po tym można zacząć patrzeć na Javę i dostrzegać, że jej semantyczna przepaść jest znacznie głębsza, a budowanie mostów nad nią jest znacznie trudniejsze. W Javie znacznie trudniej zrozumieć, czy nasz most jest dobry, czy zły, co sprawi, że się zawali, a co nie. Ale potrzebujesz jakiegoś punktu wyjścia, gdy patrzysz na kod i rozumiesz: „aha, ten getter powinien być inlinowany za każdym razem”. A potem okazuje się, że czasami tak jest, z wyjątkiem sytuacji, gdy metoda staje się za duża, a JIT zaczyna wciągać wszystko, co popadnie. Wydajność takich miejsc można przewidzieć natychmiast. Zwykle gettery działają dobrze, ale potem patrzysz na duże gorące pętle i zauważasz, że w nich pływają jakieś wywołania funkcji, których nie wiadomo, co robią. W tym tkwi problem powszechnego stosowania getterów, powód, dla którego nie są inlinowane – nie wiadomo, czy to getter. Jeśli masz supermałą bazę kodu, możesz ją po prostu zapamiętać i później powiedzieć: to jest getter, a to jest setter. W dużej bazie kodu każda funkcja żyje swoją własną historią, która w zasadzie nikomu nie jest znana. Profilowanie mówi, że straciliśmy 24% czasu na jakiejś pętli i aby zrozumieć, co ta pętla robi, musisz spojrzeć na każdą funkcję w środku. Nie da się tego zrozumieć bez studiowania funkcji, co poważnie spowalnia proces rozumienia. Dlatego nie używam getterów i setterów, osiągnąłem nowy poziom!
Skąd wziąć model kosztów? Oczywiście można coś poczytać... Ale myślę, że najlepszym sposobem jest działanie. Zbudować mały kompilator, a to będzie najlepszy sposób, aby zrozumieć model kosztów i zmieścić go w sobie. Mały kompilator, który nadawałby się do programowania mikrofalówki – to zadanie dla nowicjusza. No, mam na myśli, że jeśli już masz umiejętności programowania, to powinno wystarczyć. Wszystkie te rzeczy, jak sparsowanie ciągu, który będzie jakimś wyrażeniem algebraicznym, wydobycie stamtąd instrukcji operacji matematycznych w odpowiedniej kolejności, wzięcie odpowiednich wartości z rejestrów – to wszystko robi się w mgnieniu oka. I podczas gdy to robisz, zostanie to wydrukowane w mózgu. Myślę, że wszyscy wiedzą, czym zajmuje się kompilator. To da zrozumienie modelu kosztów.
Practical examples of performance improvements
Andrey: Na co jeszcze warto zwrócić uwagę przy pracy nad wydajnością?
Cliff: Struktury danych. Tak, nawiasem mówiąc, dawno nie prowadziłem tych zajęć... . To było zabawne, ale wymagało takiego wysiłku, a ja mam przecież jeszcze swoje życie! Dobrze. Otóż, na jednym z dużych i interesujących zajęć, 'Dokąd ucieka Twoja wydajność', dawałem studentom przykład: dwa i pół gigabajta danych fintech wczytywano z pliku CSV, a następnie trzeba było obliczyć liczbę sprzedawanych produktów. Zwykłe dane rynkowe. Pakiety UDP, przekształcone na format tekstowy, sięgające lat 70-tych. Chicago Mercantile Exchange – różne rzeczy, takie jak olej, kukurydza, soja i tym podobne. Należało policzyć te produkty, liczbę transakcji, średnią wartość obrotów i towarów, itd. To dość prosta matematyka handlowa: znaleźć kod produktu (to 1-2 znaki w tabeli haszy), uzyskać sumę, dodać ją do jednego z zestawów transakcji, dodać wolumen, dodać koszt, i kilka innych rzeczy. Bardzo prosta matematyka. Prosta implementacja była bardzo bezpośrednia: wszystko znajduje się w pliku, czytam plik i przechodzę przez niego, dzieląc poszczególne rekordy na wiersze w Javie, szukam w nich potrzebnych rzeczy i sumuję według powyższej matematyki. I to działa z jakąś niewielką prędkością.
Z takim podejściem wszystko staje się jasne, co się dzieje, a równoległe obliczenia tutaj nie pomogą, prawda? Okazuje się, że pięciokrotny wzrost wydajności można osiągnąć po prostu wybierając odpowiednie struktury danych. To zaskakuje nawet doświadczonych programistów! W moim konkretnym przypadku chodziło o to, że nie należy dokonywać alokacji pamięci w gorącej pętli. Cóż, to nie cała prawda, ale ogólnie – nie należy alokować „raz na X”, kiedy X jest wystarczająco duże. Kiedy X wynosi dwa i pół gigabajta, nie należy alokować niczego „raz za literę”, ani „raz za linię”, ani „raz za pole”, nic w tym stylu. To właśnie na to marnuje się czas. Jak to w ogóle działa? Wyobraź sobie, że wywołuję String.split() lub BufferedReader.readLine(). Readline tworzy ciąg z zestawu bajtów, które przyszły przez sieć, raz dla każdej linii, dla każdej z setek milionów linii. Biorę tę linię, analizuję ją i odrzucam. Dlaczego odrzucam – cóż, już ją przetworzyłem, to wszystko. Tak więc dla każdego bajtu, który odczytuję z tych 2.7G, zapisane zostaną dwa znaki w ciągu, co daje już 5.4G, a potem nie są mi do niczego potrzebne, więc są odrzucane. Jeśli spojrzeć na przepustowość pamięci, ładujemy 2.7G, które przechodzi przez pamięć i magistralę pamięci w procesorze, a następnie dwa razy więcej wysyła się do ciągu w pamięci, a wszystko to jest przetwarzane podczas tworzenia każdej nowej linii. Ale muszę ją przeczytać, sprzęt ją odczytuje, nawet jeśli potem wszystko będzie przetarte. A muszę ją zapisać, bo stworzyłem ciąg i pamięci podręczne się przepełniły – pamięć podręczna nie może pomieścić 2.7G. W sumie, dla każdego odczytanego bajtu czytam jeszcze dwa dodatkowe bajty i zapisuję dwa dodatkowe bajty, i w efekcie mamy proporcję 4:1 – w takim stosunku marnujemy przepustowość pamięci. A potem okazuje się, że jeśli robię String.split() – to robię to z pewnością nie po raz ostatni, tam w środku może być jeszcze 6-7 pól. Dlatego klasyczny kod do odczytu CSV z późniejszym parsowaniem linii prowadzi do strat przepustowości pamięci w okolicach 14:1 w porównaniu do tego, co naprawdę chciałbyś mieć. Jeśli wyrzucić te alokacje, można uzyskać pięciokrotne przyspieszenie.
I to nie jest wcale takie trudne. Jeśli spojrzysz na kod z odpowiedniej perspektywy, wszystko staje się dość proste, gdy zrozumiesz istotę problemu. Nie powinno się w ogóle przestawać przydzielać pamięci: problem polega tylko na tym, że coś przydzielasz, a to natychmiast umiera, spalając po drodze ważny zasób, którym w tym przypadku jest przepustowość pamięci. A wszystko to przekłada się na spadek wydajności. Na x86 zazwyczaj trzeba aktywnie spalać cykle procesora, a tutaj spaliłeś całą pamięć znacznie wcześniej. Rozwiązaniem jest zmniejszenie liczby przydzielania.
Inna część problemu polega na tym, że jeśli uruchomisz profiler, gdy skończy się pasmo pamięci, w momencie, gdy to się zdarza, zazwyczaj czekasz na powrót cache, ponieważ jest on pełen bałaganu, który właśnie stworzyłeś – wszystkich tych wierszy. Dlatego każda operacja załadunku czy zapisu staje się wolna, ponieważ prowadzi do nietrafionych odwołań do cache – cały cache stał się wolny, czekając na usunięcie śmieci. Dlatego profiler pokazuje jedynie ciepły, losowy szum, rozmazany wzdłuż całego cyklu – nie będzie żadnej osobnej gorącej instrukcji ani miejsca w kodzie. Tylko szum. A jeśli spojrzysz na cykle GC, wszystkie będą w Young Generation i niezwykle szybkie – mikrosekundy lub maksymalnie milisekundy. Bowiem cała ta pamięć umiera natychmiast. Przydzielasz miliardy gigabajtów, a on je przycina, i przycina, i znowu przycina. Wszystko to dzieje się bardzo szybko. Tak więc mamy tanie cykle GC, ciepły szum wzdłuż całego cyklu, ale chcemy uzyskać pięciokrotne przyspieszenie. W tym momencie powinno w głowie zapalić się coś i zabrzmieć: "dlaczego tak?!". Przepełnienie pasma pamięci nie objawia się w klasycznym debuggerze, trzeba uruchomić debugger sprzętowych liczników wydajności i zobaczyć to samodzielnie i bezpośrednio. A nie bezpośrednio można podejrzewać na podstawie tych trzech objawów. Trzeci objaw – to gdy patrzysz, co przydzielasz, pytasz profilera, a on odpowiada: "Zrobiłeś miliard wierszy, ale GC działał za darmo". Kiedy to się zdarza, rozumiesz, że stworzyłeś zbyt wiele obiektów i spaliłeś całe pasmo pamięci. Jest sposób, aby się z tym uporać, ale nie jest oczywisty.
Problem z strukturą danych: goła struktura, która leży u podstaw wszystkiego, co się dzieje, jest zbyt duża, zajmuje 2,7G na dysku, dlatego bardzo niepożądane jest jej kopiowanie – chciałoby się załadować ją od razu z sieciowego bufora bajtów do rejestrów, aby nie czytać i nie zapisywać do niej pięć razy. Niestety, Java domyślnie nie oferuje takiej biblioteki w składzie JDK. Ale to jest trywialne, prawda? W gruncie rzeczy to 5-10 linii kodu na wdrożenie własnego buforowanego loadera linii, który naśladuje działanie klasy string, będąc jednocześnie obwolutą wokół dolnego bufora bajtów. W rezultacie okazuje się, że pracujesz niemal jak ze stringami, ale tak naprawdę przesuwają się wskaźniki na bufor, a surowe bajty nie są nigdzie kopiowane, i w ten sposób wielokrotnie wykorzystujesz te same bufory, a system operacyjny chętnie przejmuje rzeczy, do których jest przeznaczony, jak ukryta podwójna buforowanie tych buforów bajtowych, a ty już więcej nie przetwarzasz bez końca zbędnych danych. Przy okazji, rozumiecie, że podczas pracy z GC gwarantuje się, że każda alokacja pamięci nie będzie widoczna dla procesora po ostatniej pętli GC? Dlatego to wszystko nie może być w cache'u, a potem zdarza się 100% gwarantowane spudłowanie. Podczas pracy z wskaźnikiem, na architekturze x86 odczyt rejestru z pamięci zajmuje 1-2 takty, i kiedy to się już wydarzy, płacisz, płacisz, płacisz, ponieważ cała pamięć – i to jest koszt alokacji pamięci. Prawdziwy koszt.
Innymi słowy, struktury danych to coś, co najtrudniej zmienić. A kiedy już uświadomisz sobie, że wybrałeś niewłaściwą strukturę danych, która później zrujnuje wydajność, zazwyczaj wymaga to znaczącej pracy, a jeśli tego nie zrobisz, będzie tylko gorzej. Przede wszystkim trzeba myśleć o strukturach danych, to jest ważne. Podstawowy koszt spoczywa na złożonych strukturach danych, które zaczynamy używać w stylu „skopiowałem strukturę danych X do struktury danych Y, bo forma Y bardziej mi odpowiada”. Ale operacja kopiowania (która wydaje się tania) w rzeczywistości zużywa pasmo pamięci, i tu jest zakopane całe stracone czas wykonania. Jeśli mam gigantyczny ciąg z JSON i chcę przekształcić go w zorganizowane drzewo DOM z POJO lub coś w tym stylu, operacja parsowania tego ciągu i budowania POJO, a potem nowe odwołanie do POJO w przyszłości zamienią się w dodatkowy koszt – to nie jest tanie. Z wyjątkiem przypadku, gdy będziesz korzystać z POJO znacznie częściej niż z ciągu. Na szybko, zamiast tego można spróbować rozkodować ciąg i wyciągnąć tylko to, co potrzebne, nie przekształcając w żadne POJO. Jeśli wszystko to dzieje się na drodze, na której wymagana jest maksymalna wydajność, nie ma mowy o POJO – trzeba jakoś grzebać bezpośrednio w ciągu.
Why create your own programming language
Andrey: Powiedziałeś, że aby zrozumieć model kosztów, trzeba stworzyć swój mały język…
Cliff: Nie język, a kompilator. Język i kompilator to różne rzeczy. Najważniejsza różnica - w swojej głowie.
Andrey: Przy okazji, z tego co wiem, eksperymentujesz z tworzeniem własnych języków. Po co?
Cliff: Bo dlatego, że mogę! Jestem w połowie na emeryturze, więc to moje hobby. Przez całe życie realizowałem języki innych ludzi. Dużo pracowałem nad stylem pisania kodu. A także dlatego, że dostrzegam problemy w innych językach. Widziałem, że są lepsze sposoby na wykonywanie codziennych zadań. I zamierzam z nich skorzystać. Po prostu mam dość widzenia problemów w sobie, w Javie, w Pythonie i w każdym innym języku. Obecnie piszę w React Native, JavaScript i Elm jako hobby, które nie dotyczy emerytury, a aktywnej pracy. Również piszę w Pythonie i prawdopodobnie będę kontynuował prace nad uczeniem maszynowym dla backendów Javy. Istnieje wiele popularnych języków i każdy z nich ma interesujące cechy. Każdy jest dobry w czymś swoim i można spróbować połączyć wszystkie te funkcje w jedną całość. Tak więc zajmuję się badaniem interesujących dla mnie rzeczy, zachowaniem języka, próbuję wymyślić sensowną semantykę. I jak na razie mi się udaje! Aktualnie zmagam się z semantyką pamięci, ponieważ chciałbym mieć ją tak jak w C i Javie, uzyskać silny model pamięci oraz semantykę pamięci dla załadunków i zapisów. Jednocześnie mam mieć automatyczne wnioskowanie typów jak w Haskellu. Oto, próbuję połączyć Haskell-podobne wnioskowanie typów z pamięcią działającą jak w C i Javie. Tym zajmuję się przez ostatnie 2-3 miesiące, na przykład.
Andrey: Jeśli tworzysz język, który czerpie najlepsze aspekty z innych języków, czy myślałeś, że ktoś zrobi odwrotnie: weźmie twoje pomysły i użyje ich u siebie?
Cliff: Właśnie w ten sposób powstają nowe języki! Dlaczego Java jest podobna do C? Ponieważ C miał dobry składnik, który wszyscy rozumieli i Java inspirowała się tym składnikiem, dodając do niego bezpieczeństwo typów, sprawdzanie granic tablic, GC, a także poprawili niektóre rzeczy z C. Dodali swoje. Ale inspirowali się dość mocno, prawda? Wszyscy stoją na ramionach gigantów, którzy byli przed nimi – w ten sposób dokonuje się postęp.
Andrey: Jak rozumiem, twój język będzie bezpieczny w kwestii użycia pamięci. Czy myślałeś o wdrożeniu czegoś w rodzaju borrow checkera z Rust? Patrzyłeś na to, co o tym sądzisz?
Cliff: C od lat towarzyszy mi w programowaniu, wszędzie wykorzystując malloc i free, a także zarządzając czasem życia ręcznie. Wiecie, 90-95% ręcznie zarządzanego czasu życia ma tę samą strukturę. I to bardzo, bardzo boli zajmować się tym ręcznie. Chciałbym, aby kompilator po prostu mówił, co się dzieje i co osiągnąłem swoimi działaniami. W niektórych przypadkach borrow checker robi to automatycznie. Musi również automatycznie wydobywać informacje, wszystko rozumieć, a nawet nie obarczać mnie koniecznością wyłożenia tego zrozumienia. Powinien przynajmniej przeprowadzić lokalną analizę ucieczki, a tylko jeśli mu się to nie uda, wtedy konieczne jest dodawanie adnotacji typów, które opisywałyby czas życia – a taki schemat jest znacznie bardziej skomplikowany niż borrow checker czy jakikolwiek istniejący kontroler pamięci. Wybór między „wszystko w porządku” a „nic nie zrozumiałem” – nie, powinno być coś lepszego.
Jako osoba, która napisała wiele kodu w C, uważam, że wsparcie automatycznego zarządzania czasem życia to kluczowa kwestia. Dodatkowo denerwuje mnie, jak bardzo Java wykorzystuje pamięć, a głównym problemem jest GC. W przypadku alokacji pamięci w Javie nie otrzymasz pamięci, która była lokalna w ostatniej iteracji GC. W językach z dokładniejszym zarządzaniem pamięcią nie ma takiego problemu. Gdy wywołujesz malloc, od razu otrzymujesz pamięć, która zazwyczaj była właśnie używana. Zwykle robisz jakieś tymczasowe operacje z pamięcią i natychmiast ją zwracasz. I wraca ona natychmiast do puli malloc-a, a następny cykl malloc-a ponownie ją wydobywa. Dlatego rzeczywiste wykorzystanie pamięci ogranicza się do zestawu żywych obiektów w danym momencie, plus wycieki. Jeśli nie masz poważnych wycieków, większość pamięci osiada w cache i procesorze, co działa szybko. Jednak wymaga to dużo ręcznego zarządzania pamięcią z użyciem malloc i free, wywoływanych we właściwej kolejności i we właściwym miejscu. Rust może poradzić sobie z tym samodzielnie i w wielu przypadkach daje nawet lepszą wydajność, ponieważ zużycie pamięci ogranicza się do bieżących obliczeń – w przeciwieństwie do oczekiwania na następny cykl GC, który zwolni pamięć. W rezultacie zyskaliśmy bardzo interesujący sposób na poprawę wydajności. I całkiem potężny – mówię to z doświadczenia, pracując nad przetwarzaniem danych w fintechu, co pozwalało na pięciokrotne przyspieszenie. To dość duże przyspieszenie, zwłaszcza w świecie, w którym procesory nie stają się szybsze, a my wciąż czekamy na ulepszenia.
Career of a performance engineer
AndreyChciałbym też zapytać o całą karierę. Stałeś się znany dzięki pracy przy JIT w HotSpot, a potem przeszedłeś do Azul – również firmą zajmującą się JVM. Jednak bardziej koncentrowałeś się na sprzęcie niż oprogramowaniu. A potem nagle przeszedłeś na Big Data i uczenie maszynowe, a potem na wykrywanie oszustw. Jak to się stało? To bardzo różne obszary rozwoju.
Cliff: Już od dłuższego czasu zajmuję się programowaniem i udało mi się zaznaczyć swoją obecność w różnych projektach. Gdy ludzie mówią: „o, to ty ten, który stworzył JIT dla Javy!”, zawsze mnie to bawi. A wcześniej zajmowałem się klonem PostScript – tego języka, którego Apple kiedyś używało do swoich drukarek laserowych. A przed tym pracowałem nad implementacją języka Forth. Myślę, że wspólnym motywem w moim życiu jest tworzenie narzędzi. Przez całe życie tworzę narzędzia, dzięki którym inni piszą swoje niesamowite programy. Zajmowałem się również rozwijaniem systemów operacyjnych, sterowników, debuggerów na poziomie jądra oraz języków do tworzenia systemów operacyjnych, które zaczynały się od prostych pomysłów, ale z czasem stawały się coraz bardziej skomplikowane. Główna tematyka jednak to – tworzenie narzędzi. Duża część mojego życia minęła między Azul a Sun i dotyczyła Javy. Gdy jednak zająłem się Big Data i uczeniem maszynowym, znów założyłem swoją elegancką czapkę i powiedziałem: „O, teraz mamy nietrywialny problem, a tutaj dzieje się mnóstwo interesujących rzeczy i ludzi robiących coś”. To doskonała droga do rozwoju, którędy warto podążać.
Tak, bardzo lubię obliczenia rozproszone. Moja pierwsza praca miała miejsce w studenckich czasach, gdzie programowałem w C nad projektem reklamowym. To były obliczenia rozproszone na chipach Zilog Z80, które zbierały dane do analogowego rozpoznawania tekstów, przeprowadzanego przez rzeczywisty analogowy analizator. To był fascynujący i zupełnie niezwykły temat. Jednak pojawiały się problemy, część danych nie była rozpoznawana poprawnie, więc trzeba było wyciągać obrazek i pokazywać go osobie, która mogła to przeczytać, a ta informowała, co tam było napisane. W związku z tym istniały zadania związane z danymi, a te zadania miały swój własny język. Był backend, który to wszystko przetwarzał – równolegle działające Z80 z uruchomionymi terminalami vt100 – po jednym na osobę. Istniał model programowania równoległego na Z80. Pewien wspólny fragment pamięci, który dzieliły wszystkie Z80 w konfiguracji typu „gwiazda”; dzielił się także szyną bagażową, a połowa RAM dzieliła się w sieci, podczas gdy druga połowa była prywatna lub wykorzystywana na coś innego. Złożony i sensowny system rozproszony z dzieloną… półdzieloną pamięcią. Kiedy to miało miejsce… Już nawet nie pamiętam, gdzieś w połowie lat 80. To było naprawdę dawno temu.
Tak, można powiedzieć, że 30 lat to dość dawno. Problemy związane z obliczeniami rozproszonymi istnieją już od dłuższego czasu, ludzie od zawsze zmagali się z -klasterami. Takie klastry wyglądają jak… Na przykład: jest Ethernet, a twój szybki x86 jest do niego podłączony, i teraz chcesz uzyskać fake shared memory, ponieważ nikt wtedy nie mógł zajmować się kodowaniem rozproszonych obliczeń, to było zbyt trudne, więc powstała fake shared memory z ochroną stron pamięci na x86. Jeśli pisałeś w tę stronę, informowaliśmy pozostałe procesory, że jeżeli uzyskają dostęp do tej samej shared memory, trzeba ją załadować z twojego wpisu. W ten sposób powstał coś w rodzaju protokołu wspierającego koherencję pamięci podręcznej oraz oprogramowanie do tego. Ciekawa koncepcja. Prawdziwym problemem, oczywiście, była inna kwestia. Wszystko to działało, ale szybko napotykałeś problemy z wydajnością, ponieważ nikt nie rozumiał modelu wydajności na wystarczająco dobrym poziomie – jakie są wzorce dostępu do pamięci, jak sprawić, żeby węzły nie pingowały się nawzajem w nieskończoność, i tak dalej.
W H2O wymyśliłem coś takiego: to sami twórcy odpowiadają za określenie, gdzie znajduje się równoległość, a gdzie jej nie ma. Stworzyłem model kodowania, który sprawia, że pisanie kodu o wysokiej wydajności stało się łatwe i proste. Natomiast napisanie wolno działającego kodu jest trudne, będzie wyglądać źle. Trzeba się naprawdę postarać, żeby napisać wolny kod, będziesz musiał użyć niestandardowych metod. Zwalniający kod widać na pierwszy rzut oka. W konsekwencji zazwyczaj pisze się kod, który działa szybko, ale musisz poradzić sobie z tym, co zrobić w przypadku współdzielonej pamięci. To wszystko wiąże się z dużymi tablicami, a zachowanie tam przypomina nie-woltalne duże tablice w równoległej Javie. Wyobraź sobie, że dwa wątki piszą do równoległej tablicy, jeden z nich wygrywa, a drugi, соответственно, przegrywa, i nie wiesz, kto jest kim. Jeśli nie są one woltalne, to kolejność może być dowolna – i to naprawdę działa. Ludzie rzeczywiście dbają o kolejność operacji, prawidłowo umieszczają volatile i w odpowiednich miejscach oczekują problemów z wydajnością związanych z pamięcią. W przeciwnym razie po prostu pisaliby kod jako pętle od 1 do N, gdzie N to jakies tryliony, mając nadzieję, że wszystkie skomplikowane przypadki automatycznie staną się równoległe – i tam to nie działa. Ale w H2O to nie jest ani Java, ani Scala, można to nazwać „Java minus minus”, jeśli chcesz. To bardzo zrozumiały styl programowania, przypominający pisanie prostego kodu w C lub Javie z pętlami i tablicami. Ale można przetwarzać pamięć w terabajtach. Nadal korzystam z H2O. Od czasu do czasu używam w różnych projektach – i to wciąż jest najszybsza rzecz, wyprzedzająca konkurencję dziesiątki razy. Jeśli zajmujesz się Big Data z danymi kolumnowymi, bardzo trudno jest prześcignąć H2O.
Technical challenges
Andrey: Jaki był największy wyzwanie w Pańskiej karierze?
Cliff: Czy omawiamy techniczną, czy nietechniczną część pytania? Powiedziałbym, że największe wyzwania są nietechniczne.
Jeśli chodzi o technologiczne wyzwania, to je po prostu pokonałem. Nie wiem nawet, które z nich było największe, ale było kilka dość interesujących, które zajęły sporo czasu i mentalnej walki. Kiedy dołączyłem do Sun, byłem pewny, że stworzę szybki kompilator, a wielu starszych programistów mówiło, że mi się to nie uda. Jednak podjąłem się tego zadania, napisałem kompilator aż do alokatora rejestrów, który był dość szybki. Był tak samo szybki jak nowoczesny C1, ale wówczas alokator był znacznie wolniejszy, a patrząc wstecz – to była kwestia dużej struktury danych. Potrzebowałem jej, aby napisać graficzny alokator rejestrów i nie rozumiałem dylematu między ekspresyjnością kodu a szybkością, co w tamtym czasie było bardzo ważne. Okazało się, że struktury danych często przekraczały rozmiar cache'a na x86 z tamtej epoki, więc jeśli początkowo zakładałem, że alokator rejestrów będzie zajmował 5-10 procent całego czasu JIT, to w rzeczywistości wyniosło to 50 procent.
Czas mijał, kompilator stawał się coraz bardziej zrozumiały i wydajny, przestał generować okropny kod w wielu przypadkach, a wydajność coraz częściej zaczynała przypominać to, co uzyskuje kompilator C. Oczywiście, jeśli nie piszesz jakiejś kiepskiej rzeczy, której nawet C nie przyspiesza. Jeśli piszesz kod w stylu C, uzyskujesz wydajność na poziomie C w większej liczbie przypadków. I im dalej, tym częściej kod wyglądał asymptotycznie jak kod w C, alokator rejestrów stawał się czymś zakończonym… niezależnie od tego, czy twój kod działał szybko, czy wolno. Wciąż pracowałem nad alokatorem, aby dokonywał lepszych alokacji. Stawał się coraz wolniejszy, ale osiągał coraz lepszą wydajność w sytuacjach, w których nikt już nie radził sobie. Mogłem zanurzyć się w alokatorze rejestrów, włożyć tam miesiąc pracy, a nagle cały kod zaczynał działać o 5% szybciej. Działo się to raz po raz i alokator rejestrów stał się czymś na kształt dzieła sztuki – wszyscy go kochali lub nienawidzili, a ludzie z akademii zadawali pytania dotyczące tego, „dlaczego wszystko robi się w ten sposób”, dlaczego nie. , a jaka jest różnica. Odpowiedź jest taka sama: allocator oparty na kolorowaniu grafu plus bardzo staranna praca z kodem bufora to narzędzie do zwycięstwa, najlepsza kombinacja, której nikt nie może pokonać. To dość nieoczywista rzecz. Wszystko inne, co robi kompilator – to dość znane rzeczy, chociaż również doprowadzone do poziomu sztuki. Zawsze robiłem rzeczy, które miały przekształcić kompilator w dzieło sztuki. Ale nic z tego nie było czymś nadzwyczajnym – z wyjątkiem allocatora rejestrów. Klucz w tym, że trzeba to robić starannie, pod obciążeniem i, jeśli to się zdarza (mogę wyjaśnić bardziej szczegółowo, jeśli to interesujące), oznacza to, że można bardziej agresywnie inline'ować, bez ryzyka przekroczenia punktu załamania wykresu wydajności. W tamtych czasach było mnóstwo pełnoskalowych kompilatorów, obwieszonych bajerami i gwizdkami, w których były allocatory rejestrów, ale nikt więcej tego nie potrafił.
Problem polega na tym, że jeśli dodasz metody, które należy inline'ować, zwiększając obszar inline'u, zestaw używanych wartości natychmiast przekracza liczbę rejestrów, co prowadzi do spillage. Krytyczny moment zazwyczaj następuje, gdy alokator ustępuje, a jeden dobry kandydat na spillage kosztuje więcej niż drugi, więc spillasz zupełnie dzikie rzeczy. Wartość inline'u polega na tym, że tracisz część overheadu, overheadu na wywołania i przechowywanie, możesz widzieć wartości w środku i możesz je dalej optymalizować. Koszt inline'u polega na tym, że powstaje duża liczba żywych wartości, a jeśli twój alokator rejestrów spillage'uje więcej niż to potrzebne, od razu przegrywasz. Dlatego większość alokatorów ma problem: kiedy inline przechodzi pewną granicę, wszystko wokół zaczyna spillać, a wydajność można włożyć do toalety. Ci, którzy realizują kompilator, dodają pewne heurystyki: na przykład, aby zatrzymać inline, zaczynając od pewnego wystarczająco dużego rozmiaru, ponieważ alokacje wszystko psują. Tak powstaje złamanie wykresu wydajności – wciąż inline'ujesz, wydajność powoli rośnie – a potem bam! – opada gwałtownie, ponieważ wykonujesz zbyt wiele inline'u. Tak to wszystko działało aż do pojawienia się Javy. Java wymaga znacznie więcej inline'u, więc musiałem uczynić mój alokator znacznie bardziej agresywnym, aby się wyrównał, a nie spadał, a jeśli zbyt wiele inline'owałeś – zaczyna spillać, ale potem i tak przychodzi moment „nie ma więcej spillowania”. To interesujące spostrzeżenie i przyszło do mnie po prostu znikąd, nieoczywiste, ale bardzo opłacalne. Zająłem się agresywnym inline'em i to zaprowadziło mnie w takie miejsca, gdzie wydajność Javy i C idą w parze. Są naprawdę bliskie – mogę pisać kod w Javie, który będzie znacznie szybszy od kodu w C, i tym podobne, ale w średniej, w dużym obrazie to wszystko jest mniej więcej porównywalne. Myślę, że część tej zasługi przypisuje się alokatorowi rejestrów, który pozwala mi inline'ować maksymalnie durnie. Po prostu inline'uję wszystko, co widzę. Pytanie brzmi, czy alokator działa dobrze, czy wynikowy kod działa sensownie. To był duży wyzwanie: zrozumieć to wszystko i sprawić, by działało.
A bit about register allocation and multithreading
Włodzimierz: Problemy związane z alokacją rejestrów wydają się być tematem bez końca. Ciekawe, czy kiedykolwiek coś, co wydawało się obiecujące, nie zrealizowało się w praktyce?
Cliff: Oczywiście! Alokacja rejestrów to obszar, w którym próbujesz zastosować jakieś heurystyki w celu rozwiązania NP-trudnego problemu. I nigdy nie uda ci się osiągnąć idealnego rozwiązania, prawda? To po prostu niemożliwe. Zobacz, kompilacja z wyprzedzeniem – również nie działa idealnie. Rozmowa dotyczy średnich przypadków. Typowej wydajności, więc można przyjść i zmierzyć coś, co uważasz za dobrą typową wydajność – w końcu pracujesz nad jej poprawą! Alokacja rejestrów to temat całkowicie poświęcony wydajności. Gdy już masz pierwszy prototyp, który działa i maluje, co trzeba, rozpoczyna się praca nad wydajnością. Należy nauczyć się dobrze mierzyć. Dlaczego to ważne? Jeśli masz jasne dane, można przyjrzeć się różnym obszarom i zobaczyć: aha, to pomogło tutaj, ale tam wszystko się złamało! Pojawiają się świetne pomysły, dodajesz nową heurystykę i nagle wszystko działa w średniej trochę lepiej. Lub nie działa. Miałem mnóstwo przypadków, kiedy walczyliśmy o pięć procent wydajności, które odróżniały nasze rozwiązanie od poprzedniego alokatora. I za każdym razem wygląda to tak: gdzieś wygrana, gdzieś przegrana. Jeśli masz dobre narzędzia do analizy wydajności, możesz znaleźć pomysły, które przegrały i zrozumieć, dlaczego. Może warto zostawić wszystko jak jest, a może bardziej poważnie zająć się dbałością o szczegóły, lub pójść i naprawić coś innego. To cały zestaw rzeczy! Zrobiłem ten fajny hak, ale potrzebny jest jeszcze ten, ten i ten – i dopiero ich suma daje pewne poprawki. A pojedyncze elementy mogą zawodzić. Taka jest natura pracy nad wydajnością NP-trudnych zadań.
Włodzimierz: Odnosi się wrażenie, że takie rzeczy jak malowanie w alokatorach to zadania już rozwiązane. Cóż, dla ciebie rozwiązane, sądząc po tym, co opowiadasz, więc czy w ogóle warto…
Cliff: Nie została ona rozwiązana jako taka. To ty musisz przekształcić ją w „rozwiązaną”. Istnieją trudne zadania, które trzeba rozwiązywać. Gdy to zostanie zrobione, przychodzi czas na pracę nad wydajnością. Do tej pracy należy podchodzić w odpowiedni sposób – robić benchmarki, zbierać metryki, wyjaśniać sytuacje, gdy po powrocie do poprzedniej wersji twój stary hack znów działa (lub odwrotnie, przestaje działać). I nie ustępować, aż czegoś się nie osiągnie. Jak już mówiłem, jeśli świetne pomysły, które nie wypaliły, to w dziedzinie alokacji rejestrów jest ich praktycznie nieskończoność. Można na przykład czytać publikacje naukowe. Chociaż obecnie ta dziedzina zaczęła się rozwijać dużo wolniej i stała się bardziej przejrzysta niż w czasach swojej młodości. Niemniej jednak w tej dziedzinie pracuje nieskończona liczba ludzi i warto spróbować wszystkich ich pomysłów – wszystkie czekają na swoją szansę. I nie możesz powiedzieć, jak dobre są te pomysły, jeśli ich nie wypróbujesz. Jak dobrze integrują się ze wszystkim innym w twoim alokatorze, bo alokator robi wiele rzeczy, a niektóre pomysły w twoim konkretnym alokatorze mogą nie działać, podczas gdy w innym z łatwością. Głównym sposobem na wygraną dla alokatora jest wyciągnięcie wolnych elementów poza główną ścieżkę i wymuszenie podziału wzdłuż granic wolnych ścieżek. Dlatego, jeśli chcesz uruchomić GC, pójść wolną ścieżką, deoptymalizować, wyrzucić wyjątek, wszystko w tym stylu – wiesz, że te rzeczy są stosunkowo rzadkie. I naprawdę są rzadkie, sprawdzałem to. Robisz dodatkową pracę i dzięki temu znika wiele ograniczeń z tych wolnych ścieżek, ale to nie jest zbyt ważne, ponieważ są wolne i rzadko się po nich chodzi. Na przykład wskaźnik zerowy – nigdy się nie zdarza, prawda? Musisz mieć kilka ścieżek na różne sprawy, ale nie powinny one się nawzajem zakłócać na głównym.
Włodzimierz: Co myślisz o wielowątkowości, gdy wątków jest od razu tysiące? To przydatna rzecz?
Cliff: Sukces GPU pokazuje, że jest całkiem przydatna!
Włodzimierz: Są dość wyspecjalizowane. A co z procesorami ogólnego przeznaczenia?
Cliff: Cóż, to była model biznesowy Azul. Odpowiedź pojawiła się jeszcze w erze, kiedy ludzie bardzo cenili przewidywalną wydajność. Wtedy pisanie równoległego kodu było dość trudne. Model kodowania H2O dobrze się skaluję, ale nie jest to model ogólnego przeznaczenia. Chyba że jest nieco bardziej uniwersalny niż w przypadku użycia GPU. Mówimy o złożoności opracowania czegoś takiego czy o trudności w jego używaniu? Na przykład, interesującą lekcję dał mi Azul, dość nieoczywistą: małe cache'e są w porządku.
The biggest challenge of life
Włodzimierz: Co z nietechnicznymi wyzwaniami?
Cliff: Największym wyzwaniem było to, aby nie być… miłym i uprzejmym w stosunku do innych. W rezultacie nieustannie znajdowałem się w skrajnie konfliktowych sytuacjach. Takich, w których wiedziałem, że wszystko idzie źle, ale nie wiedziałem, jak iść naprzód w rozwiązywaniu tych problemów i nie mogłem sobie z nimi poradzić. Wiele długotrwałych problemów, trwających dziesiątkami lat, pojawiło się właśnie w ten sposób. To, że w Javie są kompilatory C1 i C2 – jest bezpośrednim wynikiem tego. To, że w Javie przez ten cały czas nie było kompilacji wielopoziomowej – również. Oczywiście, że potrzebowaliśmy takiego systemu, ale nie jest oczywiste, dlaczego go nie było. Miałem problemy z jednym inżynierem… lub grupą inżynierów. Dawno temu, kiedy zacząłem pracować w Sun, byłem… Cóż, nie tylko wtedy, miałem ogólnie zawsze swoje zdanie. I uważałem za prawdę, że można po prostu wziąć tę swoją prawdę i powiedzieć prosto w twarz. Tym bardziej, że w większości czasu byłem szokująco prawdziwy. A jeśli nie podoba ci się takie podejście… zwłaszcza gdy ewidentnie się mylisz i robisz głupstwo… Ogólnie rzecz biorąc, niewiele osób mogło tolerancyjnie znosić taką formę komunikacji. Chociaż niektórzy mogli, na przykład ja. Przez całe życie kierowałem się zasadami merytokratycznymi. Jeśli pokażesz mi coś niewłaściwego, natychmiast się odwrócę i powiem: powiedziałeś bzdurę. Przy tym, oczywiście, przepraszam i wszystko takie, doceniam zasługi, jeśli takie w ogóle istnieją, i podejmuję inne właściwe działania. Z drugiej strony, szokująco często jestem szokująco prawdziwy. I to niezbyt dobrze działa w relacjach z ludźmi. Nie staram się być miły, ale stawiam sprawy jasno. „To nigdy nie zadziała, ponieważ jeden, dwa i trzy”. A oni na to: „Ojej!”. Były też inne konsekwencje, które lepiej pominąć: na przykład prowadzące do rozwodu z żoną i dziesięciu lat depresji po tym.
Challenge to jest walka z ludźmi, z ich postrzeganiem tego, co możesz lub nie możesz robić, co jest ważne, a co nie. Było wiele wyzwań dotyczących stylu kodowania. Wciąż piszę dużo kodu, a w tamtych czasach musiałem nawet zwolnić tempo, ponieważ zajmowałem się zbyt wieloma równoległymi zadaniami i robiłem je źle, zamiast skupić się na jednym. Patrząc wstecz, napisałem połowę kodu zespołu Java JIT, zespołu C2. Następny najszybszy programista pisał o połowę wolniej, następny - jeszcze o połowę wolniej, a to był wykładniczy spadek. Siódma osoba w tym szeregu była bardzo, bardzo wolna - tak już zazwyczaj bywa! Dotknąłem wielu fragmentów kodu. Obserwowałem, kto co pisze, bez wyjątku, wpatrywałem się w ich kod, recenzowałem każdego z nich i nadal pisałem więcej niż którykolwiek z nich. Podejście do ludzi nie działa zbyt dobrze. Niektórzy tego nie lubią. A kiedy nie mogą sobie z tym poradzić, zaczynają się rozmaite pretensje. Na przykład, pewnego razu powiedziano mi, żebym przestał pisać kod, bo piszę zbyt dużo, co zagraża zespołowi, a dla mnie brzmiało to jak żart: człowieku, jeśli cała reszta zespołu zniknie, a ja będę dalej pisał kod, stracisz tylko połowę zespołu. Z drugiej strony, jeśli będę kontynuował pisanie kodu i ty stracisz połowę zespołu - to brzmi jak bardzo złe zarządzanie. Nigdy szczególnie się nad tym nie zastanawiałem, nigdy o tym nie mówiłem, ale i tak było to gdzieś w mojej głowie. W zakątkach świadomości kręciła się myśl: "Czy wy wszyscy żartujecie?". Zatem największym problemem byłem ja i moje relacje z ludźmi. Teraz rozumiem siebie dużo lepiej, długo byłem liderem zespołu programistów, a teraz mówię ludziom wprost: wiesz, jestem taki, jaki jestem, i musicie z tym żyć – nic się nie stanie, jeśli tu stanę? I kiedy zaczęli sobie z tym radzić, wszystko zaczęło działać. Nie jestem ani zły, ani dobry, nie mam żadnych złych zamiarów ani egoistycznych pragnień, to po prostu moja istota i trzeba jakoś z tym żyć.
Andrey: Ostatnio wszyscy zaczęli mówić o samoświadomości dla introwertyków i ogólnie o umiejętnościach społecznych. Co można na ten temat powiedzieć?
Cliff: Tak, to było zrozumienie i lekcja, którą wyniosłem z rozwodu z żoną. Co wyniosłem z rozwodu – to zrozumienie siebie. W ten sposób zacząłem rozumieć innych ludzi. Zrozumieć, jak ta interakcja działa. To doprowadziło do odkryć jedno po drugim. Pojawiło się świadomość, kim jestem i co sobą reprezentuję. Co robię: albo zajmuję się zadaniem, albo unikam konfliktu, albo coś innego – i taki poziom samoświadomości naprawdę pomaga utrzymać siebie w ryzach. Po tym wszystkim idzie znacznie łatwiej. Jedna rzecz, którą odkryłem nie tylko u siebie, ale i u innych programistów – to niemożność werbalizacji myśli, gdy jesteś w stanie emocjonalnego stresu. Na przykład, siedzisz i programujesz, znajdujesz się w stanie przepływu, a tu biegną do ciebie i zaczynają krzyczeć w histerii, że coś się zepsuło, a teraz będą stosowane skrajne środki. I nie możesz powiedzieć ani słowa, bo jesteś w stanie emocjonalnego stresu. Zdobyta wiedza pozwala przygotować się na ten moment, przeżyć go i przejść do planu awaryjnego, po którym można już coś zrobić. Więc tak, gdy zaczynasz zdawać sobie sprawę, jak to wszystko działa – to ogromne wydarzenie, które zmienia życie.
Sam nie mogłem znaleźć odpowiednich słów, ale zapamiętałem sekwencję działań. Istota tego jest taka, że ta reakcja – jest zarówno fizyczna, jak i werbalna, i potrzebujesz przestrzeni. Takiej przestrzeni, w zenowskim sensie. Właśnie to trzeba wyjaśnić, a potem od razu odsunąć się na bok – czysto fizycznie się odsunąć. Gdy milczę, mogę przetworzyć sytuację w zakresie emocji. W miarę jak adrenalina dociera do mózgu, przełącza cię w tryb „walcz lub uciekaj”, już nie możesz nic powiedzieć, nie – teraz jesteś idiotą, inżynierem do bicia, niezdolnym do godnej odpowiedzi lub przynajmniej zatrzymania ataku, a atakujący może swobodnie atakować raz za razem. Najpierw trzeba znów stać się sobą, odzyskać kontrolę, wyjść z trybu „walcz lub uciekaj.”
I oto dlatego potrzebna jest werbalna przestrzeń. Po prostu wolna przestrzeń. Jeśli w ogóle coś mówić, można to właśnie zadeklarować, a potem pójść i naprawdę znaleźć sobie "przestrzeń": można wyjść na spacer po parku, zamknąć się pod prysznicem – to nie ma znaczenia. Najważniejsze jest, aby tymczasowo odłączyć się od tej sytuacji. Kiedy tylko na kilka sekund się odłącza, kontrola wraca, zaczynasz myśleć trzeźwo. "Dobrze, nie jestem jakimś idiotą, nie robię głupich rzeczy, jestem całkiem przydatnym człowiekiem". Gdy tylko uda ci się przekonać samego siebie, czas przejść do następnego etapu: zrozumieć, co się wydarzyło. Zostałeś zaatakowany, atak przyszedł z nieoczekiwanej strony, to była nieuczciwa, podła zasadzka. To źle. Następny krok to zrozumieć, dlaczego atakującemu to było potrzebne. Rzeczywiście, dlaczego? Może dlatego, że sam jest w furii? Dlaczego jest w furii? Na przykład, ponieważ sam się skompromitował i nie może wziąć odpowiedzialności? W ten sposób trzeba ostrożnie przeanalizować całą sytuację. Ale aby to zrobić, potrzebna jest przestrzeń do manewru, werbalna przestrzeń. Pierwszy krok to zerwanie werbalnego kontaktu. Uciec od dyskusji słownej. Odwołać ją, odejść jak najszybciej. Jeśli to rozmowa telefoniczna – po prostu odłóż słuchawkę – to umiejętność, którą zdobyłem podczas rozmów z byłą żoną. Jeśli rozmowa nie prowadzi do niczego dobrego, po prostu mów "do widzenia" i odkładaj słuchawkę. Po drugiej stronie słuchawki: "bla-bla-bla", ty odpowiadasz: "okej, na razie!" i odkładasz słuchawkę. Po prostu przerywasz rozmowę. Pięć minut później, gdy wraca do ciebie zdolność do trzeźwego myślenia, trochę się ochładzasz, staje się możliwe przemyślenie, co się właściwie stało i co dalej. I zacząć formułować przemyślaną odpowiedź, a nie tylko reagować emocjami. Dla mnie przełomem w samoświadomości było to, że w przypadku stresu emocjonalnego nie mogę mówić. Wyjście z tego stanu, przemyślenie i zaplanowanie, jak odpowiedzieć i zrekompensować problemy – oto właściwe kroki, gdy nie możesz mówić. Najprostszy sposób – uciec od sytuacji, w której manifestuje się stres emocjonalny i po prostu przestać w tym stresie uczestniczyć. Potem odzyskujesz zdolność do myślenia, gdy możesz myśleć, pojawia się możliwość mówienia, i tak dalej.
Przy okazji, w sądzie adwokat strony przeciwnej próbuje to robić z tobą – teraz już wiadomo, dlaczego. Ponieważ ma możliwość cię tak przytłoczyć, że nie będziesz w stanie nawet wymówić swojego imienia, na przykład. W dosłownym sensie, nie będziesz w stanie mówić. Jeśli to się z tobą dzieje i jeśli wiesz, że znajdziesz się w miejscu, gdzie toczą się słowne bitwy, w miejscu takim jak sąd, można przyjść ze swoim prawnikiem. Prawnik stanie w twojej obronie i powstrzyma słowny atak, a zrobi to w zupełnie legalny sposób, przywracając ci utraconą przestrzeń zen. Na przykład, musiałem kilka razy zadzwonić do rodziny, sędzia podszedł do tego dość przyjaźnie, ale adwokat strony przeciwnej krzyczał i krzyczał na mnie, nie mogłem nawet wtrącić słowa. W takich sytuacjach najlepiej sprawdza się użycie pośrednika. Pośrednik przerywa to całe ciśnienie, które na ciebie nieustannie spływa, odkrywasz potrzebną przestrzeń zen, a wraz z nią wraca zdolność mówienia. To cała dziedzina wiedzy, w której trzeba wiele się nauczyć, wiele odkryć w sobie, a wszystko to przekształca się w wysokopoziomowe decyzje strategiczne, różne dla różnych osób. Niektórzy nie mają opisanych powyżej problemów, zazwyczaj nie mają ich osoby zajmujące się sprzedażą zawodowo. Wszyscy ci ludzie, którzy utrzymują się z używania słów – znani piosenkarze, poeci, duchowni i politycy, zawsze mają coś do powiedzenia. Nie mają takich problemów, a ja je mam.
Andrey: To było… zaskakujące. Świetnie, porozmawialiśmy już całkiem sporo i czas zakończyć ten wywiad. Na pewno spotkamy się na konferencji i będziemy mogli kontynuować ten dialog. Do zobaczenia na Hydra!
Można kontynuować rozmowę z Cliffem na konferencji Hydra 2019, która odbędzie się 11-12 lipca 2019 roku w Petersburgu. Przyjedzie z wykładem . Bilety można nabyć .
Źródło: habr.com
