Голямо интервю с Клиф Клик — бащата на JIT компилацията в Java

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

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

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

Интервюто водят:

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

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

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

Клиф: Абсолютно!

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

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

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

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

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

Модел на разходите

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

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

Андрей: И как да задържите толкова много в главата си? Това се постига с много опит, нали? Къде се добива този опит?

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

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

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

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

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

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

Клиф: Дата структури. Между другото, отдавна не водя тези занятия... Rocket School. Беше забавно, но изискваше толкова усилия, а аз все пак имам и личен живот! Така че, на едно от големите и интересни занятия, „Къде отива вашият перформанс”, дадох на студентите пример: два и половина гигабайта финтех данни се четяха от CSV файл и след това трябваше да се изчисли броят на продаваните продукти. Обикновени тикерни пазарни данни. UDP пакети, превърнати в текстов формат, от 70-те години. Chicago Mercantile Exchange – всякакви неща като масло, царевица, соеви зърна и подобни. Трябваше да се преброят тези продукти, броят на сделките, средният обем на движението на средства и стоки и т.н. Това е доста проста търговска математика: намиране на кода на продукта (това са 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 обикновено трябва активно да се използват цикли на процесора, а тук вие сте изгорили цялата памет много по-рано. Решението е да се намали броят на заделянията. 
Друга част от проблема е, че ако стартирате профайлера, когато е изтекла паметта, точно в момента, в който това се случва, обикновено очаквате кеша да се върне, защото той е пълен с боклук, който току-що сте генерирали, всичките тези редове. Затова всяка операция load или store става бавна, тъй като те водят до пропуски в кеша – целият кеш стана бавен, чакайки да освободите боклука. Следователно профилерът просто показва топъл случаен шум, размазан върху целия цикъл – няма да има отделна гореща инструкция или място в кода. Само шум. И ако погледнете цикли на GC, те всички ще бъдат по Young Generation и супер бързи – микросекунди или милисекунди максимум. Тази всяка памет умира мигновено. Вие заделяте милиарди гигабайта, и той ги отрязва, и отрязва, и отново отрязва. Всичко това става много бързо. Оказва се, че има евтини цикли на GC, топъл шум през целия цикъл, но ние искаме 5-кратно ускорение. В този момент трябва нещо в главата ви да се свърже и да прозвучи: "защо така?!". Препълването на паметта не се отразява в класическия отладчик, нужно е да стартирате отладчик на хардуерни производствени счетчици и да видите това сами и директно. А не директно, може да се заподозре от тези три симптома. Третият симптом е, когато погледнете какво заделяте, питате профайлера, и той отговаря: "Направи милиард реда, но GC работи безплатно". Веднага щом това се случи, вие разбирате, че сте генерирали твърде много обекти и сте изгорили цялата памет. Този начин на разбиране съществува, но не е очевиден. 

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

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

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

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

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

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

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

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

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

Андрей: Както разбирам, вашият език ще бъде безопасен по отношение на използването на паметта. Мислили ли сте да реализирате нещо подобно на borrow checker от Rust? Заглеждали ли сте се в него, какво е мнението ви?

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

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

Андрей: Също така исках да попитам за кариерата като цяло. Вие станахте известен благодарение на работата си по JIT в HotSpot, след което преминахте в Azul – и това също е компания, свързана с JVM. Но вече се занимавахте повече с хардуер, отколкото със софтуер. А после изведнъж се прехвърлихте на Big Data и Machine Learning, а след това на откритие на измами. Как така се получи? Това са много различни области на разработка.

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

Да, аз много обичам разпределени изчисления. Първата ми работа беше още в студентските години на C, над рекламен проект. Това бяха разпределени изчисления на чипове Zilog Z80, които събираха данни за аналогово оптично разпознаване на текст, извършвано от истински аналогов анализатор. Темата беше впечатляваща и напълно необичайна. Но имаше проблеми, част от данните не се разпознаваха правилно, затова трябваше да се извлича изображение и да се показва на човек, който вече е прочел текста, за да съобщи какво всъщност се казва. Имаше данни, свързани с работните задачи, и тези задачи имаха собствен език. Имаше бекенд, който всичко това обработваше – работещи паралелно Z80 с активирани терминали vt100 – по един за всеки човек, и имаше модел за паралелно програмиране на Z80. Някаква обща памет, която всички Z80 споделяха в конфигурация тип 
Да, ще приемем, че 30 години са достатъчно дълго. Задачите, свързани с разпределените изчисления, съществуват от доста дълго време, хората отдавна се борят с Beowulf-кластери. Тези клъстери изглеждат като… Например: имаме Ethernet и твоя бърз x86 е свързан към този Ethernet, и сега искаш да получиш fake shared memory, защото никой не можеше да се занимава с кодиране на разпределени изчисления, това беше твърде сложно и затова имаше fake shared memory с защита на страниците в паметта на x86, и ако пишеш в тази страница, казвахме на останалите процесори, че ако получат достъп до същата shared memory, трябва да я заредят от теб, и по този начин се появи нещо като протокол за поддръжка на кохерентност на кешовете и софтуер за това. Интересна концепция. Истинският проблем обаче беше в нещо друго. Всичко това работеше, но ти бързо получаваше проблеми с производителността, защото никой не разбираше модела на производителността в достатъчно добра степен – какви са там модели на достъп до памет, как да се направи така, че възлите да не се пингват безкрайно един друг и така нататък.

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

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

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

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

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

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

Някои думи за алокация на регистри и многоядреност

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

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

Владимир: Има усещането, че неща като боядисването в алокаторите вече са решена задача. Е, за вас изглежда решена, judging by what you are telling, така че наистина ли си струва да…

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

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

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

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

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

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

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

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

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

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

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

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

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

Андрей: Това беше… неочаквано. Отлично, вече говорихме доста и е време да приключим това интервю. Непременно ще се срещнем на конференцията и можем да продължим този диалог. Ще се видим на Hydra!

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

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

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster