Без интернета компьютер становится не таким уж полезным устройством, а некоторые приложения и вовсе не работают. И хотя соотношение операций ввода/вывода может немного различаться, их вклад в задержку сервиса может быть вполне ощутимым.
Сейчас существует большое количество языков программирования для создания бэкенд-сервисов. Это вызывает интерес в сравнении их производительности по различным критериям. К примеру, сервис Benchmarks Game сравнивает языки программирования на основе того, как они решают различные задачи. А TechEmpower измеряет производительность веб-фреймворков.
Эти сервисы дают возможность примерно производительность языков. Но проблема в том, что они измеряют лишь специфичные параметры и выборки операций, которые не совсем точно показывают реальную картину.
Это сподвигло меня выяснить настоящую стоимость затрат ресурсов, необходимых для “голого” ввода/вывода на различных платформах. Измерение прокси TCP, кажется, дается проще всего. Он включает только обработку входящих и исходящих соединений, а также передачу необработанных байтовых данных.
Микросервис в любом случае не может быть быстрее прокси TCP, потому что это минимально возможный функционал. Зато микросервисы могут справляться с нагрузкой побольше. Вся остальная функциональность строится поверх основы: парсинг, валидация, обходы, сжатия данных, вычисления и так далее.
Можно сравнить следующие решения:
HAProxy
в режиме TCP-прокси. Сравнение со старой реализацией на языке С: http://www.haproxy.org/.draft-http-tunnel
— простое решение на C++ с базовой функциональностью (trantor
), запущенное в режиме TCP: https://github.com/cmello/draft-http-tunnel/.http-tunnel
— простой HTTP-туннель/TCP-прокси, написанный на Rust (tokio
) и запущенный в режиме TCP: https://github.com/xnuter/http-tunnel/.tcp-proxy
— реализация на Golang: https://github.com/jpillora/go-tcp-proxy.NetCrusher
— реализация на Java (Java NI0). Тесты проводились на JDK11 с модулем G1: https://github.com/NetCrusherOrg/NetCrusher-java/.pproxy
— решение на Python, основанное наasyncio
, запущенное в режиме TCP-прокси: https://pypi.org/project/pproxy/.
Все представленные выше решения используют неблокирующий ввод/вывод (non-blocking I/O). Если вам нужен действительно быстрый сервис с быстрым временем отклика и большой пропускной способностью, то можно воспользоваться этими прокси.
Примечание: я пытался подобрать лучшие реализации на Golang, Java и Python, однако допускаю, что могут найтись и другие решения на основе других материалов. В качестве бэкенд-сервера был выбран Nginx, который настроен на передачу 10 килобайт данных в режиме HTTP.
Результаты сравнения разделены на две группы:
- Baseline, C, C++, Rust — высокопроизводительные языки.
- Rust, Golang, Java, Python — языки с автоматическим управлением памятью.
Да, Rust есть в обоих списках.
Краткое описание методологии
- Два ядра выделены для TCP-прокси (cpuset).
- Еще два ядра выделены под бэкенд (Nginx).
- Частота запросов начинается с 10 000, а затем плавно поднимается до 25 000 запросов в секунду (ЗВС).
- Подключения будут переподключаться каждые 50 запросов (по 10 кб на запрос).
- Все измерения запущены на одной виртуальной машине для исключения любых помех в соединении.
- Виртуальная машина запущена в режиме вычислений (использует все доступные мощности процессора), чтобы избежать неточностей из-за работы фоновых программ.
- Время отклика измеряется в микросекундах.
Для сравнения использовались следующие характеристики:
- Перцентиль (от 50 до 99) — ключевая характеристика.
- Погрешность (99.9 и 99.99) критична для компонентов крупных распределенных систем.
- Максимальное время отклика (никогда не следует пренебрегать такими данными).
- Среднее усеченное значение — значение без учета 0,1% лучших или худших исходов вычисления для вычисления среднего значения (без погрешности).
- Стандартное отклонение от нормы — для расчета стабильности времени отклика.
По ссылке можно почитать о методологии и о том, почему были выбраны именно эти характеристики. Для сбора данных использовалась программа perf-gauge.
А теперь перейдем к результатам.
Сравнение высокопроизводительных языков: C, C++, Rust
Часто говорят, что Rust стоит наравне с C/C++ с точки зрения производительности. Рассмотрим, насколько именно “наравне” они находятся в плане обработки ввода/вывода. Ниже показаны четыре графика в порядке языков: точка отсчета, C, C++ и Rust.
На таблице ниже видно, сколько миллисекунд добавляется поверх основного бэкенд-запроса для каждой характеристики. Эти числа выражают средние значения интервала между максимальной частотой запросов (наращивание частоты в измерение не включено):
А вот как это выглядит в сравнении (потери пропускной способности в процентах от базовой точки отсчета):
Интересно, что прокси, написанный на C++, немного быстрее, чем HAProxy и Rust на уровне 99,9, однако медленнее на уровне 99,99 и выше. Стоит отметить, что, вероятно, это особенность простой реализации прокси, которая написана на колбэках, а не на обработке событий. Кроме того, были произведены замеры потребления памяти и мощности процессора. С ними можно ознакомиться по ссылке.
В заключение хочется сказать, что все три TCP-прокси, написанные на C, C++ и Rust, показали схожую производительность, а также плавную и стабильную работу.
Сравнение языков с автоматическим управлением памяти: Rust, Golang, Java, Python
А теперь приступим к сравнению этой выборки языков. К сожалению, решения на Java и Python не смогли справиться с 25 000 запросов в секунду всего на двух ядрах, поэтому Java была измерена на 15 000 ЗВС, а Python — на 10 000 ЗВС. Картинка ниже отражает статистику языков Rust, Golang, Java и Python.
Здесь уже видно значительную разницу. То, как волнообразно показал себя Rust в прошлом тесте, в данном случае выглядит довольно стабильно. Отдельно стоит взглянуть на пик в начале измерения при холодном запуске Java. Цифры в следующей таблице являются средними значениями интервалов на максимальной частоте запросов (повышение частоты не учитывалось).
Golang не отстает на уровнях 50 и 90, однако разница в значениях сильно растет на более высоком перцентиле, что отражается в значения отклонения от нормы. Но взгляните на значения на Java!
Стоит взглянуть на перцентили с отклонениями (99,9 и 99,99). Легко заметить, что разница с Rust просто огромна.
А вот как это выглядит в сравнении (процент от базовых значений Nginx):
В заключение можно сказать, что Rust показывает намного меньшее время отклика по сравнению с Golang, Python и, в особенности, Java. Golang соответствует производительности Rust только на уровне 50 и 90.
Максимальная пропускная способность
Есть еще один интересный вопрос: какое максимальное количество ЗВС может обработать прокси на каждом языке? Как и всегда, полные расчеты можно прочитать по ссылке, а мы перейдем к краткой выжимке.
Nginx способен обработать примерно 60 000 ЗВС. Если между клиентом и бэкендом добавить TCP-прокси, пропускная способность уменьшится. На графике видно, что C, C++, Rust и Golang развивают только 70–80% от пропускной способности Nginx, а Java и Python и того меньше.
- Синяя линия означает время отклика (левая шкала по оси Y) — чем меньше, тем лучше.
- Серые столбцы обозначают пропускную способность (правая шкала по оси Y) — чем выше, тем лучше.
Заключение
Эти измерения не являются комплексными и полноценными. Их цель — сравнение “голого” ввода/вывода на различных языках.
На основе этих тестов напрашивается вывод, что Rust может стать лучшей альтернативной по сравнению с Golang, Java или Python, если для вас важна стабильная производительность. Именно поэтому перед тем, как начинать писать программу на C или C++, следует подумать о реализации на Rust. Помимо высокой производительности на уровне C/C++ и того, что он подталкивает к созданию полного скелета программы еще до начала разработки, будут доступны и другие преимущества:
- автоматическое управление памятью;
- отсутствие data race;
- возможность писать сложный код;
- схожесть с Python;
- сообщество разработчиков и большая библиотека компонентов.
Читайте также:
- Бенчмарки в Golang: тестируем производительность кода
- Лучшие практики JavaScript — производительность
- Улучшите производительность с помощью веб-воркеров
Читайте нас в Telegram, VK и Дзен
Перевод статьи Eugene Retunsky: Benchmarking low-level I/O: C, C++, Rust, Golang, Java, Python