вторник, 29 июля 2014 г.

[prog.thoughts] О производительности вообще, показателях микробенчмарков в частности, ну и о C++

Начну, как водится, издалека. Тем, кому не интересна вступительная часть про SObjectizer, можно начать читать отсюда.

Для C++ сейчас есть три живых OpenSource фреймворка, дающих возможность разрабатывать софт с использованием Actor Model. Это -- SObjectizer, C++ Actor Framework (CAF, ранее libcppa) и Theron. Два последних, в отличии от SObjectizer, в своих материалах уделяют внимание своей высокой производительности. Мол, в таких-то тестах мы показываем столько-то миллионов сообщений в секунду, а в таких-то тестах -- столько-то. SObjectizer же похвастаться большими цифрами в микробенчмарках никогда не мог. Более того, оптимизация его производительности никогда не была приоритетом №1, особенно на ранних стадиях разработки SO-4 и SO-5. Более того, в том же SO-4 микробенчмарки появились лишь спустя несколько лет после начала активного использования SObjectizer в продакшене.

Причина тому проста: SObjectizer создавался как рабочий инструмент, и скорость его работы была всего лишь одним из его качеств, причем далеко не самых важных. В подавляющем большинстве случаев, для решения прикладных задач производительности SObjectizer хватало. Припоминается лишь два случая, в которых пришлось серьезно засесть за оптимизацию производительности. В первом случае это был не сам SObjectizer, а библиотека MBAPI-3, построенная над ним, в которой были более сложные правила определения получателей сообщений и рероутинга перехваченных и частично обработанных перехватчиками сообщений. Второй случай -- это мой косяк в реализации транспортного слоя с использованием ACE_Reactor-ов (не вкурил в должной степени руководство по взаимодействию ACE_Reactor-ов, ACE_Event_Handler-ов и ACE_SOCK_Stream), из-за чего в определенных ситуациях данные из сокетов вычитывались гораздо медленнее, чем это должно было происходить. Производительность же основной функциональности (т.е. обслуживание агентов и их коопераций, доставка и обработка сообщений) практически всегда была good enough.

Вопросы производительности стали выходить на передний план после того, как мы стали пытаться выпустить SObjectizer "в свет". Тут уж ничего не поделать, это одна из маркетинговых фишек. Когда конкуренты трубят, что у них пропускная способность в 20M сообщений в секунду, очень хочется смело заявить, что у нас 25M. Или, хотя бы, у нас 18.5M, но зато каждый покупатель получает 3 года бесплатного сервисного обслуживания и подарок -- фирменную чашку с логотипом нашей компании :)

Из-за этого меня несколько раз настигало что-то вроде лихорадочного стремления получить высокие показатели в микробенчмарках. В последний раз это произошло буквально несколько недель назад, во время работы над версией 5.4.0. Излечение же было несколько болезненным, но быстрым :)

Сначала плотно задуматься о том, что "где-то что-то не так", заставили результаты экспериментов с lock-free очередями. В процессе которых вспомнилось то, о чем я когда-то читал, но забыл. А именно: для выжимания максимальной производительности из современных процессоров не нужно создавать большое количество нитей и уж тем более часто передавать информацию между ними. Напротив, нужно свести количество рабочих потоков к минимуму, постараться делать всю работу в рамках одного потока, стараться как можно меньше передавать информацию между потоками, а так же попробовать устранить модификацию общих данных в ОП из нескольких потоков. Т.е., работая на процессорах с 4-8-16 и более ядрами, нужно отказаться от соблазна распределить 50 мелких задачек между всеми имеющимися ядрами. Вместо этого лучше разбить их всего на два-три потока, а то и вообще попробовать выполнить их все последовательно в одной нити, работающей на одном ядре.

Самое смешное, что окончательно от "лихорадки микробенчмарков" я избавился вчера вечером, когда читал большую статью "Why mobile web apps are slow". Статья очень большая, не новая (опубликована чуть больше года назад), да и уже попадалась мне на глаза ранее. Но тогда я только мельком ее просмотрел, а вчера прочитал полностью. Статья классная. Заставляет смотреть на вопросы производительности по-новому, даже если ты и не имеешь отношения к разработке мобильных приложений на JavaScript-е.

Так что теперь я смотрю на показатели микробенчмарков различных фреймворков для обмена сообщениями совсем иначе :) Вы заявляете, что помогаете выжать максимум из многоядерного железа и при этом говорите о пропускной способности в 20M сообщений в секунду? Но если кто-то выжимает максимум из многоядерного железа, то зачем ему 20M раз в секунду пересылать информацию между потоками? Даже если информацию нужно пересылать 1M раз -- это уже выглядит подозрительным. И даже если 100K раз. Какое отношение количество обменов сообщениями между сущностями имеет к производительности? Для производительности-то как раз нужно, чтобы этих обменов не было вообще. Ну или чтобы количество этих обменов исчислялось единицами, максимум десятками раз. Batch processing, знаете ли, рулит не только при работе с СУБД. Вместо пересылки 100K мелких сообщений лучше было бы собрать 5 пакетов по 20K штук и переслать ссылки на эти пакеты (да еще так, чтобы после их обработки выделенную под пакеты память освободила та нить, которая их собирала, или же вообще скопировать все содержимое пакета в принадлежащий другой нити буфер). А то и вовсе не передавать эти пакеты никуда, а сделать всю обработку в рамках старого-доброго однопоточного приложения: одна процедура нагенерит 100K объектов, вторая процедура их следом обработает. И все.

Но статья "Why mobile web apps are slow" заставила задуматься еще о C++ вообще. О том, какой смысл писать сейчас что-то новое на C++. И о том, почему на C++ не пишут что-то новое.

Есть у меня подозрение, что современный C++, тот, который C++11, это крайне недооцененный язык. ИМХО, репутация С++ была слишком сильно подмочена длительным принятием стандарта C++98, затем еще более длительным появлением компиляторов, хотя бы более-менее нормально подерживающих С++98/03. Фактически, если говорить про платформу Windows, что-то более-менее приличное с поддержкой C++98 появилось только с выходом VisualStudio 2003, то, что до этого было в VisualStudio 98 (т.е. Visual C++ 6.0) -- это ужас и кошмар. Ну а еще более комфортной работа с C++98/03 стала при появлении VisualStudio 2007 (там и скорость компилятора была выше, и генерируемый код быстрее, и поддержка стандарта строже). Т.е. с момента оформления стандарта до появления действительно хорошего компилятора для самой популярной платформы прошло почти 10 лет. На которые пришелся еще и бурный расцвет web-программирования, оттянувшего на себя значительную долю разработчиков с десктопа.

К чему это все привело? Да к тому, что старых C++разработчиков, начинавших в во времена до C++98, осталось совсем мало. Многие просто перешли в другие предметные области и, соответственно, на новые языки/технологии. Кто-то вообще отошел от программирования. Т.е. старых, матерых, C++ников, помнящих эволюцию C++ хотя бы с 1991-го и по нынешние дни, осталось совсем-совсем мало. Не знаю, как много новичков, которые пришли в C++ после 2003-го, подозреваю, что немного. К тому времени у C++ уже прочно сложилась карма чрезвычайно сложного, маргинального инструмента, на котором невозможно написать что-то более-менее серьезное, без большого количества глюков внутри. Такая репутация небезосновательна, но она была заслужена задолго до того, как на C++ научились программировать нормально, проложив мостики между граблями, о которых так любят говорить в форумных войнах.

Но, самое важное, все это было до C++11. А C++11 -- это уже совсем другой C++, намного более безопасный и значительно более быстрый, чем C++98/03. Всего лишь несколько новых вещей, таких как rvalue references и move semantics, lambda-функции и variadic templates, shared_ptr и unique_ptr в стандартной библиотеке, и язык стал совсем другим. Писать быстрый и надежный код на C++11 намного, очень намного проще, чем на C++98/03. Правда, многословность C++ все равно остается, что правда, то правда, но тут уж вряд ли что-то изменишь.

Только вот вряд ли к C++11 потянутся толпы желающих писать быстрые программы. По многим причинам. Начиная от безнадежно испорченной кармы C++ и заканчивая модой на функциональщину (ведь изучать Haskell -- это же гораздо круче, чем изучать C++11, моднее, да и сразу переводит неофита в категорию крутых спецов). А так же тем, что за производительность программ (пока?) не платят.

Хотя, у меня после прочтения "Why mobile web apps are slow" сложилось впечатление, что это пока. Там ведь говорится о вещах, которые касаются не только разработки под мобильные устройства. Там много о накладных расходах, налагаемых сборщиком мусора, например. А так же, вскользь, о том, что базовые принципы, заложенные при разработке языков с VM или промежуточным байт-кодом, ставят во главу угла безопасность, а не производительность. Так что пока это может быть актуально для портативных устройств (но ведь и этот рынок постоянно растет, причем его рост опережает рост аппаратных мощностей мобильных устройств). Хотя, не исключено, что и на рынке серверных и десктопных приложений скоро начнут считать деньги по-другому. И предпочитать решения, которые требуют 10 серверов вместо 20 (кстати, почему сейчас Facebook вкладывает силы в разгон своего PHP-кода, а Twitter переписывал свои куски с Ruby на Scala?).

Но это все банальности о которых я, да и не только я, говорили неоднократно. Думаю, что сейчас важнее другой момент, на который меня подтолкнула статья "Why mobile web apps are slow": а чем ваши продукты будут отличаться от продуктов конкурентов? Если конкурирующие продукты сделаны такими же посредственными хорошими программистами, как и ваши, на тех же самых инструментах и технологиях, с такими же требованиями к железу и такой же ресурсоемкостью?

Т.е., если вы выпускаете какой-то новый, революционный продукт, не имеющий аналогов и захватывающий рынок, то без разницы, на чем он написан -- на Haskell, Erlang, Scala, Java, C#, Python или Ruby. Но когда к вам подтянутся конкуренты, когда рынок будет поделен и рулить продажами будут маркетологи, устраивающие распродажи или привлекающие внимание новых пользователей удачными рекламными роликами? За счет чего снижать издержки, уменьшать стоимость транзакции и увеличивать прибыль? Ваша Java на ваших серверах будет тормозить так же, как такая же Java на серверах конкурентов. Какую-то часть забот возьмет на себя техподдержка, грамотные люди оттуда будут вовремя заменять старые диски на новые, производить апгрейд железа, оптимизировать распределение ресурсов, перекраивать лимиты расхода ресурсов и т.д. Но это мало касается разработчиков. Чем разработчики смогут помочь в такой конкурентной борьбе?

Насколько много тут возможностей? Имхо, не так уж и много.

Во-первых, это люди. Чем более толковых разработчиков вы сможете заарканить, заинтересовать и удержать, тем лучше. Новые идеи и тщательное отношение к работе дадут вам такое преимущество над командами разработчиков у конкурентов, какое вам и не снилось, даже если они будут использовать одинаковые инструменты и технологии. Проблема, правда, в том, что когда главными людьми в компании становятся маркетологи, никому нет дела до конкретных людей, занимающихся разработкой -- это обычное следствие преобразования компании из инновационной адхократии к механистической бюрократии.

Во-вторых, уникальные инструменты. Так, если и вы, и конкуренты, используете Java, то при более-менее равных командах скорость воплощения новых фич и количество ошибок на 1000 строк кода и у вас, и у конкурентов будет одинаковой. Скорость работы и требования к ресурсам, скорее всего, тоже. Однако, если вы возьмете другой инструмент, ситуация изменится. Например, у Haskell-я есть репутация языка, пропускающего гораздо меньше ошибок в успешно скомпилированный код, чем Java и C#. Это офигенное конкурентное преимущество для команды разработки. Ведь, если у вас уходит три дня на реализацию фичи и один день на тестирование, то это позволяет выводить новую функциональность на рынок быстрее, чем когда на реализацию нужно два дня и затем еще три дня на тестирование.

Аналогичный эффект могут давать и языки, способные производить очень быстрый и потребляющий мало ресурсов код. В частности, C++. Ведь если ваше решение дает время отклика на 15% быстрее конкурента, при этом требуя в два раза меньше железа, то это так же преимущество. И, если оно есть (причем оно объективно есть, уже неоднократно показывалось, что языки вроде Java/C# способны показывать сравнимую с C++ производительность только при задействовании гораздо больших ресурсов, в частности, нужны объемы памяти от 4-х до 6-ти раз большие), то почему бы его не использовать?

Для этого, правда, нужно отойти от стереотипов о том, что C++ -- это старое и глючное говно мамонта. Таковым он, может быть, и был года до 2007-го. Но сейчас, с появлением нормальной поддержки C++11 в новых C++ компиляторах, это точно не так.

Комментариев нет: