Большое интервью с Клиффом Кликом — отцом 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. Интересуется тестированием крупномасштабных backend- и распределённых систем.
Владимир Ситников из 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, но, если он этого не сделал – это будет вызов функции. Поскольку вызов лежит на горячем цикле, он отменит все другие оптимизации в этом цикле. Поэтому реальная стоимость будет намного больше. И ты тут же теряешь способность смотреть на кусок кода и понимать, что нам стоит его выполнить в терминах тактовой частоты процессора, используемой памяти и кэша. Все это становится интересно только если действительно забурился в перформанс.
Сейчас мы оказались в ситуации, когда скорости процессоров уже десятилетие как почти не растут. Старые времена возвращаются! Вы уже не можете рассчитывать на хорошую однопоточную производительность. Но если вдруг заняться параллельными вычислениями – это безумно сложно, все на тебя смотрят как на Джеймса Бонда. Десятикратные ускорения здесь обычно возникают в тех местах, где кто-то что-то прошляпил. Параллельность требует много работы. Чтобы получить то самое десятикратное ускорение, нужно понять модель стоимости. Что и сколько стоит. А для этого нужно понять, как язык ложится на нижележащее железо.
Мартин Томпсон подобрал отличное слово для своего блога Mechanical Sympathy! Необходимо понимать, что собирается делать железо, как именно оно будет это делать, и почему оно вообще делает то, что делает. Пользуясь этим, довольно просто начать считать инструкции и выяснять, куда утекает время выполнения. Если же у тебя нет соответствующей подготовки, ты просто ищешь черную кошку в темной комнате. Я постоянно вижу людей, оптимизирующих производительность, у которых нет ни малейших идей, какого черта они вообще делают. Они очень мучаются и не очень куда-то продвигаются. И когда я беру тот же самый кусок кода, подсовываю туда парочку мелких хаков и получаю пятикратное или десятикратное ускорение, они такие: ну, так нечестно, мы и так знали, что ты лучше. Поразительно. О чем это я… модель стоимости – это о том, что за код ты пишешь и как быстро он в среднем работает во всеобщей картине.
Андрей: И как такой объем удержать в голове? Это достигается большим количеством опыта, или? Где такой опыт добывается?
Клифф: Ну, свой опыт я получил не самым простым путем. Я программировал на Ассемблере еще в те времена, когда можно было разобраться в каждой отдельной инструкции. Это звучит глупо, но с тех пор у меня в голове, в памяти, навсегда остался набор инструкций 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 делает строку из набора байтиков, пришедших по сети, один раз для каждой строки, для каждой из сотен миллионов строк. Я беру эту строку, анализирую еt и выбрасываю. Почему выбрасываю – ну, я же ее уже обработал, все. Так что, для каждого байта, прочитанных из этих 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, и получить сильную модель памяти и семантику памяти для лоадов и сторов. При этом иметь автоматический вывод типов как в 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, а потом на fraud detection. Как так получилось? Это очень разные области разработки.
Клифф: Я уже довольно давно занимаюсь программированием и успел отметиться на очень разных занятиях. И когда люди говорят: «о, ты же тот, кто делал JIT для Java!», это всегда забавно. А ведь до этого я занимался клоном PostScript – того языка, который Apple когда-то использовала для своих лазерных принтеров. А до этого делал реализацию языка Forth. Думаю, общая тема для меня – это разработка инструментов. Всю жизнь делаю инструменты, с помощью которых другие люди пишут свои крутые программы. Но я занимался и разработкой операционных систем, драйверов, отладчиков уровня ядра, языков для разработки ОС, которые начинались тривиально, но со временем все усложнялись и усложнялись. Но основная тема, все-таки – разработка инструментов. Большой кусок жизни прошёл между Azul и Sun, и он был про Java. Но когда я занялся Big Data и Machine Learning, я снова надел свою парадную шляпу и сказал: «Ох, а вот теперь у нас появилась нетривиальная проблема, и тут вообще происходит куча интересных вещей и людей, которые что-то делают». Это отличный путь для развития, по которому стоит пройти.
Да, я очень люблю распределенные вычисления. Моя первая работа была в студенчестве на C, над рекламным проектом. Это были распределенные вычисления на чипах Zilog Z80, которые собирали данные для аналогового оптического распознавания текстов, производящегося настоящим аналоговым анализатором. Это была крутая и совершенно ненормальная тема. Но там были проблемы, какая-то часть не распознавалась правильно, поэтому нужно было доставать картинку и показывать ее человеку, который уже читал глазами и сообщал, что же там говорится, и поэтому там были джобы с данными, и у этих джобов был свой язык. Был бэкенд, который все это обрабатывал – работающие параллельно Z80 с запущенными терминалами vt100 – по одному на человека, и была модель параллельного программирования на Z80. Некий общий кусок памяти, который разделяли все Z80 внутри конфигурации типа «звезда»; разделялся и бэкплейн, и половина RAM разделялась внутри сети, и еще половина была приватной или уходила на что-то еще. Осмысленно сложная параллельная распределенная система с разделяемой… полуразделяемой памятью. Когда же это было… Уже и не вспомнить, где-то в середине 80-х. Довольно давно.
Да, будем считать, что 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 процентов всего времени джитования, то в реальности это вылилось в 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!