Pull to refresh

Comments 56

Не хватает в бенчмарке результатов apache ab

Ну и, думаю, с прямыми руками те же самые ЯП можно ускорить ещё на десятки процентов, а то и в разы.

"люди пишут небольшие приложения на Java с итоговым весом под 100 Мбайт"

пук в лужу. или саму jvm посчитали? тогда не забудьте ещё ОС посчитать, оно же JVM запускает. в итоге чё там, 2гб против 2.1гб выйдет

"люди пишут небольшие приложения на Java с итоговым весом под 100 Мбайт"

Вроде через GraalVM можно сделать "нативный бинарник", в который нужные куски JVM будут вкомпилены / влинкованы статически. В результате будет меньше.

В самой Java опыта не имею и именно на бекенд разработку, выполнение под JVM и подобное вообще не лезу, конкретно я имел ввиду скомпилированные бинарники из Java кода. Таких на GitHub хватает.

ИМХО не должна весить небольшая консольная утилита под 100 Мбайт, это уже перебор как-то :)

>ИМХО не должна весить небольшая консольная утилита под 100 Мбайт

и зачем такие люди как ты статьи пишут? )

ls -l target/mtLoader-1.0-jar-with-dependencies.jar

3501 Mar 5 21:01 target/mtLoader-1.0-jar-with-dependencies.jar

3501 БАЙТ, Карл, 3501 БАЙТ!

внутри

` final String URL = "http://127.0.0.1:11456/tika"; HttpClient httpClient = HttpClient.newBuilder().build(); IntStream.range(1, 1_000_000).parallel().forEach(num -> {

try {

HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(URL)) .GET() .build(); httpClient.send(request, HttpResponse.BodyHandlers.discarding()); } catch (Exception ee) { ee.printStackTrace(); } }); System.out.println("used memory: " + ((Runtime.getRuntime().totalMemory()-(Runtime.getRuntime().freeMemory())) / 1048576) + " MB"); System.out.println("allocated memory: " + (Runtime.getRuntime().totalMemory() / 1048576) + " MB"); }

`

запуск с лимитом 15МБ

java -Xmx15m -jar target/mtLoader-1.0-jar-with-dependencies.jar

used memory: 8 MB
allocated memory: 16 MB

шах и мат )))

ещё можно с 2017 года резать JRE на модули, гугли jigsaw, оставлю в качестве домашнего задания. Не думаю, что до 5МБ ужмется, но блин насколько это всё далеко от цитируемого бреда.

p.s. 100МБ jar даже очень жирные сервисы не занимают, у которых внутри и spring (30МБ) с вебсервером и IOC контейнером и куча бизнес-фарша.

p.p.s. Про apache ab то что скажем? Насколько помню, он легко 50k rps выжимает на локалхосте.

Сама идея "написать свой бенчмарк" появилась, когда я ещё не нашёл другие удобные утилиты, но а после хотелось самому что-то подобное реализовать.

А почему в бенчмарке одни ошибки?

Requests:            606176
Errors:              603539
Percent of errors:   99.56%

Потому, что он ДДОСил чужой сервер, а тот его блокировал. Кстати, такой тест считается провальным.
Меня в этой истории не удивляет то, что человек просто переписал свою программу не вдаваясь в детали, почему предыдущая версия тормозила и какие у неё были боттлнеки.

Случайно минус нажал. Прошу прощения.

Ничего, я не коллекционирую плюсы-минусы :)

Про ДДОС сервера верно, httpbin мог начать отвечать ошибками при RPS от 1К.
Я переписал приложение не потому, что не хотел чинить версию на Go, а потому, что хотел написать версию на Rust для обучения. Так-то понятно, что и Go версию можно оптимизировать, но размер бинарника вряд ли получится сильно уменьшить.

И в целом, ничего против Go не имею, это сравнение не языков, а двух "сферических коней в вакууме".

Видимо, автор был уверен, что только ошибки и будут, вон, аж u128 для подсчетов использовал :)

Глянул в исходник hey, вижу там net/http, это собственно стандартная библиотека про которую писал автор

Нет, не сравнивал с другими бенчмарками совсем, ведь на соперничество с ними и не претендую, здесь сравнивал производительность именно моих решений на двух языках.

А что за проблема с кроскомпиляцией? Под m1 нет тулчейна?

С M1 не получается скомпилировать под x86 Windows или Linux. Сам особо не вникал, но ни одно решение из интернета не сработало.

Гошный код не показали, бенчмарк на чужом удаленном сервере (небось еще и по wifi). Даж не знаю, что автор тут тестировал, но точно не языки

Размер бинаря же :)

"Лучше" в плане меньшего веса бинарника

Интересно на кой нужен меньший вес бинарника, с роутера кого-то дудосить или я не в теме. Просто привык иметь дело с продом в несколько десятков терабайт места на ссд, смысл мне за пару мегабайт код преписывать

Как минимум потому что чем меньше бинарь — тем меньше вероятность выпадения «горячих участков» из кеша. Особенно в случае с GC-языками, когда сборщик может запускаться достаточно редко, чтоб быть изгнанным из кеша, но достаточно регулярно чтоб постоянно загружаться обратно.
UFO just landed and posted this here

Не то, чтобы нужен, но я изучаю почти параллельно несколько языков и было интересно, на сколько меньше будет бинарник на Rust. Сам Go изначально задуман для бекенда, где размер бинарника не особо важен, а я хотел маленькую утилиту.

Горутина на каждый реквест, действительно, куда ж все время и память уходит :)

Я и сам писал об этом:

[...а также немного сравнения характеристик двух сферических коней в вакууме.]

Этот тест совсем не показательный, просто сравнение двух моих приложений с похожей архитектурой на двух языках. Ну а главным "камнем преткновения" был размер итогового бинарника.

UFO just landed and posted this here
Если не требуется отлаживать или анализировать бинарник, то вполне годный компромисс. Или я чего-то не знаю?
UFO just landed and posted this here
Почитал статейку по ссылке: как будто go-компилятор не умел (не умеет?) правильно собрать заголовки, из-за чего strip делал как привык, а не как надо в этих странных случаях.
Простите, но это бага компилятора, а не strip. По-хорошему, об этом надо кричать в баг-трекере, потому что некорректные заголовки бинаря — это вообще как так можно накодить компилятор??
UFO just landed and posted this here
Так я тоже нигде не утверждал, будто вы сказали что это баг strip.
Я хотел сказать, что обычно люди думают, что если нечто (бинарь) работало до воздействия (strip), но перестало после — значит виновато именно воздействие (и хотя в большинстве случаев это так, в этом конкретном примере виноваты сами бинари). И из вашего
НЕЛЬЗЯ стрипать бинарники на go
можно подумать, что это именно стрипанье их ломает, хотя оно лишь обнажает изначально некорректную структуру бинарей.
А также я хотел сказать, что если корректное стрипанье показывает проблему из-за некорректности структуры (которая, вообще-то, описана задолго до go) — НУЖНО стрипать, чтобы заваливать разработчиков go баг-репортами. Если сегодня некорректная структура приводит к проблемам после strip, то кто знает, что может сломаться завтра? Стандарты и спецификации (в данном случае ELF) не просто так существуют.

Не натыкался на этот момент ранее, спасибо. Да и в целом оно не много веса отнимает, так что я его и не использовал особо, но указать стоило для понимания.

С прошлого лета конструктор Mutex может быть static, поэтому уже можно создавать глобальные переменные и обойтись без lazy static.

Стесняюсь спросить, зачем здесь асинхронщина с tokio? Если я правильно понял сценарий, то выполнить все запросы в цикле последовательно vs распараллелить никак не повлияет на результат теста, тк меряется сумма времён обработки запросов.

Tokio использовал не из необходимости, а потому, что в Go приложении во всю использовалить корутины (горутины) и здесь хотелось чего-то похожего. К тому же это максимальное простое решение проблемы для новичка вроде меня. Т.е. мы просто на кажды запрос создаём отдельную корутину, которая и занимается обработкой своего запроса.

Особой разницы не заметил, да бинарник в 3 траза больше, потребление памяти такое же, а скорость на 10% больше. Не, в качестве изучения Rust норм. Короче паритет.

Если вы считаете только max/min/average зачем вы храните массив со всеми данными (в хипе! С реалокациями!) , вместо того чтобы хранить четыре переменные u64?

Вы каждом цикле клонируете строку ( опять же - реаллокация в хипе!) вместо того чтобы завернуть строку в Arc и получить почти бесплатный clone()?

Какой рантайм токио вы использовали? Пробовали ли вы запустить это все в one thread runtime? Как писали выше у вас нет никакой конкурентности, вам не нужно запускать треды на каждом ядре.

Спасибо за дельный совет, учту. Изначально хотел просто сделать идиоматически схожее приложение с Go версией.Под "рантаймом" подразумевал, скорее, некую схожесть с удобными корутинами в Go, ведь Tokio позволяет создавать миллионы тасков (task) и работать с ними также, как и в Go, что очень легко воспринимается при переходе между языками.

Касательно

Какой рантайм токио вы использовали? Пробовали ли вы запустить это все в one thread runtime? Как писали выше у вас нет никакой конкурентности, вам не нужно запускать треды на каждом ядре.

Я, если честно, не совсем понял. Как я уже указывал в дисклеймере в начале статьи - я и сам новичёк в Rust и многого не знаю, но спасибо за упоминание, поищу информацию об этом.

Плюсы:
Отсутствие GC (Сборщика мусора)
Кто-то может пояснить, почему это «плюс» языка?

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

Отсутствие гк в связке с идеей владения в контексте раста - даёт не только буст по перфомансу но так же лишает части проблем, как например гонка состояний и заставляет программиста думать над своим кодом. Понятно, что работает не всегда и не везде, но процент кода на порядок выше чем у скажем какого нибудь питона или го.

В гарантиях там есть довольно четкие грани - по поводу "гонки состояний" можно уточнить, что Rust без unsafe исключает data race, который UB, но позволяет создавать race condition, которые логические ошибки.

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

GC - это решение проблем с ручным управлением памятью за счет накладных расходов в рантайме. Rust тоже решает проблемы с памятью, но позволяет получить определенные гарантии статически.

Поскольку не приведен код на go, хочу уточнить у Вас один вопрос, как изменятся результаты если выключить gc.

Отключать GC не пробовал, если честно. Но сам GC включается не так уж и часто, на сколько я мог заметить - само приложение обычно не живёт долго. В статье GC упоминал в целом, как особенность языка и его рантайма.

Большое спасибо за статью! А почему Arc<Mutex<u128>> вместо atomic'а?

Спасибо за отзыв!

Я новичок в Rust и использую в первую очередь то, до чего легче всего додуматься :)
А об Arc я часто слышал в гайдах / лекциях, потому и использовал его.

На самом деле сейчас переписываю код после прочтения комментариев под статьёй и скоро изменю её, в новой, предположительно, не будет Arc совсем, просто Mutex, т.к. это глобальная переменная и нет нужны считать количество ссылок. Но пока не точно :)

Если я правильно понял по результатам, то переписывание не стоило переписывания, Go показал себя вполне достойно.

Интересно узнать сколько ушло времени и сил на отладку кода на Rust. У rust всётаки порог вхождения повыше чем в go. Соответственно и отлаживать должно быть тяжелее.

Сам Rust я, на момент написания, лениво изучал уже как пару месяцев. А на само прилодежение ушло пол дня, на первую версию. В основном спотыкался об особенности языка и Tokio, как те же sleep и interval в токио, котя в Go хватало лишь метода Sleep

Статья немного перекликается с работой, которую я проводил месяц назад. Скажу сразу, что для меня это был рабочий проект, так что он под NDA и исходники не могу выложить. Но у меня тоже было сравнение Go и Rust (и другие решения включая чистый Си), но только в совершенно другом масштабе: мне нужно было нагрузить наши железки высокоуровневым (высшие слои OSI) трафиком под завязку. iperf-тесты уже были, но нам нужен был реалистичный трафик.

Мои результаты на localhost на моем рабочем linux ноутбуке (задейственны все ядра CPU, никаких мьютексов и других пожирателей CPU, статистика собиралась lock-free алгоритмами, сокеты с TCP_NODELAY):

  • Go - 400 тыс запросов секунду

  • Tokio/hyper - 300 тыс запросов в секунду (да, у меня эта пара оказалась медленнее, но у меня была немного другая методика тестирования)

  • Чистый epoll (rust и C показали примерно одинаковые результаты) - 600 тыс запросов в секунду

  • io_uring (пробовал только Rust, программировать на Си все-равно не собирался), тестировал пакеты glommio и monoio - 800 тыс. Остановился на последнем потому что есть fallback на epoll (старые linux) и kqueue (macos).

Ещё хотел бы отметить про hyper - я от него в конце отказался полностью и написал специализированный генератор и парсер HTTP, благо протокол простой. Flamegraph показал что hyper тратит слишком много времени CPU, и хотя у него могут быть более корректные по стандарту парсеры, для benchmark-а он стал бутылочным горлышком.

Sign up to leave a comment.

Articles