Duży wywiad z Cliffem Clickiem — ojcem kompilacji Java JIT

Duży wywiad z Cliffem Clickiem — ojcem kompilacji Java JITKliknij Klif — CTO firmy Cratus (czujniki IoT do doskonalenia procesów), założyciel i współzałożyciel kilku startupów (m.in. Rocket Realtime School, Neurensic i H2O.ai) z kilkoma udanymi wyjściami. Cliff napisał swój pierwszy kompilator w wieku 15 lat (Pascal dla TRS Z-80)! Najbardziej znany jest z pracy nad C2 w Javie (The Sea of ​​​​Nodes IR). Kompilator ten pokazał światu, że JIT może produkować kod wysokiej jakości, co było jednym z czynników wyłonienia się Javy jako jednej z głównych nowoczesnych platform oprogramowania. Następnie Cliff pomógł firmie Azul Systems zbudować 864-rdzeniowy komputer mainframe z czystym oprogramowaniem Java, który obsługiwał przerwy GC na stercie o wielkości 500 gigabajtów w ciągu 10 milisekund. Ogólnie rzecz biorąc, Cliffowi udało się popracować nad wszystkimi aspektami JVM.

 
Ten habrapost to świetny wywiad z Cliffem. Porozmawiamy na następujące tematy:

  • Przejście na optymalizacje niskiego poziomu
  • Jak przeprowadzić dużą refaktoryzację
  • Model kosztów
  • Szkolenia optymalizacyjne na niskim poziomie
  • Praktyczne przykłady poprawy wydajności
  • Po co tworzyć własny język programowania
  • Kariera inżyniera wydajności
  • Wyzwania techniczne
  • Trochę o alokacji rejestrów i wielordzeniowych
  • Największe wyzwanie w życiu

Wywiad prowadzi:

  • Andriej Satarin z usług internetowych Amazon. W swojej karierze udało mu się pracować przy zupełnie innych projektach: testował rozproszoną bazę danych NewSQL w Yandex, system wykrywania chmur w Kaspersky Lab, grę wieloosobową w Mail.ru oraz usługę przeliczania cen walut w Deutsche Bank. Zainteresowany testowaniem wielkoskalowych systemów backendowych i rozproszonych.
  • Włodzimierz Sitnikow z Netcrackera. Dziesięć lat pracy nad wydajnością i skalowalnością NetCracker OS, oprogramowania wykorzystywanego przez operatorów telekomunikacyjnych do automatyzacji procesów zarządzania siecią i sprzętem sieciowym. Interesują mnie problemy wydajnościowe Java i Oracle Database. Autor kilkunastu ulepszeń wydajnościowych w oficjalnym sterowniku PostgreSQL JDBC.

Przejście na optymalizacje niskiego poziomu

Andrew: Jesteś wielkim nazwiskiem w świecie kompilacji JIT, języka Java i ogólnie pracy z wydajnością, prawda? 

Klif: To tak!

Andrew: Zacznijmy od kilku ogólnych pytań dotyczących pracy z performansem. Co sądzisz o wyborze pomiędzy optymalizacjami wysokiego i niskiego poziomu, takimi jak praca na poziomie procesora?

Klif: Tak, tutaj wszystko jest proste. Najszybszy kod to taki, który nigdy się nie uruchamia. Dlatego zawsze trzeba zaczynać od wysokiego poziomu, pracować nad algorytmami. Lepszy zapis O będzie lepszy od gorszego zapisu O, chyba że interweniują wystarczająco duże stałe. Sprawy niskiego poziomu idą na sam koniec. Zwykle, jeśli wystarczająco dobrze zoptymalizowałeś resztę stosu i nadal pozostało kilka interesujących rzeczy, jest to niski poziom. Ale jak zacząć od wysokiego poziomu? Skąd wiesz, że wykonano wystarczająco dużo pracy na wysokim szczeblu? Cóż... nie ma mowy. Nie ma gotowych przepisów. Musisz zrozumieć problem, zdecydować, co będziesz robić (aby w przyszłości nie podejmować niepotrzebnych kroków), a następnie możesz odkryć profiler, który może powiedzieć coś przydatnego. W pewnym momencie sam zdajesz sobie sprawę, że pozbyłeś się niepotrzebnych rzeczy i nadszedł czas na drobne dostrojenie. Jest to z pewnością szczególny rodzaj sztuki. Jest wielu ludzi, którzy robią niepotrzebne rzeczy, ale poruszają się tak szybko, że nie mają czasu martwić się o produktywność. Ale tak jest, dopóki pytanie nie pojawi się wprost. Zwykle w 99% przypadków nikt nie przejmuje się tym, co robię, aż do momentu, gdy na ścieżce krytycznej pojawia się ważna rzecz, która nikogo nie obchodzi. I tu wszyscy zaczynają Cię dręczyć „dlaczego od samego początku nie działało idealnie”. Ogólnie rzecz biorąc, zawsze jest coś do poprawy wydajności. Ale w 99% przypadków nie masz żadnych potencjalnych klientów! Po prostu próbujesz sprawić, żeby coś zadziałało, i w trakcie tego procesu odkrywasz, co jest ważne. Nigdy nie można z góry wiedzieć, że ten utwór musi być idealny, więc tak naprawdę musisz być doskonały we wszystkim. Ale to niemożliwe i tego nie robisz. Zawsze jest wiele rzeczy do naprawienia – i jest to całkowicie normalne.

Jak przeprowadzić dużą refaktoryzację

Andrew: Jak pracujesz nad występem? Jest to problem przekrojowy. Na przykład, czy kiedykolwiek musiałeś pracować nad problemami, które wynikały z połączenia wielu istniejących funkcjonalności?

Klif: Staram się tego unikać. Jeśli wiem, że wydajność będzie problemem, myślę o tym przed rozpoczęciem kodowania, szczególnie w przypadku struktur danych. Ale często odkrywasz to wszystko bardzo później. A potem trzeba zastosować ekstremalne środki i zrobić to, co nazywam „przepisz i podbij”: musisz chwycić wystarczająco duży kawałek. Część kodu nadal będzie musiała zostać przepisana ze względu na problemy z wydajnością lub z innego powodu. Bez względu na powód przepisywania kodu, prawie zawsze lepiej jest przepisać większy fragment niż mniejszy. W tym momencie wszyscy zaczynają się trząść ze strachu: „o mój Boże, nie można dotykać tak dużej ilości kodu!” Ale tak naprawdę to podejście prawie zawsze działa znacznie lepiej. Trzeba od razu zająć się dużym problemem, narysować wokół niego duży okrąg i powiedzieć: przepiszę wszystko w okręgu. Obramowanie jest znacznie mniejsze niż znajdująca się w nim zawartość, którą należy wymienić. A jeśli takie wytyczenie granic pozwala na perfekcyjne wykonanie pracy wewnątrz, Twoje ręce są wolne, rób co chcesz. Gdy zrozumiesz problem, proces przepisywania jest znacznie łatwiejszy, więc weź duży kęs!
Jednocześnie, gdy dokonasz dużej zmiany i zdasz sobie sprawę, że wydajność będzie problemem, możesz natychmiast zacząć się tym martwić. Zwykle zamienia się to w proste rzeczy, takie jak „nie kopiuj danych, zarządzaj danymi tak prosto, jak to możliwe, staraj się, aby były małe”. W przypadku dużych przeróbek istnieją standardowe sposoby poprawy wydajności. I prawie zawsze krążą wokół danych.

Model kosztów

Andrew: W jednym z podcastów mówiłeś o modelach kosztów w kontekście produktywności. Czy możesz wyjaśnić, co chciałeś przez to powiedzieć?

Klif: Z pewnością. Urodziłem się w erze, w której wydajność procesora była niezwykle ważna. I ta era powraca ponownie – los nie jest pozbawiony ironii. Zacząłem żyć w czasach maszyn ośmiobitowych, mój pierwszy komputer pracował z 256 bajtami. Dokładnie bajty. Wszystko było bardzo małe. Instrukcje trzeba było policzyć, a gdy zaczęliśmy przesuwać się w górę stosu języków programowania, języków było coraz więcej. Był Assembler, potem Basic, potem C, a C zajął się wieloma szczegółami, takimi jak przydział rejestrów i wybór instrukcji. Ale tam wszystko było całkiem jasne i jeśli umieściłbym wskaźnik na instancję zmiennej, to otrzymałbym obciążenie i znany jest koszt tej instrukcji. Sprzęt wytwarza określoną liczbę cykli maszynowych, więc szybkość wykonywania różnych rzeczy można obliczyć po prostu dodając wszystkie instrukcje, które zamierzasz wykonać. Każde porównanie/test/oddział/wezwanie/załadunek/sklep można zsumować i powiedzieć: to jest dla Ciebie czas realizacji. Pracując nad poprawą wydajności, z pewnością zwrócisz uwagę na to, jakie liczby odpowiadają małym cyklom na gorąco. 
Ale gdy tylko przejdziesz na Javę, Python i podobne rzeczy, bardzo szybko odejdziesz od sprzętu niskiego poziomu. Jaki jest koszt wywołania modułu pobierającego w Javie? Jeśli JIT w HotSpot jest poprawny wstawione, zostanie załadowany, ale jeśli tego nie zrobi, będzie to wywołanie funkcji. Ponieważ wywołanie znajduje się w gorącej pętli, zastąpi wszystkie inne optymalizacje w tej pętli. Dlatego rzeczywisty koszt będzie znacznie wyższy. I od razu traci się możliwość spojrzenia na kawałek kodu i zrozumienia, że ​​powinniśmy go wykonać pod kątem taktowania procesora, wykorzystanej pamięci i pamięci podręcznej. Wszystko to staje się interesujące tylko wtedy, gdy naprawdę wkręcisz się w występ.
Teraz znaleźliśmy się w sytuacji, w której prędkości procesorów prawie nie wzrastały od dekady. Dawne czasy wróciły! Nie można już liczyć na dobrą wydajność jednowątkową. Ale jeśli nagle zaczniesz zajmować się przetwarzaniem równoległym, będzie to niezwykle trudne, wszyscy patrzą na ciebie jak na Jamesa Bonda. Przyspieszenia dziesięciokrotne występują tu zwykle w miejscach, gdzie ktoś coś schrzanił. Współbieżność wymaga dużo pracy. Aby uzyskać XNUMX-krotne przyspieszenie, musisz zrozumieć model kosztów. Co i ile to kosztuje. Aby to zrobić, musisz zrozumieć, jak język pasuje do podstawowego sprzętu.
Martin Thompson wybrał świetne słowo na określenie swojego bloga Sympatia mechaniczna! Przede wszystkim musisz zrozumieć, co sprzęt będzie robił, jak dokładnie to zrobi i dlaczego robi to, co robi. Korzystając z tego, dość łatwo jest zacząć liczyć instrukcje i dowiedzieć się, dokąd zmierza czas wykonania. Jeśli nie masz odpowiedniego przeszkolenia, szukasz po prostu czarnego kota w ciemnym pokoju. Widzę ludzi, którzy cały czas optymalizują wydajność, ale nie mają pojęcia, co do cholery robią. Bardzo cierpią i nie robią dużych postępów. A kiedy biorę ten sam fragment kodu, dodaję kilka drobnych hacków i uzyskuję pięcio- lub dziesięciokrotne przyspieszenie, oni mówią: cóż, to niesprawiedliwe, już wiedzieliśmy, że jesteś lepszy. Niesamowity. O czym ja mówię… model kosztów dotyczy tego, jaki rodzaj kodu piszesz i jak szybko on działa, biorąc pod uwagę ogólny obraz.

Andrew: A jak utrzymać taki wolumin w głowie? Czy można to osiągnąć dzięki większemu doświadczeniu, czy też? Skąd bierze się takie doświadczenie?

Klif: Cóż, nie zdobyłem mojego doświadczenia w najłatwiejszy sposób. Programowałem w asemblerze w czasach, gdy można było zrozumieć każdą instrukcję. Brzmi głupio, ale od tego czasu zestaw instrukcji Z80 zawsze pozostał w mojej głowie, w mojej pamięci. Nie pamiętam imion ludzi w ciągu minuty od rozmowy, ale pamiętam kod napisany 40 lat temu. To zabawne, wygląda to na syndrom”idiota naukowiec".

Szkolenia optymalizacyjne na niskim poziomie

Andrew: Czy jest łatwiejszy sposób na dostanie się do środka?

Klif: Tak i nie. Sprzęt, którego wszyscy używamy, nie zmienił się zbytnio na przestrzeni czasu. Wszyscy korzystają z x86, z wyjątkiem smartfonów Arm. Jeśli nie robisz jakiegoś hardcorowego osadzania, robisz to samo. OK, ruszaj dalej. Instrukcje również nie zmieniły się od wieków. Musisz iść i napisać coś w Zgromadzeniu. Niewiele, ale wystarczająco, żeby zacząć rozumieć. Uśmiechasz się, ale mówię całkowicie poważnie. Musisz zrozumieć zgodność między językiem a sprzętem. Potem musisz trochę napisać i stworzyć mały kompilator zabawek dla małego języka zabawek. Zabawkowy, co oznacza, że ​​należy go wykonać w rozsądnym czasie. Może to być bardzo proste, ale musi generować instrukcje. Generowanie instrukcji pomoże Ci zrozumieć model kosztów będący pomostem pomiędzy kodem wysokiego poziomu, który wszyscy piszą, a kodem maszynowym działającym na sprzęcie. Ta korespondencja zostanie wypalona w mózgu w momencie pisania kompilatora. Nawet najprostszy kompilator. Potem można już zacząć przyglądać się Javie i faktowi, że jej semantyczna przepaść jest dużo głębsza i dużo trudniej jest nad nią budować mosty. W Javie znacznie trudniej jest zrozumieć, czy nasz most okazał się dobry czy zły, co spowoduje jego rozpad, a co nie. Potrzebujesz jednak jakiegoś punktu wyjścia, od którego spojrzysz na kod i zrozumiesz: „tak, ten moduł pobierający powinien być wstawiany za każdym razem”. A potem okazuje się, że czasami tak się dzieje, z wyjątkiem sytuacji, gdy metoda staje się zbyt duża i JIT zaczyna wszystko wpisywać. Działanie takich miejsc można natychmiast przewidzieć. Zwykle programy pobierające działają dobrze, ale potem patrzysz na duże, gorące pętle i zdajesz sobie sprawę, że krążą tam pewne wywołania funkcji, które nie wiedzą, co robią. Jest to problem związany z powszechnym stosowaniem getterów. Powodem, dla którego nie są one wbudowane, jest to, że nie jest jasne, czy są one getterami. Jeśli masz bardzo małą bazę kodu, możesz po prostu ją zapamiętać, a następnie powiedzieć: to jest getter, a to jest seter. W dużej bazie kodu każda funkcja żyje swoją własną historią, która w sumie nie jest nikomu znana. Profiler mówi, że straciliśmy 24% czasu na jakiejś pętli i aby zrozumieć, co robi ta pętla, musimy przyjrzeć się każdej funkcji znajdującej się w środku. Nie da się tego zrozumieć bez przestudiowania funkcji, a to poważnie spowalnia proces zrozumienia. Dlatego nie używam getterów i setterów, osiągnąłem nowy poziom!
Skąd wziąć model kosztów? No cóż, oczywiście można coś przeczytać... Ale myślę, że najlepszym sposobem jest działanie. Stworzenie małego kompilatora będzie najlepszym sposobem na zrozumienie modelu kosztów i dopasowanie go do własnej głowy. Mały kompilator, który nadawałby się do programowania kuchenki mikrofalowej, to zadanie dla początkującego. Cóż, jeśli masz już umiejętności programowania, to powinno wystarczyć. Wszystkie te rzeczy, takie jak analizowanie ciągu znaków, który masz jako pewnego rodzaju wyrażenie algebraiczne, wydobywanie stamtąd instrukcji operacji matematycznych we właściwej kolejności, pobieranie prawidłowych wartości z rejestrów - wszystko to odbywa się na raz. A kiedy to zrobisz, zostanie to odciśnięte w twoim mózgu. Myślę, że każdy wie, co robi kompilator. To pozwoli zrozumieć model kosztów.

Praktyczne przykłady poprawy wydajności

Andrew: Na co jeszcze zwrócić uwagę pracując nad produktywnością?

Klif: Struktury danych. Swoją drogą, tak, dawno nie prowadziłam tych zajęć… Szkoła rakietowa. Było fajnie, ale wymagało to sporo wysiłku, a przecież ja też mam życie! OK. Tak więc na jednych z dużych i interesujących zajęć „Dokąd zmierzają twoje wyniki” dałem uczniom przykład: z pliku CSV odczytano dwa i pół gigabajta danych fintech, a następnie musieli obliczyć liczbę sprzedanych produktów . Regularne dane rynkowe. Pakiety UDP konwertowane do formatu tekstowego od lat 70-tych. Chicago Mercantile Exchange – wszelkiego rodzaju rzeczy, takie jak masło, kukurydza, soja i tym podobne. Trzeba było policzyć te produkty, liczbę transakcji, średni wolumen przepływu środków i towarów itp. To całkiem prosta matematyka handlowa: znajdź kod produktu (to 1-2 znaki w tabeli skrótów), uzyskaj kwotę, dodaj ją do jednego z zestawów transakcji, dodaj wolumen, dodaj wartość i kilka innych rzeczy. Bardzo prosta matematyka. Implementacja zabawki była bardzo prosta: wszystko jest w pliku, czytam plik i poruszam się po nim, dzieląc poszczególne rekordy na ciągi Java, wyszukując w nich potrzebne rzeczy i dodając je zgodnie z opisaną powyżej matematyką. I działa przy niewielkiej prędkości.

Dzięki takiemu podejściu jest oczywiste, co się dzieje, a przetwarzanie równoległe nie pomoże, prawda? Okazuje się, że pięciokrotny wzrost wydajności można osiągnąć po prostu wybierając odpowiednie struktury danych. A to zaskakuje nawet doświadczonych programistów! W moim konkretnym przypadku sztuczka polegała na tym, że nie powinieneś dokonywać alokacji pamięci w gorącej pętli. No cóż, nie jest to cała prawda, ale ogólnie – nie należy podkreślać „raz w X”, gdy X jest wystarczająco duże. Kiedy X ma dwa i pół gigabajta, nie powinieneś przydzielać niczego „raz na literę”, „raz na linię” lub „raz na pole” itp. To tutaj spędza się czas. Jak to w ogóle działa? Wyobraź sobie, że dzwonię String.split() lub BufferedReader.readLine(). Readline tworzy ciąg znaków ze zbioru bajtów przesłanych przez sieć, raz dla każdej linii i dla każdej z setek milionów linii. Biorę ten wiersz, analizuję go i wyrzucam. Dlaczego to wyrzucam - cóż, już to przetworzyłem, to wszystko. Zatem na każdy bajt odczytany z tych 2.7G w linii zostaną zapisane dwa znaki, czyli już 5.4G i nie są mi już potrzebne do niczego więcej, więc są wyrzucane. Jeśli spojrzeć na przepustowość pamięci, ładujemy 2.7 ​​G, które przechodzi przez pamięć i magistralę pamięci w procesorze, a następnie dwa razy więcej jest wysyłane do linii znajdującej się w pamięci, a wszystko to jest postrzępione przy tworzeniu każdej nowej linii. Ale muszę to przeczytać, sprzęt to czyta, nawet jeśli później wszystko się popsuje. A muszę to spisać, bo utworzyłem linię i pamięci podręczne są pełne - pamięć podręczna nie mieści 2.7G. Czyli na każdy bajt, który czytam, czytam jeszcze dwa bajty i zapisuję jeszcze dwa bajty, i na koniec mamy stosunek 4:1 - w tym stosunku marnujemy przepustowość pamięci. A potem okazuje się, że jeśli to zrobię String.split() – to nie ostatni raz, kiedy to robię, w środku może być jeszcze 6-7 pól. Zatem klasyczny kod odczytujący plik CSV, a następnie analizujący ciągi znaków powoduje marnowanie przepustowości pamięci o około 14:1 w stosunku do tego, co faktycznie chciałbyś mieć. Jeśli wyrzucisz te wybory, możesz uzyskać pięciokrotne przyspieszenie.

I to nie jest takie trudne. Jeśli spojrzysz na kod pod odpowiednim kątem, wszystko stanie się całkiem proste, gdy zdasz sobie sprawę z problemu. Nie powinieneś całkowicie rezygnować z alokacji pamięci: jedyny problem jest taki, że coś alokujesz i to natychmiast umiera, a po drodze spala ważny zasób, którym w tym przypadku jest przepustowość pamięci. A to wszystko skutkuje spadkiem produktywności. Na x86 zwykle trzeba aktywnie spalać cykle procesora, ale tutaj spaliłeś całą pamięć znacznie wcześniej. Rozwiązaniem jest zmniejszenie ilości wyładowań. 
Inną częścią problemu jest to, że jeśli uruchomisz profiler, gdy skończy się pasek pamięci, właśnie wtedy, gdy to się stanie, zwykle będziesz czekać na powrót pamięci podręcznej, ponieważ jest ona pełna śmieci, które właśnie wygenerowałeś, wszystkich tych linii. Dlatego każda operacja ładowania lub przechowywania staje się powolna, ponieważ prowadzą do błędów w pamięci podręcznej - cała pamięć podręczna stała się powolna i czeka, aż śmieci ją opuszczą. Dlatego profiler pokaże po prostu ciepły losowy szum rozmazany w całej pętli - nie będzie osobnej gorącej instrukcji ani miejsca w kodzie. Tylko hałas. A jeśli spojrzysz na cykle GC, wszystkie są młodszej generacji i są superszybkie - maksymalnie w mikrosekundach lub milisekundach. W końcu cała ta pamięć umiera natychmiast. Przydzielasz miliardy gigabajtów, a on je odcina, odcina i jeszcze raz odcina. Wszystko to dzieje się bardzo szybko. Okazuje się, że są tanie cykle GC, ciepły szum w całym cyklu, ale chcemy uzyskać 5-krotne przyspieszenie. W tym momencie coś powinno zamknąć się w Twojej głowie i zabrzmieć: „dlaczego to jest?!” Przepełnienie paska pamięci nie jest wyświetlane w klasycznym debugerze; musisz uruchomić debuger licznika wydajności sprzętu i zobaczyć to osobiście i bezpośrednio. Ale nie można tego bezpośrednio podejrzewać na podstawie tych trzech objawów. Trzeci symptom polega na tym, że gdy spojrzysz na to, co podkreślasz, zapytasz profilera, a on odpowie: „Utworzyłeś miliard wierszy, ale GC działało za darmo”. Gdy tylko to nastąpi, zdajesz sobie sprawę, że utworzyłeś zbyt wiele obiektów i spaliłeś całą ścieżkę pamięci. Można to rozwiązać w sposób, ale nie jest to oczywiste. 

Problem leży w strukturze danych: naga struktura leżąca u podstaw wszystkiego, co się dzieje, jest za duża, zajmuje 2.7 G na dysku, więc tworzenie kopii tego jest bardzo niepożądane - chcesz to natychmiast załadować z sieciowego bufora bajtów do rejestrów, aby nie czytać i nie zapisywać linii tam i z powrotem pięć razy. Niestety Java domyślnie nie udostępnia takiej biblioteki w ramach JDK. Ale to trywialne, prawda? Zasadniczo jest to 5–10 linii kodu, które zostaną użyte do zaimplementowania własnego buforowanego modułu ładującego ciągi znaków, który powtarza zachowanie klasy string, będąc jednocześnie opakowaniem wokół bazowego bufora bajtów. W rezultacie okazuje się, że pracujesz prawie tak, jakbyś pracował z ciągami znaków, ale tak naprawdę wskaźniki do bufora przemieszczają się tam, a surowe bajty nie są nigdzie kopiowane, a co za tym idzie, w kółko wykorzystywane są te same bufory, i system operacyjny chętnie bierze na siebie zadania, do których został zaprojektowany, na przykład ukryte podwójne buforowanie buforów bajtów, dzięki czemu nie musisz już przeglądać niekończącego się strumienia niepotrzebnych danych. Swoją drogą, czy rozumiesz, że pracując z GC masz gwarancję, że każda alokacja pamięci nie będzie widoczna dla procesora po ostatnim cyklu GC? Dlatego wszystko to nie może znajdować się w pamięci podręcznej, a wtedy następuje gwarantowany w 100% błąd. Podczas pracy ze wskaźnikiem na x86 odejmowanie rejestru z pamięci zajmuje 1-2 cykle zegara i jak tylko to nastąpi, płacisz, płacisz, płacisz, bo pamięć jest cała włączona DZIEWIĘĆ skrytek – i taki jest koszt alokacji pamięci. Prawdziwa wartość.

Innymi słowy, struktury danych są rzeczą, którą najtrudniej zmienić. A kiedy już zdasz sobie sprawę, że wybrałeś niewłaściwą strukturę danych, która później zabije wydajność, zwykle czeka Cię dużo pracy, ale jeśli tego nie zrobisz, sytuacja się pogorszy. Przede wszystkim musisz pomyśleć o strukturach danych, to ważne. Główny koszt spada na grube struktury danych, które zaczynają być używane w stylu „Skopiowałem strukturę danych X do struktury danych Y, ponieważ bardziej podoba mi się kształt Y”. Ale operacja kopiowania (która wydaje się tania) w rzeczywistości marnuje przepustowość pamięci i na tym chowa się cały zmarnowany czas wykonania. Jeśli mam gigantyczny ciąg JSON i chcę go przekształcić w strukturalne drzewo DOM zawierające POJO lub coś w tym stylu, operacja analizowania tego ciągu i budowania POJO, a następnie ponowne uzyskanie dostępu do POJO później, spowoduje niepotrzebne koszty - to nie tanie. Z wyjątkiem sytuacji, gdy biegasz wokół POJO znacznie częściej niż biegasz wokół sznurka. Zamiast tego możesz spróbować odszyfrować ciąg i wyodrębnić stamtąd tylko to, czego potrzebujesz, bez przekształcania go w jakiekolwiek POJO. Jeśli wszystko to dzieje się na ścieżce, od której wymagana jest maksymalna wydajność, nie ma dla Ciebie POJO, musisz jakoś bezpośrednio zagłębić się w linię.

Po co tworzyć własny język programowania

Andrew: Powiedziałeś, że aby zrozumieć model kosztów, musisz napisać swój własny mały język...

Klif: Nie język, ale kompilator. Język i kompilator to dwie różne rzeczy. Najważniejsza różnica jest w Twojej głowie. 

Andrew: Przy okazji, o ile wiem, eksperymentujesz z tworzeniem własnych języków. Po co?

Klif: Bo mogę! Jestem na emeryturze, więc jest to moje hobby. Całe życie wdrażam języki innych ludzi. Dużo pracowałem także nad swoim stylem kodowania. A także dlatego, że widzę problemy w innych językach. Widzę, że są lepsze sposoby na robienie znanych rzeczy. A ja bym z nich skorzystał. Jestem po prostu zmęczony dostrzeganiem problemów w sobie, w Javie, w Pythonie, w jakimkolwiek innym języku. Teraz piszę w React Native, JavaScript i Elm w ramach hobby, w którym nie chodzi o emeryturę, ale o aktywną pracę. Piszę także w Pythonie i najprawdopodobniej nadal będę pracować nad uczeniem maszynowym dla backendów Java. Istnieje wiele popularnych języków i wszystkie mają ciekawe funkcje. Każdy jest dobry na swój sposób i możesz spróbować połączyć wszystkie te cechy w jedną całość. Studiuję więc rzeczy, które mnie interesują, zachowanie języka, próbując wymyślić rozsądną semantykę. I jak na razie mi się to udaje! W tej chwili walczę z semantyką pamięci, ponieważ chcę mieć ją jak w C i Javie oraz uzyskać silny model pamięci i semantykę pamięci dla ładunków i sklepów. Jednocześnie korzystaj z automatycznego wnioskowania o typie, jak w Haskell. Tutaj próbuję połączyć wnioskowanie typu Haskella z pracą z pamięcią zarówno w C, jak i Javie. Ja na przykład tak robię przez ostatnie 2-3 miesiące.

Andrew: Jeśli zbudujesz język, który będzie czerpał lepsze aspekty z innych języków, czy myślisz, że ktoś zrobi coś odwrotnego: weźmie twoje pomysły i wykorzysta je?

Klif: Dokładnie tak pojawiają się nowe języki! Dlaczego Java jest podobna do C? Ponieważ C miał dobrą składnię, którą każdy rozumiał, a Java została zainspirowana tą składnią, dodając bezpieczeństwo typów, sprawdzanie granic tablic, GC, a także ulepszyli pewne rzeczy z C. Dodali własne. Ale inspirowali się całkiem sporo, prawda? Wszyscy stoją na ramionach gigantów, którzy byli przed tobą - tak dokonuje się postępu.

Andrew: Jak rozumiem, twój język będzie bezpieczny dla pamięci. Czy myślałeś o zaimplementowaniu czegoś takiego jak moduł sprawdzania pożyczek od Rusta? Przyglądałeś się mu, co o nim myślisz?

Klif: Cóż, piszę w C od wieków, z całym tym malloc i za darmo, i ręcznie zarządzam czasem życia. Wiesz, 90-95% ręcznie sterowanego czasu życia ma tę samą strukturę. A robienie tego ręcznie jest bardzo, bardzo bolesne. Chciałbym, żeby kompilator po prostu powiedział Ci, co się tam dzieje i co osiągnąłeś dzięki swoim działaniom. W przypadku niektórych rzeczy narzędzie do sprawdzania pożyczek robi to od razu po wyjęciu z pudełka. I powinien automatycznie wyświetlać informacje, wszystko rozumieć, a nawet nie obciążać mnie prezentowaniem tego zrozumienia. Musi przeprowadzić przynajmniej lokalną analizę ucieczki i tylko jeśli się nie powiedzie, musi dodać adnotacje typu, które opisują czas życia - a taki schemat jest znacznie bardziej złożony niż moduł sprawdzający pożyczanie, czy w rzeczywistości jakikolwiek istniejący moduł sprawdzający pamięć. Wybór pomiędzy „wszystko w porządku” a „nic nie rozumiem” – nie, musi być coś lepszego. 
Tak więc, jako ktoś, kto napisał dużo kodu w C, uważam, że obsługa automatycznej kontroli czasu życia jest najważniejszą rzeczą. Mam też dość tego, ile Java zużywa pamięci, a głównym zarzutem jest GC. Kiedy alokujesz pamięć w Javie, nie odzyskasz pamięci, która była lokalna w ostatnim cyklu GC. Nie dzieje się tak w językach z bardziej precyzyjnym zarządzaniem pamięcią. Jeśli wywołasz malloc, natychmiast otrzymasz pamięć, która zwykle była właśnie używana. Zwykle robisz tymczasowe rzeczy z pamięcią i natychmiast ją zwracasz. I natychmiast wraca do puli malloc, a następny cykl malloc ponownie ją wyciąga. Dlatego rzeczywiste wykorzystanie pamięci ogranicza się do zestawu żywych obiektów w danym czasie plus wycieki. A jeśli wszystko nie wycieknie w zupełnie nieprzyzwoity sposób, większość pamięci ląduje w pamięci podręcznej i procesorze i działa szybko. Ale wymaga dużo ręcznego zarządzania pamięcią za pomocą malloc i free wywoływanych we właściwej kolejności i we właściwym miejscu. Rust sam sobie z tym radzi i w wielu przypadkach zapewnia jeszcze lepszą wydajność, ponieważ zużycie pamięci jest zawężone do bieżących obliczeń - zamiast czekać na następny cykl GC w celu zwolnienia pamięci. W rezultacie otrzymaliśmy bardzo ciekawy sposób na poprawę wydajności. I dość potężny - to znaczy, robiłem takie rzeczy przy przetwarzaniu danych dla fintechu i to pozwoliło mi uzyskać przyspieszenie około pięciokrotne. To całkiem duży impuls, szczególnie w świecie, w którym procesory nie są coraz szybsze i wciąż czekamy na ulepszenia.

Kariera inżyniera wydajności

Andrew: Chciałbym też zapytać ogólnie o karierę. Zyskałeś rozgłos dzięki pracy w JIT w HotSpot, a następnie przeniosłeś się do Azul, który również jest firmą JVM. Ale pracowaliśmy już więcej nad sprzętem niż oprogramowaniem. A potem nagle przeszli na Big Data i uczenie maszynowe, a następnie na wykrywanie oszustw. Jak to się stało? To są bardzo różne obszary rozwoju.

Klif: Programuję już od dłuższego czasu i udało mi się uczęszczać na wiele różnych zajęć. A kiedy ludzie mówią: „och, to ty zrobiłeś JIT dla Java!”, zawsze jest to zabawne. Ale wcześniej pracowałem nad klonem PostScriptu – języka, którego Apple używał kiedyś w swoich drukarkach laserowych. A wcześniej zrobiłem implementację języka Forth. Myślę, że moim wspólnym tematem jest rozwój narzędzi. Całe życie tworzę narzędzia, za pomocą których inni ludzie piszą swoje fajne programy. Ale byłem także zaangażowany w rozwój systemów operacyjnych, sterowników, debuggerów na poziomie jądra, języków do tworzenia systemów operacyjnych, które na początku były banalne, ale z biegiem czasu stawały się coraz bardziej złożone. Jednak głównym tematem nadal pozostaje rozwój narzędzi. Duża część mojego życia upłynęła pomiędzy Azul i Sun i dotyczyła Javy. Ale kiedy zająłem się Big Data i uczeniem maszynowym, ponownie założyłem kapelusz i powiedziałem: „Och, teraz mamy nietrywialny problem, a dzieje się wiele interesujących rzeczy i ludzie coś robią”. To świetna ścieżka rozwoju, którą warto obrać.

Tak, naprawdę uwielbiam przetwarzanie rozproszone. Moją pierwszą pracą była praca jako studentka w C przy projekcie reklamowym. Miało to charakter obliczeń rozproszonych na chipach Zilog Z80, które zbierały dane do analogowego OCR, generowanego przez prawdziwy analizator analogowy. To był fajny i całkowicie szalony temat. Ale były problemy, jakaś część nie została poprawnie rozpoznana, więc trzeba było zrobić zdjęcie i pokazać je osobie, która już oczami umiała czytać i zgłosić, co jest na niej napisane, i dlatego były zadania z danymi i te zadania mieli swój własny język. Istniał backend, który to wszystko przetwarzał – Z80 działał równolegle z działającymi terminalami vt100 – po jednym na osobę, a na Z80 istniał model programowania równoległego. Jakiś wspólny fragment pamięci współdzielony przez wszystkie Z80 w konfiguracji gwiazdy; Płyta montażowa również była współdzielona, ​​a połowa pamięci RAM była współdzielona w sieci, a druga połowa była prywatna lub poszła do czegoś innego. Znacząco złożony, równoległy system rozproszony ze współdzieloną... częściowo współdzieloną pamięcią. Kiedy to było... Już nawet nie pamiętam, gdzieś w połowie lat 80-tych. Całkiem dawno temu. 
Tak, załóżmy, że 30 lat to dość dawno temu, problemy związane z przetwarzaniem rozproszonym istnieją już od dawna, ludzie od dawna toczą wojnę z Beowulf-klastry. Takie klastry wyglądają tak... Na przykład: jest Ethernet i twój szybki x86 jest podłączony do tego Ethernetu, a teraz chcesz uzyskać fałszywą pamięć współdzieloną, ponieważ nikt wtedy nie potrafił kodować obliczeń rozproszonych, było to zbyt trudne i dlatego nie było była fałszywą pamięcią współdzieloną ze stronami pamięci współdzielonej na x86 i jeśli napisałeś na tę stronę, to powiedzieliśmy innym procesorom, że jeśli uzyskają dostęp do tej samej pamięci współdzielonej, będą musiały zostać załadowane od ciebie, a zatem coś w rodzaju protokołu do obsługi pojawiła się spójność pamięci podręcznej i oprogramowanie do tego. Ciekawa koncepcja. Prawdziwym problemem było oczywiście coś innego. Wszystko to zadziałało, ale szybko pojawiły się problemy z wydajnością, ponieważ nikt nie rozumiał modeli wydajności na wystarczająco dobrym poziomie – jakie były wzorce dostępu do pamięci, jak się upewnić, że węzły nie pingowały się nawzajem w nieskończoność i tak dalej.

W H2O wymyśliłem, że to sami programiści są odpowiedzialni za określenie, gdzie równoległość jest ukryta, a gdzie nie. Wymyśliłem model kodowania, który sprawił, że pisanie kodu o wysokiej wydajności było łatwe i proste. Ale pisanie wolno działającego kodu jest trudne, będzie źle wyglądać. Musisz poważnie spróbować napisać powolny kod, będziesz musiał użyć niestandardowych metod. Kod hamulca widać na pierwszy rzut oka. W rezultacie zwykle piszesz kod, który działa szybko, ale musisz dowiedzieć się, co zrobić w przypadku pamięci współdzielonej. Wszystko to jest powiązane z dużymi tablicami, a zachowanie jest podobne do nieulotnych dużych tablic w równoległej Javie. To znaczy wyobraź sobie, że dwa wątki piszą do tablicy równoległej, jeden z nich wygrywa, a drugi odpowiednio przegrywa i nie wiesz, który jest który. Jeśli nie są one zmienne, kolejność może być dowolna - i to działa naprawdę dobrze. Ludziom naprawdę zależy na kolejności operacji, umieszczają zmienne w odpowiednich miejscach i spodziewają się problemów z wydajnością związanych z pamięcią we właściwych miejscach. W przeciwnym razie po prostu napisaliby kod w postaci pętli od 1 do N, gdzie N to kilka bilionów, w nadziei, że wszystkie złożone przypadki automatycznie staną się równoległe - a w tym przypadku to nie działa. Ale w H2O nie jest to ani Java, ani Scala; jeśli chcesz, możesz to uznać za „Java minus minus”. Jest to bardzo przejrzysty styl programowania, podobny do pisania prostego kodu C lub Java z pętlami i tablicami. Ale jednocześnie pamięć można przetwarzać w terabajtach. Nadal używam H2O. Używam go od czasu do czasu w różnych projektach - i nadal jest najszybszą rzeczą, kilkadziesiąt razy szybszą od konkurencji. Jeśli tworzysz Big Data z danymi kolumnowymi, bardzo trudno jest pokonać H2O.

Wyzwania techniczne

Andrew: Jakie było Twoje największe wyzwanie w całej karierze?

Klif: Czy omawiamy techniczną czy nietechniczną część problemu? Powiedziałbym, że największe wyzwania nie mają charakteru technicznego. 
Jeśli chodzi o wyzwania techniczne. Po prostu ich pokonałem. Nie wiem nawet, który był największy, ale było kilka całkiem interesujących, które zajmowały sporo czasu, zmagania mentalne. Kiedy poszedłem do Suna, byłem pewien, że zrobię szybki kompilator, a grupa seniorów odpowiedziała, że ​​nigdy mi się to nie uda. Ale poszedłem tą ścieżką, zapisałem kompilator do alokatora rejestru i było to dość szybkie. Był tak szybki jak nowoczesny C1, ale wówczas alokator był znacznie wolniejszy i patrząc wstecz, był to duży problem ze strukturą danych. Potrzebowałem go do napisania graficznego alokatora rejestrów i nie rozumiałem dylematu pomiędzy wyrazistością kodu a szybkością, który istniał w tamtych czasach i był bardzo ważny. Okazało się, że struktura danych zwykle przekracza rozmiar pamięci podręcznej na ówczesnych x86, dlatego jeśli początkowo założyłem, że alokator rejestrów wypracuje 5-10 procent całkowitego czasu jittera, to w rzeczywistości okazało się, że 50 procent.

Z biegiem czasu kompilator stał się czystszy i wydajniejszy, w coraz większej liczbie przypadków przestał generować okropny kod, a wydajność coraz bardziej przypominała to, co produkuje kompilator C. Chyba, że ​​napiszesz jakieś bzdury, których nawet C nie przyspieszy . Jeśli napiszesz kod taki jak C, w większej liczbie przypadków uzyskasz wydajność podobną do C. Im dalej szedłeś, tym częściej otrzymywałeś kod, który asymptotycznie pokrywał się z poziomem C, alokator rejestrów zaczął wyglądać jak coś kompletnego... niezależnie od tego, czy Twój kod działa szybko, czy wolno. Kontynuowałem pracę nad alokatorem, aby dokonywał lepszych selekcji. Stawał się coraz wolniejszy, ale dawał coraz lepsze wyniki w przypadkach, w których nikt inny nie mógł sobie poradzić. Mógłbym zanurzyć się w alokatorze rejestrów, pogrzebać tam miesiąc pracy i nagle cały kod zacząłby działać o 5% szybciej. Działo się to raz po raz i alokator rejestru stawał się czymś w rodzaju dzieła sztuki – wszystkim się to podobało lub nienawidziło, a ludzie z akademii zadawali pytania na temat „dlaczego wszystko jest tak zrobione”, dlaczego nie skanowanie liniowei jaka jest różnica. Odpowiedź jest wciąż ta sama: alokator oparty na kolorowaniu grafów i bardzo starannej pracy z kodem buforowym to broń zwycięstwa, najlepsza kombinacja, której nikt nie jest w stanie pokonać. A to rzecz raczej nieoczywista. Wszystko inne, co kompilator tam robi, jest dość dobrze zbadane, chociaż również zostało sprowadzone do poziomu sztuki. Zawsze robiłem rzeczy, które miały zamienić kompilator w dzieło sztuki. Ale nic z tego nie było niczym nadzwyczajnym – z wyjątkiem alokatora rejestru. Sztuka polega na tym, żeby zachować ostrożność wyciąć pod obciążeniem, a jeśli tak się stanie (mogę wyjaśnić bardziej szczegółowo, jeśli jesteś zainteresowany), oznacza to, że możesz jeździć bardziej agresywnie, bez ryzyka przewrócenia się w harmonogramie wydajności. W tamtych czasach istniało mnóstwo pełnowymiarowych kompilatorów, obwieszonych bombkami i gwizdkami, które miały alokatory rejestrów, ale nikt inny nie mógł tego zrobić.

Problem w tym, że jeśli dodamy metody podlegające inline, zwiększaniu i zwiększaniu obszaru inline, to zbiór używanych wartości od razu przewyższa liczbę rejestrów i trzeba je wycinać. Poziom krytyczny zwykle pojawia się, gdy alokator się poddaje i jeden dobry kandydat na wyciek jest wart drugiego, sprzedasz jakieś ogólnie szalone rzeczy. Wartość wstawiania tutaj polega na tym, że tracisz część narzutu, narzutu na połączenia i zapisywanie, możesz zobaczyć wartości w środku i możesz je dalej optymalizować. Koszt inline polega na tym, że powstaje duża liczba wartości na żywo, a jeśli Twój alokator rejestru spali się bardziej niż to konieczne, natychmiast przegrywasz. Dlatego większość alokatorów ma problem: kiedy inline przekracza pewną linię, wszystko na świecie zaczyna być wycinane, a produktywność można spuścić do toalety. Ci, którzy implementują kompilator, dodają trochę heurystyki: na przykład, aby zatrzymać wstawianie, zaczynając od wystarczająco dużego rozmiaru, ponieważ alokacje wszystko zrujnują. W ten sposób powstaje załamanie na wykresie wydajności – inline, inline, wydajność powoli rośnie – a potem bum! – spada jak podnośnik, bo za dużo podłożyłeś. Tak wszystko działało przed pojawieniem się Javy. Java wymaga dużo więcej wstawiania, więc musiałem uczynić mój alokator znacznie bardziej agresywnym, aby wyrównywał się, a nie zawieszał, a jeśli wpiszesz za dużo, zaczął się rozlewać, ale wtedy wciąż nadchodził moment, w którym nie było już rozlewania. To ciekawa obserwacja, która przyszła mi do głowy nie wiadomo skąd, nie była oczywista, ale opłaciła się. Zająłem się agresywną inlinecją i zaprowadziło mnie to do miejsc, gdzie wydajność Java i C idą ramię w ramię. Są naprawdę blisko — potrafię napisać kod w Javie, który jest znacznie szybszy niż kod C i tym podobne, ale ogólnie rzecz biorąc, są one mniej więcej porównywalne. Myślę, że częścią tej zasługi jest alokator rejestrów, który pozwala mi na wstawianie tak głupio, jak to możliwe. Po prostu wpisuję wszystko, co widzę. Pytanie brzmi, czy alokator działa dobrze i czy wynikiem jest inteligentnie działający kod. To było duże wyzwanie: zrozumieć to wszystko i sprawić, by zadziałało.

Trochę o alokacji rejestrów i wielordzeniowych

Władimir: Problemy takie jak przydzielanie rejestrów wydają się być odwiecznym, niekończącym się tematem. Zastanawiam się, czy kiedykolwiek pojawił się pomysł, który wydawał się obiecujący, ale potem zawiódł w praktyce?

Klif: Z pewnością! Alokacja rejestrów to obszar, w którym próbujesz znaleźć heurystyki, aby rozwiązać problem NP-zupełny. A nigdy nie da się osiągnąć idealnego rozwiązania, prawda? To jest po prostu niemożliwe. Spójrz, kompilacja Ahead of Time - również działa słabo. Rozmowa tutaj dotyczy kilku przeciętnych przypadków. O typowej wydajności, więc możesz iść i zmierzyć coś, co uważasz za dobrą, typową wydajność - w końcu pracujesz nad tym, aby ją ulepszyć! Alokacja rejestrów to temat dotyczący wydajności. Gdy już mamy pierwszy prototyp, działa i malujemy co potrzeba, rozpoczynają się prace wykonawcze. Trzeba nauczyć się dobrze mierzyć. Dlaczego to jest ważne? Jeśli masz jasne dane, możesz spojrzeć na różne obszary i zobaczyć: tak, tutaj pomogło, ale tam wszystko się zepsuło! Pojawia się kilka dobrych pomysłów, dodajesz nową heurystykę i nagle wszystko zaczyna działać średnio trochę lepiej. Albo się nie uruchamia. Miałem kilka przypadków, w których walczyliśmy o pięcioprocentową wydajność, która odróżniała nasz rozwój od poprzedniego alokatora. I za każdym razem wygląda to tak: gdzieś wygrywasz, gdzieś przegrywasz. Jeśli masz dobre narzędzia do analizy wydajności, możesz znaleźć tracące pomysły i zrozumieć, dlaczego zawiodły. Może warto zostawić wszystko tak jak jest, a może podejść do tuningu poważniej, albo wyjść i naprawić coś innego. To cała masa rzeczy! Zrobiłem ten fajny hack, ale potrzebuję też tego, i tego, i tego - a ich łączna kombinacja daje pewne ulepszenia. A samotnicy mogą ponieść porażkę. Taka jest natura pracy wydajnościowej nad problemami NP-zupełnymi.

Władimir: Można odnieść wrażenie, że takie rzeczy jak malowanie w rozdzielaczach to problem, który został już rozwiązany. Cóż, decyzja należy do ciebie, sądząc po tym, co mówisz, więc czy w takim razie w ogóle warto…

Klif: Sprawa nie została rozwiązana jako taka. To ty musisz zamienić sprawę w „rozwiązaną”. Są trudne problemy i trzeba je rozwiązać. Gdy już to zrobisz, czas popracować nad produktywnością. Musisz odpowiednio podejść do tej pracy - rób testy porównawcze, zbieraj metryki, wyjaśniaj sytuacje, gdy po powrocie do poprzedniej wersji stary hack zaczął znowu działać (lub odwrotnie, przestał). I nie poddawaj się, dopóki czegoś nie osiągniesz. Jak już powiedziałem, jeśli są fajne pomysły, które nie zadziałały, ale w zakresie alokacji rejestrów pomysłów, jest to w przybliżeniu nieskończone. Można na przykład czytać publikacje naukowe. Chociaż teraz obszar ten zaczął poruszać się znacznie wolniej i stał się wyraźniejszy niż w młodości. Jednak na tym polu pracuje niezliczona ilość osób i wszystkie ich pomysły są warte wypróbowania, wszystkie czekają na rozwój. I nie możesz stwierdzić, jak dobre są, jeśli ich nie spróbujesz. Jak dobrze integrują się ze wszystkim innym w twoim alokatorze, ponieważ alokator robi wiele rzeczy i niektóre pomysły nie będą działać w twoim konkretnym alokatorze, ale w innym alokatorze będą z łatwością. Głównym sposobem na wygraną dla alokatora jest wyciągnięcie powolnych rzeczy poza główną ścieżkę i zmuszenie ich do rozdzielenia się wzdłuż granic wolnych ścieżek. Więc jeśli chcesz uruchomić GC, pójść powolną ścieżką, dokonać deoptymalizacji, zgłosić wyjątek i tak dalej – wiesz, że te rzeczy są stosunkowo rzadkie. A są naprawdę rzadkie, sprawdzałem. Wykonujesz dodatkową pracę, co usuwa wiele ograniczeń na tych wolnych ścieżkach, ale tak naprawdę nie ma to znaczenia, ponieważ są one wolne i rzadko podróżowane. Na przykład wskaźnik zerowy - to się nigdy nie zdarza, prawda? Musisz mieć kilka ścieżek do różnych rzeczy, ale nie powinny one kolidować z główną. 

Władimir: Co sądzisz o wielordzeniowych, gdy jednocześnie działają tysiące rdzeni? Czy to przydatna rzecz?

Klif: Sukces procesora graficznego pokazuje, że jest on całkiem przydatny!

Władimir: Są dość wyspecjalizowani. A co z procesorami ogólnego przeznaczenia?

Klif: Cóż, taki był model biznesowy Azula. Odpowiedź pojawiła się w czasach, gdy ludzie naprawdę kochali przewidywalną wydajność. Pisanie kodu równoległego było wówczas trudne. Model kodowania H2O jest wysoce skalowalny, ale nie jest to model ogólnego przeznaczenia. Być może trochę bardziej ogólnie niż w przypadku korzystania z procesora graficznego. Czy mówimy o złożoności opracowania takiej rzeczy, czy o złożoności jej użycia? Na przykład Azul dał mi ciekawą lekcję, dość nieoczywistą: małe skrytki są normalne. 

Największe wyzwanie w życiu

Władimir: A co z wyzwaniami nietechnicznymi?

Klif: Największym wyzwaniem nie było bycie... miłym i miłym dla ludzi. 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 ruszyć dalej z tymi problemami i nie mogłem sobie z nimi poradzić. W ten sposób powstało wiele długotrwałych problemów, trwających dziesięciolecia. Bezpośrednią konsekwencją tego jest fakt, że Java ma kompilatory C1 i C2. Bezpośrednią konsekwencją jest także brak kompilacji wielopoziomowej w Javie przez dziesięć lat z rzędu. Jest oczywiste, ż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, dawno temu, kiedy zaczynałam pracę w Sun, byłam… No dobrze, nie tylko wtedy, ale generalnie zawsze mam swoje zdanie na każdy temat. I pomyślałam, że to prawda, że ​​możesz po prostu przyjąć tę swoją prawdę i powiedzieć ją wprost. Zwłaszcza, że ​​przez większość czasu miałem szokująco rację. A jeśli nie podoba Ci się takie podejście… szczególnie jeśli ewidentnie się mylisz i robisz bzdury… Generalnie niewiele osób toleruje taką formę komunikacji. Chociaż niektórzy mogliby, tak jak ja. Całe swoje życie zbudowałem na zasadach merytokratycznych. Jeśli wskażesz mi coś złego, natychmiast się odwrócę i powiem: powiedziałeś bzdury. Jednocześnie oczywiście przepraszam i tak dalej, zwrócę uwagę na zalety, jeśli takie istnieją, i podejmę inne właściwe działania. Z drugiej strony mam szokująco rację co do szokująco dużego odsetka całkowitego czasu. I nie sprawdza się to zbyt dobrze w relacjach z ludźmi. Nie chcę być miły, ale zadaję pytanie bez ogródek. „To nigdy nie zadziała, ponieważ raz, dwa i trzy”. A oni na to: „Och!” Były też inne konsekwencje, które prawdopodobnie lepiej było zignorować: na przykład te, które doprowadziły do ​​rozwodu z żoną, a potem dziesięciu lat depresji.

Wyzwanie to walka z ludźmi, z ich postrzeganiem tego, co można, a czego nie można zrobić, co jest ważne, a co nie. Było wiele wyzwań związanych ze stylem kodowania. Nadal piszę dużo kodu i w tamtych czasach musiałem nawet zwolnić, bo wykonywałem zbyt wiele równoległych zadań i robiłem je słabo, zamiast skupić się na jednym. Patrząc wstecz, napisałem połowę kodu polecenia Java JIT, polecenia C2. Następny najszybszy programista pisał o połowę wolniej, kolejny o połowę wolniej, a spadek był wykładniczy. Siódma osoba w tym rzędzie była bardzo, bardzo powolna – to zawsze się zdarza! Dotknąłem dużo kodu. Przyjrzałem się, kto i co napisał, bez wyjątku, wpatrywałem się w ich kod, przeglądałem każdy z nich i nadal pisałem więcej sam niż którykolwiek z nich. To podejście nie sprawdza się zbyt dobrze w przypadku ludzi. Niektórym się to nie podoba. A kiedy nie mogą sobie z tym poradzić, zaczynają się wszelkiego rodzaju skargi. Na przykład kiedyś powiedziano mi, żebym przestał kodować, ponieważ piszę za dużo kodu i zagraża to zespołowi, ale dla mnie to wszystko brzmiało jak żart: stary, jeśli reszta zespołu zniknie, a ja będę dalej pisać kod, ty stracimy tylko połowę drużyn. Z drugiej strony, jeśli będę dalej pisać kod, a ty stracisz połowę zespołu, będzie to wyglądało na bardzo złe zarządzanie. Nigdy tak naprawdę o tym nie myślałem, nigdy o tym nie mówiłem, ale wciąż było to gdzieś w mojej głowie. Z tyłu głowy krążyła mi myśl: „Żartujesz?” Zatem największym problemem byłem ja i moje relacje z ludźmi. Teraz rozumiem siebie znacznie lepiej, przez długi czas byłem liderem zespołu programistów, a teraz mówię ludziom wprost: wiesz, jestem, jaki jestem i będziesz musiał sobie ze mną radzić - czy będzie w porządku, jeśli stanę Tutaj? A kiedy zaczęli sobie z tym radzić, wszystko zadziałało. Tak naprawdę nie jestem ani zły, ani dobry, nie mam złych zamiarów ani egoistycznych dążeń, to po prostu moja esencja i trzeba z tym jakoś żyć.

Andrew: Niedawno wszyscy zaczęli mówić o samoświadomości dla introwertyków i ogólnie o umiejętnościach miękkich. Co możesz o tym powiedzieć?

Klif: Tak, to był wniosek i lekcja, jaką wyciągnąłem z rozwodu z żoną. To, czego nauczyłem się po rozwodzie, to zrozumienie siebie. W ten sposób zacząłem rozumieć innych ludzi. Zrozum, jak działa ta interakcja. Prowadziło to do kolejnych odkryć. Była świadomość tego, kim jestem i co reprezentuję. Co robię: albo jestem zajęty zadaniem, albo unikam konfliktu, albo robię coś innego – a ten poziom samoświadomości naprawdę pomaga zachować kontrolę. Potem wszystko idzie dużo łatwiej. Jedną rzeczą, którą odkryłem nie tylko u siebie, ale także u innych programistów, jest niemożność zwerbalizowania myśli, gdy jesteś w stanie stresu emocjonalnego. Na przykład siedzisz i kodujesz, w stanie flow, a potem oni przybiegają do ciebie i zaczynają histerycznie krzyczeć, że coś jest zepsute i teraz zostaną podjęte przeciwko tobie ekstremalne środki. I nie możesz powiedzieć ani słowa, ponieważ jesteś w stanie stresu emocjonalnego. Zdobyta wiedza pozwala przygotować się na ten moment, przetrwać go i przejść do planu odosobnienia, po którym można już coś zrobić. Więc tak, kiedy zaczynasz zdawać sobie sprawę, jak to wszystko działa, jest to ogromne wydarzenie zmieniające życie. 
Sam nie mogłem znaleźć odpowiednich słów, ale zapamiętałem sekwencję działań. Rzecz w tym, że ta reakcja jest w równym stopniu fizyczna, co werbalna i potrzebna jest przestrzeń. Taka przestrzeń w sensie zen. To jest dokładnie to, co należy wyjaśnić, a następnie natychmiast odsunąć się na bok – czysto fizycznie. Kiedy milczę werbalnie, potrafię przetworzyć sytuację emocjonalnie. Gdy adrenalina dociera do Twojego mózgu, przełącza Cię w tryb walki lub ucieczki, nie możesz już nic powiedzieć, nie - teraz jesteś idiotą, inżynierem biczowania, niezdolnym do przyzwoitej reakcji ani nawet zatrzymania ataku, a atakujący jest wolny atakować raz po raz. Najpierw musisz na nowo stać się sobą, odzyskać kontrolę, wyjść z trybu „walcz lub uciekaj”.

A do tego potrzebujemy przestrzeni werbalnej. Tylko wolne miejsce. Jeśli w ogóle coś powiesz, możesz powiedzieć dokładnie to, a potem iść i naprawdę znaleźć „przestrzeń” dla siebie: iść na spacer do parku, zamknąć się pod prysznicem – to nie ma znaczenia. Najważniejsze jest tymczasowe odłączenie się od tej sytuacji. Gdy tylko wyłączysz się chociaż na kilka sekund, kontrola wraca, zaczynasz trzeźwo myśleć. „OK, nie jestem jakimś idiotą, nie robię głupich rzeczy, jestem całkiem użyteczną osobą”. Kiedy już będziesz w stanie się przekonać, czas przejść do następnego etapu: zrozumienia, co się stało. Zostałeś zaatakowany. Atak nastąpił z nieoczekiwanej strony. To była nieuczciwa, podła zasadzka. To jest złe. Następnym krokiem jest zrozumienie, dlaczego atakujący tego potrzebował. Naprawdę? Dlaczego? Może dlatego, że on sam jest wściekły? Dlaczego jest zły? Na przykład dlatego, że sam schrzanił sprawę i nie może wziąć na siebie odpowiedzialności? W ten sposób należy ostrożnie podejść do całej sytuacji. Ale to wymaga pola manewru, przestrzeni werbalnej. Pierwszym krokiem jest zerwanie kontaktu werbalnego. Unikaj dyskusji słowami. Anuluj to, odejdź tak szybko, jak to możliwe. Jeśli jest to rozmowa telefoniczna, po prostu się rozłącz – jest to umiejętność, której nauczyłem się podczas komunikacji z moją byłą żoną. Jeśli rozmowa nie idzie w dobrym kierunku, po prostu powiedz „do widzenia” i rozłącz się. Z drugiej strony telefonu: „bla bla bla”, ty odpowiadasz: „tak, pa!” i rozłącz się. Po prostu zakończ rozmowę. Pięć minut później, kiedy wróci do Ciebie zdolność rozsądnego myślenia, trochę ochłoniesz, możliwe stanie się myślenie o wszystkim, o tym, co się wydarzyło i co będzie dalej. I zacznij formułować przemyślaną odpowiedź, a nie tylko reagować pod wpływem emocji. Dla mnie przełomem w samoświadomości był właśnie fakt, że w przypadku stresu emocjonalnego nie mogę mówić. Wyjście z tego stanu, przemyślenie i zaplanowanie, jak zareagować i zrekompensować problemy – to właściwe kroki w przypadku, gdy nie możesz mówić. Najłatwiej jest uciec od sytuacji, w której objawia się stres emocjonalny i po prostu przestać uczestniczyć w tym stresie. Potem staniesz się zdolny do myślenia, kiedy będziesz mógł myśleć, staniesz się zdolny do mówienia i tak dalej.

Nawiasem mówiąc, w sądzie przeciwny prawnik próbuje ci to zrobić - teraz jest jasne, dlaczego. Bo ma zdolność stłumienia Cię do takiego stanu, że nie będziesz mógł nawet wymówić swojego imienia np. W bardzo realnym sensie nie będziesz mógł mówić. Jeśli przydarzy Ci się taka sytuacja i wiesz, że znajdziesz się w miejscu, w którym toczą się słowne potyczki, w miejscu takim jak sąd, to możesz przyjechać ze swoim prawnikiem. Prawnik stanie w Twojej obronie i zaprzestanie ataku słownego, i zrobi to w sposób całkowicie legalny, a utracona przestrzeń Zen wróci do Ciebie. Na przykład musiałem kilka razy zadzwonić do rodziny, sędzia był w tej sprawie dość przyjazny, ale prawnik strony przeciwnej krzyczał na mnie i krzyczał, nie mogłem nawet dojść do słowa. W takich przypadkach najlepszym rozwiązaniem dla mnie jest skorzystanie z pomocy mediatora. Mediator zatrzymuje całą tę presję, która na ciebie spada nieprzerwanym strumieniem, znajdujesz niezbędną przestrzeń Zen, a wraz z nią powraca zdolność mówienia. To cała dziedzina wiedzy, w której jest wiele do przestudiowania, wiele do odkrycia w sobie, a wszystko to przekłada się na strategiczne decyzje wysokiego szczebla, które są różne dla różnych ludzi. Niektórzy ludzie nie mają opisanych powyżej problemów; zazwyczaj nie mają ich ludzie, którzy są zawodowymi sprzedawcami. Wszyscy ci ludzie, którzy żyją ze słów – znani piosenkarze, poeci, przywódcy religijni i politycy – oni zawsze mają coś do powiedzenia. Oni nie mają takich problemów, ale ja tak.

Andrew: To było... nieoczekiwane. Świetnie, dużo już rozmawialiśmy i czas zakończyć ten wywiad. Na pewno spotkamy się na konferencji i będziemy mogli kontynuować ten dialog. Do zobaczenia na Hydrze!

Rozmowę z Cliffem będziesz mógł kontynuować na konferencji Hydra 2019, która odbędzie się w dniach 11-12 lipca 2019 w St. Petersburgu. Przyjdzie z raportem „Doświadczenia ze sprzętową pamięcią transakcyjną Azul”. Bilety można kupić na oficjalnej stronie internetowej.

Źródło: www.habr.com

Dodaj komentarz