Я своими гуманитарными мозгами всегда думал так — если программист знает, как сделать перфоманснее — значит надо сделать перфоманснее. Производительное решение = правильное решение. Один язык программирования может быть медленнее другого, и если это выяснится — язык программирования отправляется на помойку.
Ну и уж точно — если разработчик — специалист в области перфоманса, он будет топить за все эти вещи, даже если они неверны.
Естественно, все это чушь, но не мне вам об этом говорить. Поэтому к нам в подкаст пришел Андрей Акиньшин — разработчик и математик, кандидат физико-математических наук, мейнтейнер BenchmarkDotNet и perfolizer, автор книги Pro .NET Benchmarking и просто очень, очень крутой инженер.
Ниже — избранные цитаты.
Предусмотреть в бенчмарках все невозможно
У моего коллеги недавно случилось следующее. Он программировал с утра, у него всё было хорошо, всё работало быстро. В какой-то момент всё начало втыкать — Rider работает медленно, IDEA, браузер – всё работает медленно. Он никак не мог понять, в чём же дело? А потом понял. Он работал на ноутбуке чёрного цвета, который стоял у окна. С утра было довольно прохладно, а днём поднялось солнышко, ноутбук очень сильно нагрелся и ушёл в термальный троттлинг.
Он знает, что есть такая штука, знает, что физическое окружение может влиять на перфоманс, и он довольно быстро понял, что происходит. У него в голове была модель, согласно которой работает мир, и в рамках этой модели он более-менее быстро разобрался – что же происходит.
То есть — самый главный навык, который можно получить в бенчмаркинге – это не знание всего абсолютно во всех деталях – всех рантаймов и всех железяк. Главное — понимание, как тебе нужно действовать, чтобы найти проблему, желательно максимально быстро с минимальными усилиями.
Я приведу аналогию с языками. Когда ты учишь свой первый функциональный ЯП, тебе нужно немного модифицировать своё отношение к миру — понять принципы функционального программирования, то, как вообще нужно мыслить. Потом ты берёшь следующий функциональный язык Х, и у тебя в голове эти принципы уже есть. Ты смотришь пару hello world и тоже начинаешь писать.
При этом ты можешь не знать каких-то нюансов языка. Ты можешь не знать, как работают определенные синтаксические конструкции, но тебя это уже не настолько сильно смущает. Ты чувствуешь себя комфортно и пишешь. Столкнулся с непонятным поведением – почитал мануал, разобрался, новый факт легко лёг в твою картину мира, и ты пошёл дальше. И ты никогда не выучишь все нюансы всех функциональных языков в мире, но общий подход у тебя в голове останется.
Я считаю, что нужно в каждой области доходить до похожего уровня, а дальше идти вширь.
В какой-то момент в бенчмаркинге я сосредоточился именно на точности измерения, на особенности определенных рантаймов, железяк, ещё чего-то. Потом я перестал открывать для себя Америку каждый раз, и все перфоманс-проблемы начали попадать в уже известные мне классы. И я пошёл вширь – в сторону перфоманс-анализа: что делать с цифрами, которые мы намерили. А это та область, где я ещё не достиг граней познания. Мне уже стали понятны некторые вещи, но впереди еще большой кусок работы — понять, как же всё это применять на практике, какие формулы использовать, какие не использовать, какие подходы хорошие, какие нет.
Бенчмаркинг ради бенчмаркинга – не самое лучшее, чем можно заниматься
Всегда должны быть какие-то бизнес-требования по производительности, ты всегда должен понимать, к чему ты стремишься. Если у тебя нет бизнес-требований, то заниматься перфомансом тоже смысла нет. Соответственно, когда бизнес-требования есть, ты уже начинаешь понимать, какие подходы хотя бы на глазок ты можешь использовать, а какие нет. Если нет — ты идёшь, бенчмаркаешь, проверяешь – какие подходы укладываются в твои требования.
И когда у тебя есть набор алгоритмов, вариантов написания кода, дизайна и прочего, и все укладываются в требования — ты уже выбираешь то, что будет более консистентно с остальным проектом, что отражает, твои взгляды на эстетику, на то, как правильно писать код.
Грубо говоря, если у меня в коллекции есть максимум 10 элементов, и есть два варианта — написать простой алгоритм за куб или очень сложный за n*log n — я напишу простой за куб, который будет понятен всем, который будет легко поддерживать и модифицировать. Потому что я понимаю — мои перфомансные ограничения он никогда в жизни не пробьёт.
Если ты написал медленное решение для маленького набора данных, а потом его использовали для большого, и у этого не было сверхплохих последствий (обычно их нету) – ну, пошли и починили. Зато в голове будет модель того, как в будущем этих ошибок избежать.
Например, ты можешь в самом начале метода поставить assert, что количество элементов в коллекции не превышает такого-то числа. Тогда следующий программист, кто случайно попытается заиспользовать твой метод, сразу увидит exception и не будет его использовать. Такие вещи приходят с опытом.
Есть другая проблема – изменчивые бизнес-требования. Они точно будут меняться – это аксиома нашей действительности, от этого никуда не уйти. С опытом ты уже на глазок сможешь предсказывать, где требования могут измениться, где стоит заложиться на хороший уровень перфоманса, где может возрасти нагрузка.
Пока этой интуиции нет, можно идти методом проб и ошибок и смотреть, что получается.
У тебя есть всегда трейдоф между перфомансом и красотой
Если ты будешь писать максимально производительно, скорее всего, твой код будет просто ужасный, отвратительный — и даже если закрыть глаза на эстетику — его будет сложно поддерживать, в нём постоянно будут возникать трудноуловимые баги, потому что архитектура плохая, код плохой, всё плохое.
Я считаю, нужно ориентироваться на текущие бизнес-требования, и в рамках них писать максимально чистый, понятный, красивый, поддерживаемый код. А в тот момент, когда оно начинает жать, (или есть ощущение, что скоро начнёт), то тогда уже что-то менять.
И даже если ты будешь всегда концентрироваться исключительно на перфомансе — нет такой штуки, как идеально оптимизированный, максимально производительный код. Это значит всё — забыли про C#, забыли про все красивые языки. А лучше вообще писать в машинных кодах, потому что Ассемблер – он тоже по синтаксису ограничен. А если сразу по байтикам писать, то ты получишь перфоманс-boost.
В некоторых случаях самый быстрый код оказывается самым красивым, самым очевидным, самым правильным. Но такие трейдофы неизбежно возникают в десятках и сотнях мелких моментов. Скажем, есть такая штука, как проверки за выход границ массива. Ты можешь согласиться, что рантайм за тебя побеспокоится о том, чтобы проверить во всех местах выход за границы массива, и если ты обратишься к минус первому элементу, у тебя вылетит exception и ты не прочитаешь из левого куска памяти.
И вот за эту уверенность, что из не из того куска памяти ты точно никогда не вычитаешь — ты платишь небольшим кусочком перфоманса. То есть мы используем перфоманс, как ресурс для того, чтобы сделать программу более стабильной, понятной и поддерживаемой.
У языка нет такого свойства как перфоманс
Если ты видишь статью, что язык Х быстрее, чем язык Y – можешь закрывать статью. Язык – это математическая абстракция. Это набор правил, согласно которому составляется программа. У него нет производительности, у него нет перфоманса, это нечто, что существует в твоей голове и находит воплощение в текстовом редакторе.
Перфоманс есть у конкретных рантаймов, окружений, у конкретных программ, у конкретных апишек. Вот когда ты учитываешь все эти факторы, то можно рассуждать о перфомансе. Но там случается комбинаторный взрыв, и ты не можешь сказать, что один код на вот этом языке всегда быстрее, чем другой код на вот этом, потому что выходят новые версии железа и рантаймов. Ты никогда в жизни не переберешь все возможные комбинации внешних факторов. Апишки, которые ты используешь — принципиально разные.
Например, в условном языке на ранних стадиях развития реализовали метод для сортировки с помощью пузырька. Ну, не знаю — ребята хотели побыстрее выкатить релиз, написали простейшую сортировку, которую могли сделать. Ты взял, заиспользовал этот метод, и он оказался медленнее на больших данных, чем в другом языке, где сделан quicksort. Значит ли это, что ты можешь говорить о перфомансе каких-то языков? Нет. Ты можешь говорить, что вот эта конкретно апишка из этого языка на этой операционной системе, вот на этом железе, вот в этих окружениях работает медленнее, чем другая апишка из другого языка в другом environment. Так можно сказать. Но получится очень длинный абзац текста, чтобы сформулировать правильно.
Условно можно сказать, что C++ в большинстве случаев работает быстрее, чем JavaScript. Но более правильно будет сказать, что С++ программисты с хорошим опытом С++, которые пишут на С++, напишут программу, которая скорее всего будет быстрее, чем джаваскриптер на JavaScript напишет что-то, что будет работать в браузере.
Но и здесь есть очень много оговорок. А что если чувак, который писал на JavaScript скажет, что это не так, и пойдёт там на какой-нибудь там WebAssembly или еще чего-нибудь переделывать. Или найдет на GitHub суперинтерпретатор-компилятор JavaScript, который работает с очень усечённым подмножеством JS на три с половиной синтаксические конструкции, но зато выдаёт сверхбыстрый нативный код.
И вот там при желании можно написать такой код, который обгонит С++. Более того, можно написать свой компилятор JavaScript, который будет задизайнен, чтобы компилировать одну-единственную программу и обогнать по скорости «плюсы». И Это, в принципе — валидный вариант.
Социальное давление популярного опенсорсного проекта
С ростом и известностью проектов приходит некий уровень ответственности. Но на самом деле у тебя не появляются обязательства. Этот факт не всегда просто понять, особенно когда приходят всякие люди на GitHub и говорят: «У меня вот здесь не работает! Почините срочно! Мне очень надо, чтоб вот это заработало. Идите и чините!» Или чувак заводит ишью, а я в отпуске. Проходит три-четыре дня, я даже не видел того, что он что-то там завёл. Я где-то отдыхаю, и чувак начинает — «Какого хрена вы мне не отвечаете? Что вообще у этого проекта за комьюнити?! Вы вообще все отвратительные люди, с вами надо делать плохие вещи! Я потратил своё время, написал вам, что вы неправы, а вы с этим вообще ничего не делаете, уже четыре дня меня игнорируете! Как так можно?!»
И чем популярнее проект, тем больше социальное давление от людей, которые считают, что опенсорс – это место, где другие люди за бесплатно делают за тебя твою работу. Но на самом деле это не так.
И вот, когда появляется иммунитет против людей, которые от тебя что-то хотят, то жить становится намного проще. Сейчас я добираюсь до BenchmarkDorNet, когда у меня есть время и настроение покодить. Я знаю, что там много багов. Они в основном некритичные, и касаются каких-то маргинальных случаев — в таком-то окружении с последней превьюхой пятого DotNet что-то где-то не работает. Ну и ладно, пущай не работает. Когда у меня будет настроение, я пойду и пофикшу.
Если другим людям нужно, они могут пофиксить сами и прислать пулреквест — сделаю ревью, когда у меня будет время и настроение.
Смотрите весь подкаст здесь.
Автор: Артем Малышев