Как стать автором
Обновить

Комментарии 110

Срочно переписываем микросервисы на Rust (шутка)!

Думаю сервисам работающих с потоковой обработкой данных, например такими как звук и видео, стоит обратить внимание на Rust.

В остальном Go будет удобнее.

Кстати, а чем Go удобнее для Вас?

Неинопланетным синтаксисом, как минимум

Это, кажется, дело вкуса. По моим ощущениям в сравнении с C/Java/PHP оба достаточно инопланетные.

Кстати, меня удивило, что количество строк практическо одинаковое (700 и 711), при том, что Go продвигается именно под соусом "простоты".

А в чём ещё проще?

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

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

И о чём это говорит? О том, что вы лучше знакомы с Go.

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

Например так сразу и не поймёшь, что значат эти ваши лайфтаймы, turbofish, восклицательные и вопросительные знаки, если ни разу до этого Rust не видел.

Ну не знаю, у Go вполне C-подобный синтаксис, только чуть упрощён.

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

Ещё чем? Ну, простотой работы с горутинами - забываешь про треды и async как страшный сон.

Лично меня очень путала разница = / := (и ещё можно пользоваться var, а можно и нет). А читать мешает то, что тип указывается после переменной/параметра, но без двоеточия. Для меня привычно смотрятся варианты bool a (как в C/Java/PHP) и a: bool (как в Паскале/Python/TypeScript). А вот Go в обоих случаях идёт особым путём.

Число строк и простота -- это совершенно несвязанные вещи, скорее даже наоборот, код, условно, на Perl или Haskell будет короче, а на APL еще короче. И длина будет обратно пропорциональна простоте.

НЛО прилетело и опубликовало эту надпись здесь
Много кто вначале своего пути изучения раста плюётся от синтаксиса. Однако практика показывает, что уже спустя месяц неторопливого изучения языка, все эти претензии куда-то магически улетучиваются )) Мне кажется это в основном потому что синтаксис на самом деле не слишком отличается, просто визуальная составляющая обматывает.

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

всем всегда становится неочевидным*

В очередной раз попрошу предложить улучшения чтобы синтаксис был не инопланетным. Что стоит поменять, как считаете? Что конкретно инопланетное и нужно убрать?

  1. Переделать неймспейсы и вот все это с двоеточий, двойных двоеточий на точки.

  2. Унифицировать скобки (лямбды с вертикальными палками - это кто-то специально постарался).

  3. Убрать/упростить всю пунктуацию, какую можно - это не только упростит восприятие, но и уменьшит сложность топтания по клавиатуре.

Вау. Действительно инопланетянский. Хоть и красиво.

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

  2. Раст, это не про "тяп-ляп и в продакшн", это про вдумчивую расстановку скобочек, точечек, апострофов, чёрточек и двоеточий, но так, чтобы потом работало веками.

если сделать круглые скобки, будет больше путаницы

Почему другим языкам это не мешает? Насколько было действительно целесообразно вводить настолько разнообразный синтаксис?

это про вдумчивую расстановку 

Ну так вопрос был про "что упростить", чтобы быстрее решало вопросы бизнеса путем ускорения разработки и того самого тяп-ляп. Веками не нужно чтобы работало - ТЗ может меняться довольно часто и инструмент должен помогать бежать быстрее уже сейчас.

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

А, ну и я строго против замены палок для лямбд на круглые скобки!

  1. С двоеточиями проще видеть и отличать статические члены от членов переменных. В целом наверное можно было бы обойтись без них, учитывая что переменные в PascalCase именовать не принято. Окей, берем.
  2. лямбды как раз весьма удобны, позволяют писать |(x, y)| для деконструкции, например. Тут видно где аргументы а где тупл разбирается. Ну да ладно, принимаем.
  3. Можно привести пример, а то это как тот пункт ?????? пунат из саус парка. за которым следует PROFIT. А то на текущий момент кажется предлагается замена шила на мыло, которое на читаемость не повлияет примерно никак и вопрос больше вкусовщины.

Почему другим языкам это не мешает? Насколько было действительно целесообразно вводить настолько разнообразный синтаксис?

Жсу мешает, он потому заставляет расставлять скобочки где по хорошему хотелось бы их избежать. Например foos.map([x,y] => {x, y}) написать не выйдет.

Можно привести пример

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

Тут скорее набор вот таких неприятных мелочей, которые by design и не будут меняться, но они сильно портят впечатление при первом-втором-третьем знакомстве и приходится привыкать через страдания, чтобы хоть как-то двигаться вперед. Тот же голанг оказался настолько простым, я бы даже сказал деревянным, что порог входа просто нулевой. Да, горотины, каналы и все это - нужно изучать отдельно (как и вообще мультитред в принципе), но основы просты и приятны - через 3-4 дня человек садится и пишет работающий код, а не борется с компилятором со stackoveflow наперевес на каждый чих.

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

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


let mut x = ...
...
let x = x;

То поясните что вам тут не нравится и что стоило бы опять же сделать иначе в таком случае?


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

Вот у меня противоположенный опыт какой-то. Скобки/звездочки — как-то странно намешаны без особой логики, ничего не понятно.

НЛО прилетело и опубликовало эту надпись здесь

В отличие от обычных функций, список аргументов у замыканий выделяется не круглыми скобками, а вертикальными чертами:

foo().map(|x| x + 2)

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

Предоставляемые Rust возможности требуют явного синтаксического разделения статических обращений xx::yy::zz и динамических xx.yy.zz, где результат будет зависеть от значения объектов xx, yy и zzво время выполнения. Возможны ситуации, когда программист должен использовать канонический путь для обращения к методу, когда ему нужно явно указать тип или типаж, на котором будет вызван данный метод:

let foo = Foo;
foo.bar();      // вызов метода `bar` из `Foo`

Bar::bar(&foo); // вызов метода `bar` из реализации типажа `Bar` для `Foo`

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

Foo::bar(&foo); // вызов метода `bar` из `Foo` у объекта `foo`
Foo.bazz(&foo); // вызов метода `bazz` у созданного на месте объекта `Foo`

Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов". Поэтому возникает необходимость различать динамические и статические обращения: механизм их работы сильно отличается, как и последствия, к которым они приводят, так что программист должен видеть это в коде явно.

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

Ну строго говоря ничего не мешало сделать точку тогда было бы. Пример


Foo.bazz(&foo); // вызов метода `bazz` у созданного на месте объекта `Foo`

в расте не прошел бы потому что переменная не может называться Foo, а только foo или FOO. Если мы применяем дефолтные правила форматирования, конечно же.


А производить же заточку языка под маргиналов которые будут именовать как попало возможно не очень нужно.

Вот никогда-никогда запись Foo не может оказаться выражением, создающим экземпляр типа Foo без полей?

Нет, тогда было бы Foo {}

Когда проектировались неймспейсы через :: ничто не мешало просто запретить так писать и требовать {} в конце.

Это просто немного сахара закостылили для кодогенерации. И как раз с предложением выше этого бы не надо было делать — всегда была бы только одна запись struct Foo {}

Здесь принято дрочить на всё, что делают Google и Apple. Golang — это Google Oberon. Дальше надо объяснять?

Стыдно признаться, но сборкой мусора.

Горутины+каналы. Остальное либо плохо, либо вторично, но... Горутины+каналы!

В основном удобен, по сравнению с остальными:

  1. Читаемостью кода.

  2. Инфраструктурой, инструментами и библиотеками.

  3. Чаще всего код Go предсказуем, хотя не без сюрпризов. В целом runtime и устройство работы с памятью понятно.

  4. Быстротой компиляции и выполнения.

  5. Легкая подготовка docker контейнеров, благодаря одному бинарнику без зависимости от внешних библиотек. Go как будто для этого создан.

  6. Тесты - часть языка.

Не удобен:

  1. Типизированный `nil`.

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

  3. Стандартный wrap ошибок теряет тип. Из-за чего можно хранить только один тип ошибки. Suberrors будут уже тупо строками. Это приходится решать с помощью сторонних библиотек. Например: https://github.com/gjbae1212/go-wraperror

Знаете, я с этим категорически не согласен. Хотя Rust и позиционирует себя как "язык для системного программирования", на нем очень круто писать вообще что угодно.

Я пришел из мира web разработки, где я использовал php и js. Если говорить про Go и Rust, я ни один из них не использовал для коммерческой разработки, кроме простеньких лямбд на первом. Но у меня есть пара пет-проектов. Сначала я использовал Go для их реализации, но с ростом сложности кода, количества копипасты, if err != nil моя продуктивность и мотивация почти улетучились. На тот момент дженериков не было, и работать с коллекциями было отнюдь не весело. Когда я переписал проект на Rust, я заметил, что мой код стал гораздо надежнее, стабильнее и куда более управляемым. Я смог нарастить функционал проектов в разы! И это не потоковая обработка звука или видео. Это обычный телеграм-бот с кучей интеграций со сторонними веб-сервисами.

С чем я не буду спорить, так это с тем, что Rust сложный, и не такой очевидный. Особенно если брать в расчет макросы, в которых может быть скрыта вообще любая магия. Мне потребовалось где-то два месяца, чтобы набраться смелости написать первую программу. Но нормальный механизм обработки ошибок, null-safety, строгий, но справедливый компилятор, куча синтаксического сахара, итераторы и прочие плюшки очень сильно подкупают. Компилятор дает очень много гарантий. Чего не скажешь про Go, где любой указатель может оказаться nil.

Да. Типизированный `nil` это большой минус Go.

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

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

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

Проблема в том что на русском об этом статей почти нет, а на английском нужно копать. Я очень многое понял ковыряясь в исходниках std библиотек и фреймворков (grpc).

P.S. джененири к сожалению тоже не полноценные.

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

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

Оба языка очень сильно отличаются от мейнстримовых JS, Python, JVM-семейства и прочих. Но сложно отрицать, что Rust отличается от всех этих языков и Go в том числе гораздо, гораздо сильнее. Borrow Checking, Lifetimes, почти полное отсутствие рантайма и рефлексии, трейты, отсутствие сборщика мусора и прочее ярко выделяются на общем фоне и относительно сложны для понимания рядовому разработчику. Но почему-то в моем конкретном случае это никак не помешало, а возможно даже помогло полюбить этот язык всем сердцем. И я не C/C++ разработчик, который устал от Segfault'ов. А человек, который писал на PHP/JS. Наверное, о чем-то это говорит :)

> И тем не менее, я не познал дзена Go, а все больше понимал, что чего-то мне не хватает.

Да, есть такой момент — если надо из программы на Go «выжать» максимум, то надо много читать.

Обычно если надо из Go выжать максимум, к нему биндят Си-библиотеку :)

И это прям беда на самом деле.

Ну такое, сго медленный, решение про "много читать" правильнее

Зависит от случая. Например, я сейчас делаю внутренний сервис для обработки фото. И нет ничего шустрее и эффективнее libvips. А оно на C(может ++). И какие бы проблемы не доставлял биндинг, получается выгоднее.

Хотя с управлением памятью там прям беда.

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

Пока что все нативные либы для картинок работают хуже и жрут непомерно много памяти.

Я бы предложил поспорить на деньги что могу написать нужный функционал на нативке работающий лучше чем сго, но хз сколько нужно денег)

Погуглите libvips.

Боюсь, что у меня не хватит денег. Да и не факт, что получится. Все что я находил нативного отстаёт в разы как по памяти, так и по cpu и скорости в целом.

Ну и идея «просто переписать» либу с довольно большим сообществом и историей…ну хз хз

Лично мне было тяжело воспринимать данные измерений, если они были записаны без разделителей десятичных разрядов (т.е. как 210349179, а не 603,500,301), слишком уж числа длинные. А в разных местах написано то так, то эдак, что еще сильнее затрудняет восприятие.

Спасибо!

Имхо это не подделка, а редакторские правки :)

6,03500301e+8 ;)?
у физиков обычно так

А вот бы ещё теперь кто-то взялся за исходник на Rust и добавил сколько-то гвардейцев обратно...

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

Так Rust же изначально был с green threads, но в дальнейшем от этого отказались в пользу нативных потоков, т.к. green threads не всегда нужны, а это добавляет лишний runtime. Сейчас, взамен, есть крейт tokio, который можно использовать только тогда, когда это нужно.

С чего начать копания в кишках Go, чтобы при возникновении подобных вопросов понимать откуда ноги растут?

С изучения Си :) И я не шучу)

Судя по всему, таки, придется.

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


Сибираюсь "черкнуть" пару статей про обобщенные типы в Go, там этот вопрос будет рассматриваться.

Ждем! С удовольствием гляну.

Практически все тесты вида "язык X быстрее языка Y" сводятся к тому, что "одинаковые" программы на самом деле используют разные структуры данных

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

Максимум, что они проверяют - оптимизацию языком максимально наивной версии кода.

Так может в этом и суть перфоманса языка? Суметь оптимизировать наивный код среднестатистического девелопера

Это суть компилятора. А язык это просто спецификация обычно, как она реализована под капотом - это уже вторично.

Значит ли это, что для условного питона можно написать реализацию компилятора, который будет выдавать программы, сравнимые с условным rustc по производительности? Чтобы обычный наивный код с kwargs и декораторами работал также, как и обычный наивный код с traits и generics. И ещё интересно во сколько раз этот гипотетический компилятор будет сложнее условного gcc или LLVM.

Где я про такое сказал? Я просто указал что спецификация языка и её реализация - это две параллельные вещи.

Есть условный Си - есть куча разных компиляторов под его различные стандарты (C89 и далее). Есть Borland C, есть GCC, есть Clang, есть ещё бог весть что.

Если у вас в рамках спецификации получится написать быстрый питон - честь и хвала. В принципе, есть куча реализаций Питона разной степени соответствия спецификации - PyPy, Jython и прочее.

Тот же Go из сабжа изначально собирался компилятором написанным на Си (или плюсах, не помню), потом эволюционировал до самосборки. И с каждым релизом становится чуть быстрее там и сям, при этом соответствуя спецификации.

Как ни старайся, реализовать Java/Python/Ruby/JS/Go/C#/PHP без сборщика мусора не выйдет, например. Ну тупо потому что операцию создания объектов в языки завезли, а удаление нет. И это тут же немедленно накладывает ограничения на то какой может быть реализация. И в языках полно других конструкций, накладывающих ограничения на эффективность реализации (про kwargs в питоне уже сказали выше).

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

Kotlin и Swift же смогли сделать без сборщика мусора, так в чем проблема то?

Про котлин не верю, дайте пруфлинк. Про свифт - ну так он течёт на циклических ссылках, потому что вместо полноценного GC там всего лишь рефкаунтинг. И в спецификации языка прописано, что это забота программиста следить за циклами: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

Ну так "коллектор циклических ссылок" это и есть GC, со всеми его минусами.

Я и не утверждал обратного, лишь вторил по поводу Swift'а.

Здесь есть свое противоречие.

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

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

сравнивать Go и Rust в числодробильных задачах просто бессмысленно. Go не для этого создан, его компилятор не делает многие оптимизации (собственно в статье об этом написано) и заведомо проиграет LLVM.

В мое время это были "окончательные точки над i в сравнении производительности С++ и С#".

Было же время...

какой их смысл сравнивать, если go даже java медленнее? :) любая программа которая должна работать больше нескольких секунд и которая хоть как-то создаёт объекты работает медленней...

о, я смотрю пригорело :) минусы летят, а конструктивно никто поспорить не может :) факт есть факт, в go очень простой сборщик мусора, лет через 10 или 15 может будет нормальный (это если ещё в него как следует вложатся), но сегодня он в роли гири и конкурировать с хорошо оптимизированным рантаймом java не может (зачем тогда вообще сравнивать с rust?). а если уж вы решите писать программу таким образом, чтобы она генерировала минимум мусора, то и на java у вас будет производительность около нативная (наверняка и на .net будет не кардинально хуже). по этому go и попал с спец нишу сетевых программ, а не стал заменой плюсам например. прасцице если кого обидел или расстроил :)

То что гц го намного проще джавового и вероятно крутой — допустим факт. А вот то что это автоматически делает все программы на го медленными — это ваши выдумки.


И это к слову вам говорит человек который го очень… недолюбливает.

а чему ещё тормозить? есть на гите проект CardRaytracerBenchmark (кстати джавовский вариант там плохой по части IO, но не суть) можете посмотреть на разницу между платформами и языками. ну и погуглить про производительность go, тут на самом деле есть заметные проблемы, хотя похоже многие их не ожидают, ведь go компилируемый. если в таком вычислительном бенчмарке go сливает java то зачем он вообще нужен тогда? тем более что go по отзывам совсем не язык мечты...

Чему угодно. Если вы брутфосите SHA256 хэш для стркои известного размера в 50 символов то ГЦ у вас мешать не будет совершенно, к примеру.

:| какой смысл приводить крайние примеры? что они опровергают?

У кого крайние, а у кого жизненные. Мы тут на 20% снизили общее время работы программы раставив 3 "лишних" ифа с проверкой размеров массивов в паре мест. Бывает и такое.

Можно просто _ = slice[тут самый большой элемент массива который будет использован] ( конечно если есть увереность что длина именно та). В стандартной библиотеке этот костыль в десятках мест, особенно в crypto.

Ну либо просто в начале функции l := len(slice) и по нему играть, компилятору этого достаточно чтобы не подставлять везде проверку.

там бывает хитрее, например мне каком-то месте пришлось писать условие
if i <= len - 1 && i + 1 <= len { ... }. Если любую часть условия убрать то у компилятора там эвристика ломалась и он хреново генерировал код.

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

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

Дефолтный G1 GC ориентируются на stw паузы в 200мс, и он реально так и работает. С этим надо конкурировать? Да благодаря пинку от go, как раз появились шенандоа и zgc.

ну раз про паузы знаете, то должны знать, что кроме них есть и общий оверхед работы gc. помню давно кто-то пытался рекламировать go мол в нём паузы меньше, но про оверхед умолчал :) так что тут есть свой trade off и очевидно, что расплата за паузы растёт нелинейно (если вы не используете какие-то другие хитрости). если видели, то я выше упоминал CardRaytracerBenchmark, в нём вычислительная программа в которой есть классы типа вектор и т.д.. вариант с go работает медленней чем java, в том числе благодаря тому, что java умеет автоматом избавляться от создания объекта в heap. Но в целом пришлось гуглить и выяснилось, что gc go это не его сильная сторона и по-моему это существенный фактор при выборе на чём делать.

если честно первый раз слышу, что это go пинал разработчиков jvm :) sun java это не первая джвава, которую купил оракл, до этого была и другая компания, которая делала свою jvm и у них на сколько помню паузы были в районе 70 ms, ну и azul свой gc ещё лет 10 назад представил у которого по сути задержек нет, только спец требования к оборудованию и размеру хипа. так что вряд ли тут на go кто-то смотрел, правда каким образом там происходит сборка я не знаю

я выше упоминал CardRaytracerBenchmark

Посмотрел эти бенчмарки и решил немного покрасноглазить.

  1. В Go реализации писать в stdout без буфера, а в java с буфером - грязный ход. И зачем в цикле что-то в output выводить, там IO мерят что ли?

  2. В скриптах run.sh в java реализации весь вывод идет в /dev/null, в гошном run.sh такого нет.

  3. Очень много лишний лишних объектов просто ради объектов (тестировали аллокатор?). Раз уж речь про GC, то ради интереса снял профиль CPU у Go приложения, так там GC даже в TOP 20 функций нет

Очередные бенчмарки ни о чем. Кстати, на моей машине гошная реализация с /dev/null выдала меньше времени, чем пару раз прогнанная джавовая. Если без изменений запускать run.sh, то да, java c /dev/null быстрее чем Go с терминальным stdout))

И стоит упомянуть, что java-код явно писал джавист. Про Go реализацию я бы так не сказал, иначе хотя бы не копировались структуры в функции\методы зазря, а передавались по ссылке

там же итоговый файл, на сколько помню, получается в пару мегабайт, хотите сказать, что на ваш диск он записывается секунд 5-10? то что вы пишете насчёт /dev/null не очень правдоподобно короче :)

"в stdout без буфера" поинт может быть валидный, на сколько помню в java там тоже были проблемы, миллион лишних отдельных системных вызовов тест искажают сильно.

в принципе вы можете попробовать исправить и сделать пул. ну или закинуть отдельно исправленный файл, а я ради интереса прогоню у себя.

"Очень много лишний лишних объектов просто ради объектов" например? в первую очередь программа пишется ради читаемости остальными, в java там тоже объекты vector во всю в коде плодятся и с ними код на много лучше чем к примеру массивами и отдельными методами. А если так повыдёргивать базовые вещи, то смысл тогда в java или go?

Подскажите, а есть ли какие-то ограничения на бенчмарк? Например, можно ли распараллелить? Можно ли вставить, например, sync.Pool ради переиспользования объектов?

я видел, что люди делали несколько версий: однопоточную и многопоточную. на java есть версии с graal native (предкомпиляцию всей программы в нативный код) и запуск без gc вообще (сколько там ело памяти я хз), так что варианты могут быть разные. на си делали ассемблерные вставки чтобы дёрнуть векторные инструкции. можете попробовать прибить костыли с sync.Pool (не гошник, не знаю что это такое). я думаю можно подправить и "ванильную" версию, если там проблемы с буферизацией вывода например, а остальные эксперименты отдельным новым проектом лучше.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

(А также @PsyHaSTe) Так, ну, я немного покопался в коде и вот что понял:

data Triplet = Triplet !Double !Double !Double deriving Show

getNextTriplet :: Triplet -> Triplet
getNextTriplet (Triplet a b c)
  | abs app <= 1.0 = Triplet b c app
  | otherwise = Triplet b c (1.0 / app)
  where
    app = a + b - c

iterateOver' :: Triplet -> Int -> Triplet
iterateOver' trip n = foldl' (\t _ -> getNextTriplet t) trip [1..n]

Наверняка тут можно уйти в unboxed types...

Примечание

В оригинальном коде, конечно, чуть сложнее (если текущий и следующий Triplets очень схожи, то об этом будет написано в консоли и всё (то есть можно брать iterateOver' и не париться):

(~=) :: Double -> Double -> Bool
a ~= b = abs (a - b) < 1e-14

isConvergent :: Triplet -> Triplet -> Bool
isConvergent (Triplet a b c) (Triplet x y z)
  = (a ~= x) && (b ~= y) && (c ~= z)

iterateOver :: Triplet -> Int -> (Triplet, Maybe (Int, Double))
iterateOver triplet cycles
  = foldl' f (triplet, Nothing) [1..cycles]
  where
    f (trip, r) n = (nextTrip, nextR)
      where
        nextTrip = getNextTriplet trip
        Triplet a b c = nextTrip
        nextR = if isConvergent trip nextTrip then r <|> Just (n, b) else r

Далее поговорим про конкурентность. Вообще там много кода, направленного на создание собственного бенчмарка. Суть в следующем: есть series_size, который характеризует, как много таких вот iterateOver' будет запущенно параллельно. Далее для каждого r \in [1; \text{tasks_max}]делаем следующее: запускаем \lceil \frac{r}{\text{series_size}}\rceilраз в series_size параллельных потоков iterateOver' с рандомно сгенерированным Triplet и фиксированным n. Ну и собираем какие-то метрики, которые агрегируем и выводим на экран.

Кстати, в коде, судя по всему, есть логическая ошибка. Допустим, tasks_max = 10, а series_size = 3. Тогда для r = 1 лишь один раз запустится iterateOver', хотя мы, вроде как, тестируем конкурентность и iterateOver' должен конкурентно запуститься вseries_size = 3 потоках. Ну, так же и для всех r, не кратных series_size.

Пояснение к предыдущему абзацу

Перепишем вот этот код так, чтобы он просто печатал нам, какая task в какой серии запускается.

Получится

package main

import "fmt"

func count_series(n_tasks, series_size int) int {
	n_series := n_tasks / series_size

	if series_size*n_series < n_tasks {
		n_series++
	}

	return n_series
}

func main() {
	n_tasks := 10
	series_size := 3
	n_series := count_series(n_tasks, series_size)
	task_idx := 0

	for series_idx := 0; series_idx < n_series; series_idx++ {
		count_tasks_series := 0
		for task_idx < n_tasks && count_tasks_series < series_size {
			fmt.Printf("Running task # %d in series # %d\n", task_idx, series_idx)
			count_tasks_series++
			task_idx++
		}
	}
}

Ну и что мы получим?

Running task # 0 in series # 0
Running task # 1 in series # 0
Running task # 2 in series # 0
Running task # 3 in series # 1
Running task # 4 in series # 1
Running task # 5 in series # 1
Running task # 6 in series # 2
Running task # 7 in series # 2
Running task # 8 in series # 2
Running task # 9 in series # 3

То есть таски 0..2, 3..5, 6..8 реально запускаются по-нормальному так, что параллельно будут выполняться ровно три (series_size) функции. А вот таска # 9 будет выполняться одна. Очевидно, одна параллельная задача выполнится быстрее трёх параллельных, так что это будет влиять на результаты бенчмарка.

Да и в чём смысл сравнивать горутины с каким-то там crossbeam в Rust, я не знаю. Ведь горутины нужны не для того, чтобы дробить числа, а для IO.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Посмотрел код, плюсую товарища 0xd34df00d выше, ничего не понял. Какие-то кроссбимы, мутируют при этом шаренные переменные...


Вопрос, если в расте в томл дописать:


[profile.release]
lto = true
codegen-units = 1

И запускать как RUSTFLAGS="-C target-cpu=native" cargo run --release s
то результаты станут лучше? У меня просто мак с М1, и компиляция всегда идет под текущий проц. А на х86 может быть приличная разница.

Взял похожий процессор i5-3470@3.2 (как я понял, у автора i5-3570@3.4).


  • запуск как у автора: 597,728,631
  • изменён Cargo.toml: 597,728,631
  • изменён Cargo.toml + target-cpu=native: 690,607,734

Выигрыш виден, но Ivy Bridge — довольно старая архитектура (~10 лет), поэтому полагаю что на более современных процессорах разница между target-cpu=native\generic будет ещё больше.


Ещё я решил посмотреть, что будет если вместо target-cpu=native попробовать включить оптимизации с упором на компактность бинарника, а не производительность. Получилось следующее: скорость работы снижается существенно, а вот размер — не очень.


  • s: 143,389,733 (2635K)
  • z: 143,410,296 (2665K)
  • 3: 597,728,631 (2692K)

Результаты размера без strip.

На самом деле, не такая уж приличная разница (что подтверждается другими бенчмарками, которые я курирую). Мои данные:

Современная архитектура: Intel Xeon E-2324G

Go (go3, go run concgo.go s): 748,502,994
Rust (rust2, cargo run --release s, запуск как у автора): 941,619,585
Rust (rust2, cargo run --release s, изменён Cargo.toml): 939,849,624
Rust (rust2, RUSTFLAGS="-C target-cpu=native" cargo run --release s, Cargo.toml + target-cpu=native): 965,250,965

В данном случае, lto дает даже чуть худшие результаты, но это прогрешности, и я никогда не видел, чтобы были реально лучше результаты (использование cpu=native совершенно другая история, и это реально может давать улучшения). Насколько я видел, LTO имеет смысл когда линкуются много файлов, и для этого данного теста это не особо важно.

Лто там так, до кучи, прост на всякий случай. Меньший бинарник должен давать лушее кеширование инструкций, в теории. НА практике разница и правда невелика.

По поводу кода, написанном на Golang: я попробовал написать пару версий.

  1. Тот же вариант, что и описан в оригинальной статье. type SickTriplet = [3]float64

  2. Вариант, использующий type Triplet struct { a, b, c float64 }, но создающий новую копию при каждой итерации

  3. То же, что и (2), но только данные мутируются на месте.

Код
package triplet 

import (
	"math/rand"
	"math"
)

type SickTriplet = [3]float64

func randomSickTriplet() *SickTriplet {
	return &SickTriplet{rand.Float64(), rand.Float64(), rand.Float64()}
}

func getNextSickTriplet(trip *SickTriplet) *SickTriplet {
	app := trip[0] + trip[1] - trip[2]
	if math.Abs(app) <= 1.0 {
		return &SickTriplet{trip[1], trip[2], app}
	} else {
		return &SickTriplet{trip[1], trip[2], 1.0 / app}
	}
}

func calcSickTriplet(trip *SickTriplet, num int) *SickTriplet {
	for i := 0; i < num; i++ {
		trip = getNextSickTriplet(trip)
	}
	return trip
}

func fastCalcSickTriplet(trip *SickTriplet, num int) *SickTriplet {
	for i := 0; i < num; i++ {
		app := trip[0] + trip[1] - trip[2]
		trip[0] = trip[1]
		trip[1] = trip[2]
		if math.Abs(app) <= 1.0 {
			trip[2] = app
		} else {
			trip[2] = 1.0 / app	
		}
	}
	return trip
}

type Triplet struct { a, b, c float64 }

func randomTriplet() *Triplet {
	return &Triplet{rand.Float64(), rand.Float64(), rand.Float64()}
}

func fastCalcTriplet(trip *Triplet, num int) *Triplet {
	for i := 0; i < num; i++ {
		app := trip.a + trip.b - trip.c
		trip.a = trip.b
		trip.b = trip.c
		if math.Abs(app) <= 1.0 {
			trip.c = app
		} else {
			trip.c = 1.0 / app	
		}
	}
	return trip
}

func getNextTriplet(trip *Triplet) *Triplet {
	app := trip.a + trip.b - trip.c
	if math.Abs(app) <= 1.0 {
		return &Triplet{trip.b, trip.c, app}
	} else {
		return &Triplet{trip.b, trip.c, 1.0 / app}
	}
}


func slowCalcTriplet(trip *Triplet, num int) *Triplet {
	for i := 0; i < num; i++ {
		trip = getNextTriplet(trip)
	}
	return trip
}

Сам бенчмарк:

package triplet

import "testing"


const TIMES = 1000000


func BenchmarkFastCalcTriplet(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fastCalcTriplet(randomTriplet(), TIMES)
	}
}

func BenchmarkSlowCalcTriplet(b *testing.B) {
	for i := 0; i < b.N; i++ {
		slowCalcTriplet(randomTriplet(), TIMES)
	}
}

func BenchmarkCalcSickTriplet(b *testing.B) {
	for i := 0; i < b.N; i++ {
		calcSickTriplet(randomSickTriplet(), TIMES)
	}
}

func BenchmarkFastCalcSickTriplet(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fastCalcSickTriplet(randomSickTriplet(), TIMES)
	}
}

Результаты такие:

BenchmarkFastCalcTriplet-4       	     548	   2177936 ns/op	      24 B/op	       1 allocs/op
BenchmarkSlowCalcTriplet-4       	      62	  18613275 ns/op	24000044 B/op	 1000001 allocs/op
BenchmarkCalcSickTriplet-4       	      62	  18929575 ns/op	24000036 B/op	 1000001 allocs/op
BenchmarkFastCalcSickTriplet-4   	     534	   2225462 ns/op	      24 B/op	       1 allocs/op

Получается, выбор структуры данных не особо и влияет?

В golang gcflags="-l=4" включит middle stack inline

Интересный вариант. Попробовал так:


go run -gcflags="-l=4 -m -m" concgo.go s > o 2>&1


  • На результаты не повлияло
  • В o пишут:
    .\concgo.go:60:6: cannot inline is_convergent: function too complex: cost 87 exceeds budget 80
  • Т.е. таки проблема с is_convergent не во "вложенности", а в "сложности"?
  • В исходниках пишут:
    making 1 the default and -l disable. Additional levels (beyond -l) may be buggy and
    // are not supported.
  • Вроде нет указаний, что -l=4 можно использовать "в бою"?

А можно было ли попробовать этот лимит 80 снять через исходники?)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории