Страхотно интервю с Cliff Click, бащата на JIT компилацията в Java

Страхотно интервю с Cliff Click, бащата на JIT компилацията в JavaCliff Click — Технически директор на Cratus (IoT сензори за подобряване на процеси), основател и съосновател на няколко стартиращи компании (включително Rocket Realtime School, Neurensic и H2O.ai) с няколко успешни излизания. Клиф написа първия си компилатор на 15 години (Паскал за TRS Z-80)! Той е най-известен с работата си върху C2 в Java (The Sea of ​​​​Nodes IR). Този компилатор показа на света, че JIT може да произвежда висококачествен код, което беше един от факторите за появата на Java като една от основните съвременни софтуерни платформи. Тогава Клиф помогна на Azul Systems да изгради 864-ядрен мейнфрейм с чист Java софтуер, който поддържа GC паузи на 500-гигабайтова купчина в рамките на 10 милисекунди. Като цяло Клиф успя да работи по всички аспекти на JVM.

 
Този хабрапост е страхотно интервю с Клиф. Ще говорим по следните теми:

  • Преход към оптимизации на ниско ниво
  • Как да направите голям рефакторинг
  • Разходен модел
  • Обучение за оптимизация на ниско ниво
  • Практически примери за подобряване на производителността
  • Защо да създавате свой собствен език за програмиране
  • Кариера на инженер по производителност
  • Технически предизвикателства
  • Малко за разпределението на регистъра и многоядрените
  • Най-голямото предизвикателство в живота

Интервютата се провеждат от:

  • Андрей Сатарин от Amazon Web Services. В кариерата си той успя да работи в напълно различни проекти: тества разпределената база данни NewSQL в Yandex, система за откриване на облаци в Kaspersky Lab, мултиплейър игра в Mail.ru и услуга за изчисляване на валутните цени в Deutsche Bank. Интересува се от тестване на широкомащабни бекенд и разпределени системи.
  • Владимир Ситников от Netcracker. Десет години работа върху производителността и скалируемостта на NetCracker OS, софтуер, използван от телекомуникационните оператори за автоматизиране на процесите на управление на мрежата и мрежовото оборудване. Интересуват се от проблеми с производителността на Java и Oracle Database. Автор на повече от дузина подобрения на производителността в официалния JDBC драйвер на PostgreSQL.

Преход към оптимизации на ниско ниво

Андрю: Вие сте голямо име в света на JIT компилирането, Java и работата с производителността като цяло, нали? 

Клиф: Така е!

Андрю: Нека започнем с някои общи въпроси относно изпълнението. Какво мислите за избора между оптимизации на високо и ниско ниво, като работа на ниво процесор?

Клиф: Да, тук всичко е просто. Най-бързият код е този, който никога не се изпълнява. Затова винаги трябва да започнете от високо ниво, да работите върху алгоритми. По-добрата О нотация ще победи по-лошата О нотация, освен ако не се намесят някои достатъчно големи константи. Нещата от ниско ниво остават последни. Обикновено, ако сте оптимизирали останалата част от стека си достатъчно добре и все още има останали интересни неща, това е ниско ниво. Но как да започнем от високо ниво? Откъде знаете, че е свършена достатъчно работа на високо ниво? Ами... няма начин. Няма готови рецепти. Трябва да разберете проблема, да решите какво ще правите (за да не предприемате ненужни стъпки в бъдеще) и тогава можете да разкриете профайлъра, който може да каже нещо полезно. В един момент вие сами осъзнавате, че сте се отървали от ненужните неща и е време да направите някои фини настройки на ниско ниво. Това определено е особен вид изкуство. Има много хора, които правят ненужни неща, но се движат толкова бързо, че нямат време да се тревожат за производителността. Но това е докато въпросът не изникне направо. Обикновено в 99% от времето на никой не му пука какво правя, до момента, в който на критичния път се появи важно нещо, което не го интересува. И тук всички започват да ви натякват „защо не работи перфектно от самото начало“. Като цяло винаги има какво да се подобри в производителността. Но 99% от времето нямате потенциални клиенти! Просто се опитвате да накарате нещо да работи и в процеса разбирате кое е важно. Никога не можете да знаете предварително, че това парче трябва да бъде перфектно, така че всъщност трябва да сте перфектни във всичко. Но това е невъзможно и вие не го правите. Винаги има много неща за оправяне – и това е напълно нормално.

Как да направите голям рефакторинг

Андрю: Как работите върху представление? Това е междусекторен проблем. Например, трябвало ли ви е някога да работите върху проблеми, които възникват от пресичането на много съществуващи функционалности?

Клиф: Опитвам се да го избягвам. Ако знам, че производителността ще бъде проблем, мисля за това, преди да започна да кодирам, особено със структури от данни. Но често откривате всичко това много по-късно. И тогава трябва да отидете до крайни мерки и да направите това, което аз наричам „пренаписване и завладяване“: трябва да вземете достатъчно голямо парче. Част от кода все пак ще трябва да бъде пренаписан поради проблеми с производителността или нещо друго. Каквато и да е причината за пренаписване на код, почти винаги е по-добре да се пренапише по-голямо парче, отколкото по-малко. В този момент всички започват да треперят от страх: „Боже мой, не можете да пипате толкова много код!“ Но всъщност този подход почти винаги работи много по-добре. Трябва незабавно да се заемете с голям проблем, да нарисувате голям кръг около него и да кажете: Ще пренапиша всичко вътре в кръга. Рамката е много по-малка от съдържанието вътре в нея, което трябва да бъде заменено. И ако такова очертаване на границите ви позволява да вършите работата вътре перфектно, ръцете ви са свободни, правете каквото искате. След като разберете проблема, процесът на пренаписване е много по-лесен, така че вземете голяма хапка!
В същото време, когато направите голямо пренаписване и разберете, че производителността ще бъде проблем, можете веднага да започнете да се тревожите за това. Това обикновено се превръща в прости неща като „не копирайте данни, управлявайте данните възможно най-просто, направете ги малки“. При големи пренаписвания има стандартни начини за подобряване на производителността. И те почти винаги се въртят около данни.

Разходен модел

Андрю: В един от подкастите говорихте за модели на разходите в контекста на производителността. Можете ли да обясните какво имате предвид с това?

Клиф: Разбира се. Роден съм в епоха, когато производителността на процесора беше изключително важна. И тази епоха се завръща отново - съдбата не е без ирония. Започнах да живея в дните на осембитовите машини; първият ми компютър работеше с 256 байта. Точно байтове. Всичко беше много малко. Инструкциите трябваше да бъдат преброени и когато започнахме да се придвижваме нагоре в стека на езиците за програмиране, езиците поеха все повече и повече. Имаше Assembler, след това Basic, след това C и C се погрижи за много подробности, като разпределение на регистри и избор на инструкции. Но там всичко беше съвсем ясно и ако направя указател към екземпляр на променлива, тогава ще получа натоварване и цената на тази инструкция е известна. Хардуерът произвежда определен брой машинни цикли, така че скоростта на изпълнение на различни неща може да се изчисли просто чрез сумиране на всички инструкции, които ще изпълните. Всяко сравнение/тест/клон/обаждане/зареждане/съхранение може да се сумира и да се каже: това е времето за изпълнение за вас. Когато работите върху подобряването на производителността, определено ще обърнете внимание на това кои числа съответстват на малките горещи цикли. 
Но веднага щом преминете към Java, Python и подобни неща, много бързо се отдалечавате от хардуера на ниско ниво. Каква е цената за извикване на гетер в Java? Ако JIT в HotSpot е правилен вграден, ще се зареди, но ако не е направил това, ще бъде извикване на функция. Тъй като повикването е на горещ цикъл, то ще замени всички други оптимизации в този цикъл. Следователно реалната цена ще бъде много по-висока. И веднага губите способността да погледнете част от кода и да разберете, че трябва да го изпълним по отношение на тактовата честота на процесора, използваната памет и кеш. Всичко това става интересно само ако наистина влезеш в представлението.
Сега се намираме в ситуация, в която скоростта на процесора почти не се е увеличила от десетилетие. Старите времена се върнаха! Вече не можете да разчитате на добра еднонишкова производителност. Но ако внезапно попаднете в паралелни изчисления, това е невероятно трудно, всички ви гледат като Джеймс Бонд. Десеткратно ускорение тук обикновено се получава на места, където някой е объркал нещо. Паралелността изисква много работа. За да получите това XNUMXx ускорение, трябва да разберете модела на разходите. Какво и колко струва? И за да направите това, трябва да разберете как езикът пасва на основния хардуер.
Мартин Томпсън избра страхотна дума за своя блог Механична симпатия! Трябва да разберете какво ще прави хардуерът, как точно ще го прави и защо прави това, което прави на първо място. Използвайки това, е доста лесно да започнете да броите инструкциите и да разберете къде отива времето за изпълнение. Ако нямате подходящо обучение, просто търсите черна котка в тъмна стая. Виждам хора, оптимизиращи производителността през цялото време, които нямат представа какво, по дяволите, правят. Те страдат много и не напредват много. И когато взема същата част от кода, вмъквам няколко малки хака и получавам пет- или десеткратно ускоряване, те казват: добре, това не е честно, вече знаехме, че си по-добър. невероятно За какво говоря... моделът на разходите е за това какъв код пишете и колко бързо работи средно в голямата картина.

Андрю: И как можеш да поддържаш такъв обем в главата си? Това с повече опит ли се постига или? Откъде такъв опит?

Клиф: Е, не получих опита си по най-лесния начин. Програмирах в Асамблея в дните, когато можехте да разберете всяка една инструкция. Звучи глупаво, но оттогава наборът от инструкции на Z80 винаги е останал в главата ми, в паметта ми. Не помня имената на хората в рамките на минута разговор, но си спомням код, написан преди 40 години. Смешно е, изглежда като синдром"идиот учен".

Обучение за оптимизация на ниско ниво

Андрю: Има ли по-лесен начин да влезете?

Клиф: Да и не. Хардуерът, който всички ние използваме, не се е променил много с времето. Всички използват x86, с изключение на смартфоните Arm. Ако не правите някакво хардкор вграждане, вие правите същото. Добре, следващият. Инструкциите също не са се променили от векове. Трябва да отидете и да напишете нещо в събранието. Не много, но достатъчно, за да започнем да разбираме. Ти се усмихваш, но аз говоря напълно сериозно. Трябва да разберете съответствието между език и хардуер. След това трябва да отидете и да напишете малко и да направите малък компилатор за играчки за малък език за играчки. Подобно на играчка означава, че трябва да бъде направено за разумен период от време. Може да е супер просто, но трябва да генерира инструкции. Актът на генериране на инструкция ще ви помогне да разберете модела на разходите за моста между кода на високо ниво, който всеки пише, и машинния код, който работи на хардуера. Тази кореспонденция ще бъде изгорена в мозъка по време на писане на компилатора. Дори най-простият компилатор. След това можете да започнете да разглеждате Java и факта, че нейната семантична пропаст е много по-дълбока и е много по-трудно да се изграждат мостове над нея. В Java е много по-трудно да разберем дали нашият мост се е оказал добър или лош, какво ще го накара да се разпадне и какво не. Но имате нужда от някаква начална точка, където да погледнете кода и да разберете: „да, този инструмент за получаване трябва да бъде вграден всеки път.“ И тогава се оказва, че понякога това се случва, с изключение на ситуацията, когато методът стане твърде голям и JIT започва да вгражда всичко. Ефективността на такива места може да се предвиди незабавно. Обикновено гетерите работят добре, но след това гледате големи горещи цикли и осъзнавате, че има някои извиквания на функции, които се носят наоколо, които не знаят какво правят. Това е проблемът с широкото използване на гетъри, причината да не са вградени е, че не е ясно дали са гетър. Ако имате супер малка кодова база, можете просто да я запомните и след това да кажете: това е геттер, а това е сетер. В голяма кодова база всяка функция живее своя собствена история, която като цяло не е известна на никого. Профайлърът казва, че сме загубили 24% от времето на някакъв цикъл и за да разберем какво прави този цикъл, трябва да разгледаме всяка функция вътре. Невъзможно е да се разбере това без да се изучава функцията и това сериозно забавя процеса на разбиране. Ето защо не използвам гетери и сетери, достигнах ново ниво!
Къде да взема модела на разходите? Е, можете да прочетете нещо, разбира се... Но според мен най-добрият начин е да действате. Създаването на малък компилатор ще бъде най-добрият начин да разберете модела на разходите и да го поставите в собствената си глава. Малък компилатор, който би бил подходящ за програмиране на микровълнова печка, е задача за начинаещ. Е, искам да кажа, че ако вече имате умения по програмиране, това трябва да е достатъчно. Всички тези неща като анализиране на низ, който имате като някакъв вид алгебричен израз, извличане на инструкции за математически операции от там в правилния ред, вземане на правилните стойности от регистрите - всичко това се прави наведнъж. И докато го правите, то ще се запечата в мозъка ви. Мисля, че всеки знае какво прави компилаторът. И това ще даде разбиране за модела на разходите.

Практически примери за подобряване на производителността

Андрю: На какво друго трябва да обърнете внимание, когато работите върху производителността?

Клиф: Структури от данни. Между другото, да, не съм преподавал тези класове от дълго време... Ракетно училище. Беше забавно, но изискваше много усилия, а и имам живот! ДОБРЕ. И така, в един от големите и интересни часове, „Къде отива вашето представяне“, дадох на учениците пример: два и половина гигабайта финтех данни бяха прочетени от CSV файл и след това те трябваше да изчислят броя на продадените продукти . Редовни тикови пазарни данни. UDP пакетите се преобразуват в текстов формат от 70-те години. Чикагската търговска борса - всякакви неща като масло, царевица, соя и подобни неща. Беше необходимо да се преброят тези продукти, броят на транзакциите, средният обем на движение на средства и стоки и т.н. Това е доста проста търговска математика: намерете кода на продукта (това е 1-2 знака в хеш-таблицата), вземете сумата, добавете я към един от наборите за търговия, добавете обем, добавете стойност и няколко други неща. Много проста математика. Реализацията на играчката беше много ясна: всичко е във файл, аз чета файла и се движа през него, разделям отделни записи на Java низове, търся необходимите неща в тях и ги събирам според математиката, описана по-горе. И работи на някаква ниска скорост.

С този подход е очевидно какво се случва и паралелното изчисление няма да помогне, нали? Оказва се, че петкратно увеличение на производителността може да се постигне просто чрез избор на правилните структури от данни. И това изненадва дори опитни програмисти! В моя конкретен случай трикът беше, че не трябва да правите разпределения на паметта в горещ цикъл. Е, това не е цялата истина, но като цяло - не трябва да подчертавате „веднъж в X“, когато X е достатъчно голям. Когато X е два и половина гигабайта, не трябва да разпределяте нищо „веднъж на буква“, или „веднъж на ред“, или „веднъж на поле“, нещо подобно. Това е мястото, където се прекарва времето. Как изобщо работи това? Представете си, че се обаждам String.split() или BufferedReader.readLine(). Readline прави низ от набор от байтове, дошли по мрежата, веднъж за всеки ред, за всеки от стотиците милиони редове. Взимам този ред, анализирам го и го изхвърлям. Защо го изхвърлям - добре, вече го обработих, това е всичко. Така че за всеки байт, прочетен от тези 2.7G, два знака ще бъдат записани в реда, тоест вече 5.4G, и не ми трябват за нищо повече, така че те се изхвърлят. Ако погледнете честотната лента на паметта, ние зареждаме 2.7G, който минава през паметта и шината на паметта в процесора, а след това два пъти повече се изпраща към линията, която лежи в паметта, и всичко това се разтрива, когато се създава всяка нова линия. Но трябва да го прочета, хардуерът го чете, дори ако всичко е протрито по-късно. И трябва да го запиша, защото създадох ред и кешовете са пълни - кешът не може да побере 2.7G. И така, за всеки байт, който чета, чета още два байта и записвам още два байта и накрая те имат съотношение 4:1 - в това съотношение губим честотна лента на паметта. И тогава се оказва, че ако го направя String.split() – това не е последният път, когато го правя, може да има още 6-7 полета вътре. Така че класическият код за четене на CSV и след това анализиране на низовете води до загуба на честотна лента на паметта от около 14:1 спрямо това, което всъщност бихте искали да имате. Ако изхвърлите тези селекции, можете да получите петкратно ускорение.

И не е толкова трудно. Ако погледнете кода от правилния ъгъл, всичко става доста просто, след като осъзнаете проблема. Не трябва да спирате да разпределяте памет напълно: единственият проблем е, че разпределяте нещо и то веднага умира, като по пътя изгаря важен ресурс, който в този случай е честотната лента на паметта. И всичко това води до спад в производителността. На x86 обикновено трябва активно да записвате цикли на процесора, но тук сте изгорили цялата памет много по-рано. Решението е да се намали количеството на изхвърлянето. 
Другата част от проблема е, че ако стартирате профайлъра, когато лентата на паметта свърши, точно когато това се случи, обикновено чакате кеша да се върне, защото е пълен с боклук, който току-що сте създали, всички тези редове. Следователно всяка операция за зареждане или съхраняване става бавна, защото те водят до пропуски в кеша - целият кеш е станал бавен, чакайки боклука да го напусне. Следователно профилиращият просто ще покаже топъл произволен шум, размазан в целия цикъл - няма да има отделна гореща инструкция или място в кода. Само шум. И ако погледнете циклите на GC, всички те са младо поколение и са супер бързи - микросекунди или милисекунди максимум. В края на краищата цялата тази памет умира моментално. Разпределяте милиарди гигабайти, а той ги реже, и ги реже, и пак ги реже. Всичко това става много бързо. Оказва се, че има евтини GC цикли, топъл шум по време на целия цикъл, но ние искаме да получим 5x ускорение. В този момент нещо трябва да се затвори в главата ви и да прозвучи: "защо е това?!" Препълването на лентата на паметта не се показва в класическия дебъгер; трябва да стартирате дебъгера за брояч на производителността на хардуера и да го видите сами и директно. Но това не може да се подозира директно от тези три симптома. Третият симптом е, когато погледнете какво подчертавате, попитайте профилиращия и той отговаря: „Направихте милиард редове, но GC работи безплатно.“ Веднага щом това се случи, осъзнавате, че сте създали твърде много обекти и сте изгорили цялата лента на паметта. Има начин да разберем това, но не е очевидно. 

Проблемът е в структурата на данните: голата структура, която е в основата на всичко, което се случва, е твърде голяма, има 2.7 G на диска, така че правенето на копие на това нещо е много нежелателно - искате да го заредите от мрежовия байтов буфер веднага в регистрите, за да не четете и пишете в реда напред и назад пет пъти. За съжаление, Java не ви дава такава библиотека като част от JDK по подразбиране. Но това е тривиално, нали? По същество това са 5-10 реда код, които ще бъдат използвани за внедряване на ваш собствен буфериран низ за зареждане, който повтаря поведението на низовия клас, като същевременно е обвивка около основния байтов буфер. В резултат на това се оказва, че работите почти като с низове, но всъщност указателите към буфера се движат там и необработените байтове не се копират никъде и по този начин същите буфери се използват отново и отново и операционната система е щастлива да поеме върху себе си нещата, за които е предназначена, като скрито двойно буфериране на тези байтови буфери, и вече не се занимавате с безкраен поток от ненужни данни. Между другото, разбирате ли, че при работа с GC е гарантирано, че всяко разпределение на паметта няма да бъде видимо за процесора след последния цикъл на GC? Следователно всичко това не може да бъде в кеша и тогава се получава 100% гарантиран пропуск. Когато работите с указател, на x86, изваждането на регистър от паметта отнема 1-2 такта и веднага щом това се случи, плащате, плащате, плащате, защото паметта е включена ДЕВЕТ кеша – и това е цената на разпределението на паметта. Реална стойност.

С други думи, структурите от данни са най-трудното нещо за промяна. И след като осъзнаете, че сте избрали грешната структура на данните, която ще убие производителността по-късно, обикновено има много работа за вършене, но ако не го направите, нещата ще се влошат. На първо място, трябва да помислите за структурите от данни, това е важно. Основната цена тук пада върху мастните структури от данни, които започват да се използват в стила на „Копирах структура от данни X в структура от данни Y, защото харесвам формата на Y повече.“ Но операцията по копиране (която изглежда евтина) всъщност губи честотна лента на паметта и там е заровено цялото загубено време за изпълнение. Ако имам огромен низ от JSON и искам да го превърна в структурирано DOM дърво на POJO или нещо подобно, операцията по анализиране на този низ и изграждане на POJO и след това достъп до POJO отново по-късно ще доведе до ненужни разходи - това е не е евтино. Освен ако бягате около POJO много по-често, отколкото бягате около низ. Вместо това можете да опитате да дешифрирате низа и да извлечете само това, от което се нуждаете, без да го превръщате в POJO. Ако всичко това се случи на път, от който се изисква максимална производителност, няма POJO за вас, трябва по някакъв начин да се заровите директно в линията.

Защо да създавате свой собствен език за програмиране

Андрю: Казахте, че за да разберете модела на разходите, трябва да напишете свой собствен малък език...

Клиф: Не е език, а компилатор. Език и компилатор са две различни неща. Най-важната разлика е в главата ви. 

Андрю: Между другото, доколкото знам, вие експериментирате със създаването на свои собствени езици. За какво?

Клиф: Защото мога! Аз съм полупенсионер, така че това е моето хоби. През целия си живот внедрявам езици на други хора. Също така работих много върху моя стил на кодиране. А също и защото виждам проблеми в други езици. Виждам, че има по-добри начини да правите познати неща. И аз бих ги използвал. Просто съм уморен да виждам проблеми в себе си, в Java, в Python, във всеки друг език. Сега пиша в React Native, JavaScript и Elm като хоби, което не е за пенсиониране, а за активна работа. Аз също пиша на Python и най-вероятно ще продължа да работя върху машинното обучение за бекенд на Java. Има много популярни езици и всички те имат интересни функции. Всеки е добър по свой начин и можете да опитате да обедините всички тези характеристики. И така, изучавам неща, които ме интересуват, поведението на езика, опитвайки се да измисля разумна семантика. И досега успявам! В момента се боря със семантиката на паметта, защото искам да я имам като в C и Java и да получа силен модел на памет и семантика на паметта за зареждания и съхранявания. В същото време имайте автоматично извеждане на типа като в Haskell. Тук се опитвам да смеся Haskell-подобен тип извод с работа с памет както в C, така и в Java. Това правя аз в последните 2-3 месеца например.

Андрю: Ако създадете език, който взема по-добри аспекти от други езици, мислите ли, че някой ще направи обратното: ще вземе вашите идеи и ще ги използва?

Клиф: Точно така се появяват новите езици! Защо Java е подобна на C? Тъй като C имаше добър синтаксис, който всички разбираха, и Java беше вдъхновена от този синтаксис, добавяйки безопасност на типа, проверка на границите на масива, GC, и те също подобриха някои неща от C. Те добавиха свои собствени. Но те бяха вдъхновени доста, нали? Всеки стои на раменете на гигантите, които са дошли преди вас - така се постига прогрес.

Андрю: Доколкото разбирам, вашият език ще бъде безопасен за паметта. Мислили ли сте да внедрите нещо като проверка на заеми от Rust? Гледала ли си го, какво мислиш за него?

Клиф: Е, аз пиша C от векове, с целия този malloc и безплатно, и ръчно управлявам живота. Знаете ли, 90-95% от ръчно контролирания живот има същата структура. И е много, много болезнено да го правите ръчно. Бих искал компилаторът просто да ви каже какво се случва там и какво сте постигнали с вашите действия. За някои неща Borrow Checker прави това веднага. И трябва автоматично да показва информация, да разбира всичко и дори да не ме натоварва с представянето на това разбиране. Той трябва да направи поне локален escape анализ и само ако не успее, тогава трябва да добави анотации за тип, които ще опишат продължителността на живота - и такава схема е много по-сложна от проверка на заемане или всъщност всяка съществуваща проверка на паметта. Изборът между „всичко е наред“ и „нищо не разбирам“ - не, трябва да има нещо по-добро. 
И така, като човек, който е написал много код на C, мисля, че поддържането на автоматичен контрол на живота е най-важното нещо. Освен това ми писна от това колко Java използва памет и основното оплакване е GC. Когато разпределяте памет в Java, няма да си върнете паметта, която е била локална при последния GC цикъл. Това не е така в езиците с по-прецизно управление на паметта. Ако извикате malloc, веднага получавате паметта, която обикновено току-що е била използвана. Обикновено правите някои временни неща с паметта и веднага я връщате обратно. И веднага се връща в пула malloc и следващият цикъл malloc го изважда отново. Следователно действителното използване на паметта се свежда до набора от живи обекти в даден момент плюс течове. И ако всичко не изтече по напълно неприличен начин, по-голямата част от паметта се озовава в кеш паметта и процесора и работи бързо. Но изисква много ръчно управление на паметта с malloc и free, извикани в правилния ред, на правилното място. Rust може да се справи с това правилно сам и в много случаи дава дори по-добра производителност, тъй като потреблението на памет е стеснено само до текущото изчисление - за разлика от изчакването на следващия GC цикъл за освобождаване на памет. В резултат на това получихме много интересен начин за подобряване на производителността. И доста мощен - искам да кажа, че направих такива неща, когато обработвах данни за fintech, и това ми позволи да ускоря около пет пъти. Това е доста голям тласък, особено в свят, в който процесорите не стават по-бързи и все още чакаме подобрения.

Кариера на инженер по производителност

Андрю: Бих искал също да разпитам за кариерите като цяло. Издигнахте се до известност с работата си JIT в HotSpot и след това се преместихте в Azul, която също е JVM компания. Но вече работехме повече върху хардуера, отколкото върху софтуера. И тогава внезапно преминаха към големи данни и машинно обучение, а след това към откриване на измами. Как се случи това? Това са много различни области на развитие.

Клиф: Програмирах от доста дълго време и успях да взема много различни курсове. И когато хората кажат: „О, ти си този, който направи JIT за Java!“, винаги е смешно. Но преди това работех върху клонинг на PostScript - езикът, който Apple някога използваше за своите лазерни принтери. А преди това направих имплементация на езика Forth. Мисля, че общата тема за мен е разработването на инструменти. Цял живот правя инструменти, с които други хора пишат страхотните си програми. Но също така участвах в разработването на операционни системи, драйвери, дебъгери на ниво ядро, езици за разработка на ОС, които започнаха тривиално, но с времето станаха все по-сложни. Но основната тема все още е разработването на инструменти. Голяма част от живота ми премина между Azul и Sun и става дума за Java. Но когато започнах да се занимавам с големи данни и машинно обучение, отново си сложих елегантната шапка и казах: „О, сега имаме нетривиален проблем и има много интересни неща, които се случват и хората правят неща.“ Това е чудесен път за развитие.

Да, наистина обичам разпределените изчисления. Първата ми работа беше като студент в С, по рекламен проект. Това беше разпределено изчисление на чипове Zilog Z80, които събираха данни за аналогово OCR, произведени от истински аналогов анализатор. Беше готина и напълно луда тема. Но имаше проблеми, някаква част не беше разпозната правилно, така че трябваше да извадите снимка и да я покажете на човек, който вече можеше да чете с очите си и да докладва какво пише, и следователно имаше задачи с данни и тези задачи имаха собствен език. Имаше бекенд, който обработваше всичко това - Z80, работещи паралелно с работещи vt100 терминали - по един на човек, и имаше модел за паралелно програмиране на Z80. Част от общата памет, споделена от всички Z80 в звездна конфигурация; Задната платка също беше споделена и половината от RAM беше споделена в мрежата, а друга половина беше частна или отиде на нещо друго. Смислено сложна паралелна разпределена система със споделена... полусподелена памет. Кога беше това... Дори не мога да си спомня, някъде в средата на 80-те. Преди доста време. 
Да, нека приемем, че 30 години са доста отдавна. Проблемите, свързани с разпределените изчисления, съществуват от доста дълго време; хората отдавна са във война с Beowulf- клъстери. Такива клъстери изглеждат като... Например: има Ethernet и вашият бърз x86 е свързан към този Ethernet и сега искате да получите фалшива споделена памет, защото тогава никой не можеше да прави разпределено изчислително кодиране, беше твърде трудно и следователно там беше фалшива споделена памет със защитени страници на паметта на x86 и ако сте писали на тази страница, тогава казахме на други процесори, че ако имат достъп до същата споделена памет, тя ще трябва да бъде заредена от вас и по този начин нещо като протокол за поддръжка появи се кохерентност на кеша и софтуер за това. Интересна концепция. Истинският проблем, разбира се, беше нещо друго. Всичко това проработи, но бързо се появиха проблеми с производителността, защото никой не разбра моделите на производителност на достатъчно добро ниво - какви модели за достъп до паметта имаше, как да се уверите, че възлите не се пингват безкрайно един друг и т.н.

Това, което измислих в H2O е, че самите разработчици са отговорни за определянето къде паралелизмът е скрит и къде не. Измислих модел на кодиране, който направи писането на код с висока производителност лесно и просто. Но писането на бавно работещ код е трудно, ще изглежда зле. Трябва сериозно да се опитате да пишете бавен код, ще трябва да използвате нестандартни методи. Спирачният код се вижда от пръв поглед. В резултат на това обикновено пишете код, който работи бързо, но трябва да разберете какво да правите в случай на споделена памет. Всичко това е свързано с големи масиви и поведението там е подобно на енергонезависимите големи масиви в паралелна Java. Искам да кажа, представете си, че две нишки пишат в паралелен масив, едната от тях печели, а другата съответно губи и не знаете коя е коя. Ако те не са променливи, тогава редът може да бъде какъвто искате - и това работи много добре. Хората наистина се интересуват от реда на операциите, те поставят volatile на правилните места и очакват свързани с паметта проблеми с производителността на правилните места. В противен случай те просто биха написали код под формата на цикли от 1 до N, където N е няколко трилиона, с надеждата, че всички сложни случаи автоматично ще станат паралелни - и това не работи там. Но в H2O това не е нито Java, нито Scala; можете да го считате за „Java минус минус“, ако искате. Това е много ясен стил на програмиране и е подобен на писане на прост C или Java код с цикли и масиви. Но в същото време паметта може да се обработва в терабайти. Все още използвам H2O. Използвам го от време на време в различни проекти - и все още е най-бързото нещо, десетки пъти по-бързо от конкурентите си. Ако правите големи данни с колонни данни, е много трудно да победите H2O.

Технически предизвикателства

Андрю: Кое е най-голямото ви предизвикателство в цялата ви кариера?

Клиф: Техническата или нетехническата част на въпроса обсъждаме? Бих казал, че най-големите предизвикателства не са технически. 
Що се отнася до техническите предизвикателства. Просто ги победих. Дори не знам кой беше най-големият, но имаше някои доста интересни, които отнеха доста време, психическа борба. Когато отидох в Sun, бях сигурен, че ще направя бърз компилатор, а куп възрастни казаха в отговор, че никога няма да успея. Но аз следвах този път, написах компилатор до разпределителя на регистъра и беше доста бързо. Беше толкова бърз, колкото съвременния C1, но тогава разпределителят беше много по-бавен и погледнато назад това беше голям проблем със структурата на данните. Трябваше ми, за да напиша графичен разпределител на регистри и не разбирах дилемата между експресивността на кода и скоростта, която съществуваше в онази епоха и беше много важна. Оказа се, че структурата на данните обикновено надвишава размера на кеша на x86 от това време и следователно, ако първоначално предположих, че разпределителят на регистър ще изработи 5-10 процента от общото време на трептене, тогава в действителност се оказа, че 50 процента.

С течение на времето компилаторът стана по-чист и по-ефективен, спря да генерира ужасен код в повече случаи и производителността започна все повече да прилича на това, което произвежда компилатор на C. Освен ако, разбира се, не напишете някакви глупости, че дори C не ускорява . Ако пишете код като C, ще получите производителност като C в повече случаи. И колкото по-нататък отивахте, толкова по-често получавахте код, който асимптотично съвпадаше с ниво C, разпределителят на регистъра започна да изглежда като нещо завършено... независимо дали вашият код работи бързо или бавно. Продължих да работя върху разпределителя, за да го накарам да прави по-добри селекции. Той ставаше все по-бавен и по-бавен, но даваше все по-добри резултати в случаите, когато никой друг не можеше да се справи. Бих могъл да се потопя в разпределител на регистър, да погреба един месец работа там и изведнъж целият код ще започне да се изпълнява с 5% по-бързо. Това се случваше периодично и разпределителят на регистъра се превърна в нещо като произведение на изкуството - всички го обичаха или мразеха, а хора от академията задаваха въпроси на тема "защо всичко е направено така", защо не линейно сканиране, и каква е разликата. Отговорът е все същият: разпределител, базиран на оцветяване на графика плюс много внимателна работа с буферния код, е равно на оръжие за победа, най-добрата комбинация, която никой не може да победи. И това е доста неочевидно нещо. Всичко останало, което компилаторът прави там, са доста добре проучени неща, макар че и те са доведени до ниво изкуство. Винаги правех неща, които трябваше да превърнат компилатора в произведение на изкуството. Но нищо от това не беше нещо изключително - с изключение на разпределителя на регистъра. Номерът е да внимавате изсече под натоварване и, ако това се случи (мога да обясня по-подробно, ако се интересувам), това означава, че можете да влезете по-агресивно, без риск от падане през пречупване в графика за изпълнение. В онези дни имаше куп пълномащабни компилатори, окачени с дрънкулки и свирки, които имаха разпределители на регистри, но никой друг не можеше да го направи.

Проблемът е, че ако добавите методи, които подлежат на вграждане, увеличаване и увеличаване на областта на вграждане, наборът от използвани стойности незабавно изпреварва броя на регистрите и трябва да ги изрежете. Критичното ниво обикновено идва, когато разпределителят се откаже и един добър кандидат за разлив струва друг, ще продадете някои обикновено диви неща. Стойността на вграждането тук е, че губите част от режийните разходи, режийни за извикване и запазване, можете да видите стойностите вътре и да ги оптимизирате допълнително. Цената на вграждането е, че се формират голям брой живи стойности и ако вашият регистър разпределител изгори повече от необходимото, вие незабавно губите. Следователно повечето разпределители имат проблем: когато вграждането пресече определена линия, всичко в света започва да се съкращава и производителността може да бъде изхвърлена в тоалетната. Тези, които прилагат компилатора, добавят някои евристики: например, за да спрат вграждането, започвайки с някакъв достатъчно голям размер, тъй като разпределенията ще съсипят всичко. Ето как се образува пречупване в графиката на производителността - вкарвате, вкарвате, производителността бавно расте - и тогава бум! – пада надолу като бърз жак, защото сте подредили твърде много. Ето как работи всичко преди появата на Java. Java изисква много повече вграждане, така че трябваше да направя своя разпределител много по-агресивен, така че да се изравнява, вместо да се срива, и ако вградите твърде много, започва да се разлива, но след това моментът „без повече разливане“ все още идва. Това е интересно наблюдение и ми дойде от нищото, не е очевидно, но се изплати добре. Започнах с агресивното вграждане и това ме отведе до места, където изпълнението на Java и C работят рамо до рамо. Те са наистина близки - мога да напиша Java код, който е значително по-бърз от C код и подобни неща, но средно, в голямата картина на нещата, те са приблизително сравними. Мисля, че част от тази заслуга е разпределителят на регистъра, който ми позволява да вмъквам възможно най-глупаво. Просто вмъквам всичко, което виждам. Въпросът тук е дали разпределителят работи добре, дали резултатът е интелигентно работещ код. Това беше голямо предизвикателство: да разбера всичко това и да го накарам да работи.

Малко за разпределението на регистъра и многоядрените

Владимир: Проблеми като разпределението на регистъра изглеждат като някаква вечна, безкрайна тема. Чудя се дали някога е имало идея, която да изглежда обещаваща и след това да се провали на практика?

Клиф: Разбира се! Разпределението на регистъра е област, в която се опитвате да намерите някаква евристика за решаване на NP-пълен проблем. И никога не можете да постигнете перфектно решение, нали? Това е просто невъзможно. Вижте, компилация Ahead of Time - също работи зле. Разговорът тук е за някакви средни случаи. Относно типичното представяне, така че можете да отидете и да измерите нещо, което смятате за добро типично представяне - в крайна сметка вие работите за подобряването му! Разпределението на регистъра е тема, свързана изцяло с производителността. След като имате първия прототип, той работи и рисува това, което е необходимо, работата по изпълнението започва. Трябва да се научите да измервате добре. Защо е важно? Ако имате ясни данни, можете да разгледате различни области и да видите: да, тук помогна, но там всичко се счупи! Появяват се някои добри идеи, добавяте нови евристики и изведнъж всичко започва да работи средно малко по-добре. Или не тръгва. Имах куп случаи, в които се борехме за петте процента ефективност, които отличаваха нашето развитие от предишния разпределител. И всеки път изглежда така: някъде печелиш, някъде губиш. Ако имате добри инструменти за анализ на ефективността, можете да намерите губещите идеи и да разберете защо се провалят. Може би си струва да оставите всичко както си е или може би да предприемете по-сериозен подход към фината настройка или да излезете и да поправите нещо друго. Това са цял куп неща! Направих този готин хак, но имам нужда и от този, и от този, и от този - и общата им комбинация дава някои подобрения. И самотниците могат да се провалят. Това е естеството на работата по изпълнение на NP-пълни проблеми.

Владимир: Човек получава усещането, че неща като рисуване в разпределителите са проблем, който вече е решен. Е, така е решено за теб, съдейки по това, което казваш, значи въобще си струва...

Клиф: Не е решен като такъв. Вие сте този, който трябва да го превърнете в „решен“. Има трудни проблеми и те трябва да бъдат решени. След като това е направено, е време да работим върху производителността. Трябва да подходите към тази работа по съответния начин - правете бенчмаркове, събирайте показатели, обяснявайте ситуации, когато, когато сте се върнали към предишна версия, вашият стар хак е започнал да работи отново (или обратното, спрял). И не се отказвайте, докато не постигнете нещо. Както вече казах, ако има готини идеи, които не са работили, но в областта на разпределението на регистрите на идеи е почти безкрайно. Можете например да четете научни публикации. Въпреки че сега тази област започна да се движи много по-бавно и стана по-ясна, отколкото в младостта си. Въпреки това, има безброй хора, които работят в тази област и всичките им идеи си заслужават да бъдат изпробвани, всички те чакат своето време. И не можете да кажете колко са добри, освен ако не ги опитате. Колко добре се интегрират с всичко останало във вашия разпределител, тъй като разпределителят прави много неща и някои идеи няма да работят във вашия конкретен разпределител, но в друг разпределител ще го направят лесно. Основният начин да спечели за разпределителя е да издърпа бавните неща извън главния път и да ги принуди да се разделят по границите на бавните пътища. Така че, ако искате да стартирате GC, поемете по бавния път, деоптимизирайте, хвърлете изключение и всички тези неща - знаете, че тези неща са относително редки. И наистина са редки, проверих. Вършите допълнителна работа и това премахва много от ограниченията на тези бавни пътеки, но всъщност няма значение, защото те са бавни и рядко се пътува. Например нулев указател - никога не се случва, нали? Трябва да имате няколко пътя за различни неща, но те не трябва да пречат на основния. 

Владимир: Какво мислите за многоядрените, когато има хиляди ядра едновременно? Това полезно нещо ли е?

Клиф: Успехът на GPU показва, че е доста полезен!

Владимир: Те са доста специализирани. Какво ще кажете за процесорите с общо предназначение?

Клиф: Е, това беше бизнес моделът на Azul. Отговорът се върна в ерата, когато хората наистина обичаха предвидимото представяне. Тогава беше трудно да се пише паралелен код. H2O кодиращият модел е много мащабируем, но не е модел с общо предназначение. Може би малко по-общ, отколкото при използване на GPU. Дали говорим за сложността на разработването на такова нещо или за сложността на използването му? Например Azul ме научи на интересен урок, доста неочевиден: малките кешове са нормални. 

Най-голямото предизвикателство в живота

Владимир: Какво ще кажете за нетехническите предизвикателства?

Клиф: Най-голямото предизвикателство не беше да бъда... добър и мил с хората. И в резултат постоянно попадах в изключително конфликтни ситуации. Тези, при които знаех, че нещата вървят наред, но не знаех как да продължа напред с тези проблеми и не можех да се справя с тях. Много дългосрочни проблеми, продължаващи десетилетия, възникнаха по този начин. Фактът, че Java има C1 и C2 компилатори е пряко следствие от това. Пряко следствие е и фактът, че нямаше многостепенна компилация в Java десет години подред. Очевидно е, че имаме нужда от такава система, но не е ясно защо я нямаше. Имах проблеми с един инженер... или група инженери. Едно време, когато започнах работа в Sun, бях... Добре, не само тогава, аз по принцип винаги имам собствено мнение за всичко. И си помислих, че е вярно, че можеш просто да вземеш тази своя истина и да я кажеш директно. Особено след като бях шокиращо прав през повечето време. И ако не ви харесва този подход... особено ако очевидно грешите и правите глупости... Като цяло малко хора биха могли да понесат тази форма на комуникация. Въпреки че някои биха могли, като мен. Изградих целия си живот на меритократични принципи. Ако ми покажеш нещо нередно, веднага ще се обърна и ще кажа: казахте глупости. В същото време, разбира се, се извинявам и всичко това, ще отбележа основанията, ако има такива, и ще предприема други правилни действия. От друга страна съм шокиращо прав за шокиращо голям процент от общото време. И не работи много добре в отношенията с хората. Не се опитвам да бъда мил, но задавам въпроса направо. „Това никога няма да проработи, защото едно, две и три.“ И те бяха като "О!" Имаше и други последствия, които вероятно е по-добре да пренебрегнем: например тези, които доведоха до развода с жена ми и десет години депресия след това.

Предизвикателството е борба с хората, с тяхното възприемане какво можеш или не можеш да направиш, кое е важно и кое не. Имаше много предизвикателства относно стила на кодиране. Все още пиша много код и в онези дни дори трябваше да забавя, защото изпълнявах твърде много паралелни задачи и ги изпълнявах зле, вместо да се съсредоточа върху една. Поглеждайки назад, написах половината код за Java JIT командата, командата C2. Следващият най-бърз кодер пише наполовина по-бавно, следващият наполовина по-бавно и това беше експоненциален спад. Седмият човек в този ред беше много, много бавен - това винаги се случва! Докоснах много код. Гледах кой какво е написал, без изключение, взирах се в техния код, прегледах всеки един от тях и все пак продължих да пиша повече аз, отколкото всеки един от тях. Този подход не работи много добре с хората. Някои хора не харесват това. И когато не могат да се справят, започват всякакви оплаквания. Например, веднъж ми казаха да спра да кодирам, защото пишех твърде много код и това застрашаваше екипа, и всичко това ми прозвуча като шега: пич, ако останалата част от екипа изчезне и аз продължа да пиша код, ти ще загуби само половината отбори. От друга страна, ако продължа да пиша код и вие загубите половината екип, това звучи като много лошо управление. Никога не съм мислил за това, никога не съм говорил за това, но все още беше някъде в главата ми. В съзнанието ми се въртеше мисълта: „Всички ли се шегувате с мен?“ И така, най-големият проблем бях аз и отношенията ми с хората. Сега разбирам себе си много по-добре, дълго време бях ръководител на екип за програмисти и сега директно казвам на хората: знаете, аз съм това, което съм, и ще трябва да се справите с мен - добре ли е, ако стоя тук? И когато започнаха да се занимават с това, всичко се получи. Всъщност аз не съм нито лош, нито добър, нямам лоши намерения или егоистични стремежи, просто това е моята същност и трябва някак си да живея с нея.

Андрю: Съвсем наскоро всички започнаха да говорят за самосъзнанието на интровертите и меките умения като цяло. Какво можете да кажете за това?

Клиф: Да, това беше прозрението и урокът, който научих от развода с жена ми. Това, което научих от развода, беше да разбера себе си. Така започнах да разбирам другите хора. Разберете как работи това взаимодействие. Това доведе до открития едно след друго. Имаше осъзнаване кой съм и какво представлявам. Какво правя: или съм зает със задачата, или избягвам конфликт, или нещо друго - и това ниво на самосъзнание наистина помага да се държа под контрол. След това всичко става много по-лесно. Едно нещо, което открих не само в себе си, но и в други програмисти, е неспособността да вербализирате мислите си, когато сте в състояние на емоционален стрес. Например, вие седите там и кодирате, в състояние на поток, а след това те идват при вас и започват да крещят в истерия, че нещо е счупено и сега ще бъдат взети крайни мерки срещу вас. И не можете да кажете нито дума, защото сте в състояние на емоционален стрес. Придобитите знания ви позволяват да се подготвите за този момент, да го преживеете и да преминете към план за отстъпление, след който можете да направите нещо. Така че да, когато започнете да осъзнавате как работи всичко, това е огромно събитие, променящо живота. 
Самият аз не можах да намеря правилните думи, но запомних последователността от действия. Въпросът е, че тази реакция е колкото физическа, толкова и вербална и имате нужда от пространство. Такова пространство, в дзен смисъла. Точно това трябва да се обясни и след това веднага да се отдръпнете – чисто физически да се отдръпнете. Когато замълча устно, мога да обработя ситуацията емоционално. Когато адреналинът достигне мозъка ви, превключва ви в режим на битка или бягство, вече не можете да кажете нищо, не - сега сте идиот, бичуващ инженер, неспособен на достоен отговор или дори да спре атаката, а нападателят е свободен да атакува отново и отново. Първо трябва да станете себе си отново, да възвърнете контрола, да излезете от режима „бий се или бягай“.

А за това имаме нужда от словесно пространство. Само свободно място. Ако изобщо кажете нещо, тогава можете да кажете точно това и след това отидете и наистина намерете „пространство“ за себе си: отидете на разходка в парка, заключете се под душа - няма значение. Основното нещо е временно да се изключите от тази ситуация. Веднага щом изключите поне за няколко секунди, контролът се връща, започвате да мислите трезво. „Добре, аз не съм някакъв идиот, не правя глупави неща, аз съм доста полезен човек.“ След като сте успели да се убедите, е време да преминете към следващия етап: разбиране на случилото се. Вие бяхте нападнати, атаката дойде откъдето не сте я очаквали, беше нечестна, подла засада. Това е лошо. Следващата стъпка е да разберете защо нападателят се нуждае от това. Наистина, защо? Може би защото самият той е бесен? Защо е луд? Например, защото се е прецакал и не може да поеме отговорност? Това е начинът да се справите внимателно с цялата ситуация. Но това изисква поле за маневриране, вербално пространство. Първата стъпка е да прекъснете вербалния контакт. Избягвайте дискусията с думи. Отменете го, тръгнете възможно най-бързо. Ако е телефонен разговор, просто затворете - това е умение, което научих от общуването с бившата ми жена. Ако разговорът не върви добре, просто кажете „довиждане“ и затворете. От другата страна на телефона: „бла бла бла“, вие отговаряте: „да, чао!“ и затворете. Просто прекратете разговора. Пет минути по-късно, когато ви се върне способността да мислите разумно, вие сте се охладили малко, става възможно да мислите за всичко, какво се е случило и какво ще се случи по-нататък. И започнете да формулирате обмислен отговор, вместо просто да реагирате емоционално. За мен пробивът в самосъзнанието беше именно фактът, че при емоционален стрес не мога да говоря. Излизане от това състояние, мислене и планиране как да реагирате и да компенсирате проблемите - това са правилните стъпки в случай, когато не можете да говорите. Най-лесният начин е да избягате от ситуацията, в която се проявява емоционалният стрес, и просто да спрете да участвате в този стрес. След това ставаш способен да мислиш, когато можеш да мислиш, ставаш способен да говориш и т.н.

Между другото, в съда, противниковият адвокат се опитва да направи това с вас - сега е ясно защо. Защото той има способността да те подтисне до такова състояние, че дори да не можеш да произнесеш името си например. В много реален смисъл няма да можете да говорите. Ако това се случи с вас и ако знаете, че ще попаднете на място, където се водят словесни битки, на място като съд, тогава можете да дойдете с вашия адвокат. Адвокатът ще се застъпи за вас и ще спре словесната атака и ще го направи по напълно законен начин, а загубеното дзен пространство ще ви се върне. Например, трябваше да се обадя на семейството си няколко пъти, съдията беше доста приятелски настроен по този въпрос, но противниковият адвокат ми крещеше и викаше, дори не успях да кажа дума. В тези случаи използването на медиатор работи най-добре за мен. Медиаторът спира целия този натиск, който се излива върху вас в непрекъснат поток, намирате необходимото дзен пространство, а с него се връща и способността да говорите. Това е цяла област на познание, в която има много за изучаване, много за откриване в себе си и всичко това се превръща в стратегически решения на високо ниво, които са различни за различните хора. Някои хора нямат проблемите, описани по-горе; обикновено хората, които са професионални търговци, ги нямат. Всички тези хора, които си изкарват хляба с думи – известни певци, поети, религиозни водачи и политици, те винаги имат какво да кажат. Те нямат такива проблеми, но аз имам.

Андрю: Беше... неочаквано. Страхотно, вече говорихме много и е време да приключим с това интервю. Със сигурност ще се срещнем на конференцията и ще можем да продължим този диалог. Ще се видим в Hydra!

Можете да продължите разговора си с Клиф на конференцията Hydra 2019, която ще се проведе на 11-12 юли 2019 г. в Санкт Петербург. Той ще дойде с доклад „Опитът с хардуерната транзакционна памет на Azul“. Билети могат да бъдат закупени на официалния уебсайт.

Източник: www.habr.com

Добавяне на нов коментар