Комментарии 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
для подсчетов использовал :)
А что за проблема с кроскомпиляцией? Под m1 нет тулчейна?
Узким местом все равно будет база.
Гошный код не показали, бенчмарк на чужом удаленном сервере (небось еще и по wifi). Даж не знаю, что автор тут тестировал, но точно не языки
Размер бинаря же :)
"Лучше" в плане меньшего веса бинарника
Интересно на кой нужен меньший вес бинарника, с роутера кого-то дудосить или я не в теме. Просто привык иметь дело с продом в несколько десятков терабайт места на ссд, смысл мне за пару мегабайт код преписывать
Не то, чтобы нужен, но я изучаю почти параллельно несколько языков и было интересно, на сколько меньше будет бинарник на Rust. Сам Go изначально задуман для бекенда, где размер бинарника не особо важен, а я хотел маленькую утилиту.
Код в соседней репе автора, https://github.com/xeynyty/go-bench
Я и сам писал об этом:
[...а также немного сравнения характеристик двух сферических коней в вакууме.]
Этот тест совсем не показательный, просто сравнение двух моих приложений с похожей архитектурой на двух языках. Ну а главным "камнем преткновения" был размер итогового бинарника.
Простите, но это бага компилятора, а не 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 и многого не знаю, но спасибо за упоминание, поищу информацию об этом.
*ERRORS.lock().unwrap() += 1;
есть такое: https://doc.rust-lang.org/std/sync/atomic/index.html
Плюсы:Кто-то может пояснить, почему это «плюс» языка?
Отсутствие GC (Сборщика мусора)
Все просто. Сборщик мусора занимает цпу, память, лишает программиста бдительности и сам по себе предоставляет рантайм который определяет язык, его модель и опять же влияет на перфоманс. О том что он медленный и неэффективный в средних/больших проектах думаю даже и говорить не нужно.
Отсутствие гк в связке с идеей владения в контексте раста - даёт не только буст по перфомансу но так же лишает части проблем, как например гонка состояний и заставляет программиста думать над своим кодом. Понятно, что работает не всегда и не везде, но процент кода на порядок выше чем у скажем какого нибудь питона или го.
В гарантиях там есть довольно четкие грани - по поводу "гонки состояний" можно уточнить, что Rust без unsafe исключает data race, который UB, но позволяет создавать race condition, которые логические ошибки.
GC - это решение проблем с ручным управлением памятью за счет накладных расходов в рантайме. Rust тоже решает проблемы с памятью, но позволяет получить определенные гарантии статически.
Поскольку не приведен код на go, хочу уточнить у Вас один вопрос, как изменятся результаты если выключить gc.
Большое спасибо за статью! А почему Arc<Mutex<u128>> вместо atomic'а?
Спасибо за отзыв!
Я новичок в Rust и использую в первую очередь то, до чего легче всего додуматься :)
А об Arc я часто слышал в гайдах / лекциях, потому и использовал его.
На самом деле сейчас переписываю код после прочтения комментариев под статьёй и скоро изменю её, в новой, предположительно, не будет Arc совсем, просто Mutex, т.к. это глобальная переменная и нет нужны считать количество ссылок. Но пока не точно :)
Если я правильно понял по результатам, то переписывание не стоило переписывания, Go показал себя вполне достойно.
Интересно узнать сколько ушло времени и сил на отладку кода на Rust. У rust всётаки порог вхождения повыше чем в go. Соответственно и отлаживать должно быть тяжелее.
Статья немного перекликается с работой, которую я проводил месяц назад. Скажу сразу, что для меня это был рабочий проект, так что он под 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-а он стал бутылочным горлышком.
Братаны, размер, да? UPX попробуйте. В два раза ужимает!
Как я приложение с Go на Rust переписывал