Комментарии 281
Да но в расте это не включено везде, потому что llvm ломается
А что ещё осталось включить?
Вроде уже зачинили, я так понимаю ждут пока в мастер вольется чтобы обратно по-дефолту поставить.
Можно и щас включить через флаг, но на свой страх и риск.
Я склонен верить, что в большом количестве сценариев у Rust есть преимущество в плане возможности проведения различных оптимизаций, потому что это memory-safe язык
безопасность памяти ортогональна производительности. Ну, за исключением того факта, что безопасное подмножество раста попросту не позволит делать некоторые оптимизации.
Простейший пример:
foo(int& x, int& y, int[] z) {
...
x = y;
...
}
Компилятор C++ обязан в этом месте читать из памяти и писать в память — потому что он не знает объекты
x
и y
— это один объект или это разные объекты… а может один из них — это ещё и элемент массива?В Rust вся эта информация доступна не только программисту, но и ком пилятору и он может её использовать… однако пока что он использует её плохо, да ещё иногда и генерит нееправильный код — потому в стабильной ветке Rust подобные оптимизации отключены.
Используйте в С/С++ restrict и будет вам счастье.
Как было сказано выше, в гцц и ллвм есть баги с restrict: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87609 и https://bugs.llvm.org/show_bug.cgi?id=39282. Нашли баги из-за раста, в котором этот restrict раньше подставлялся автоматически для каждой &mut
ссылки. Сейчас из-за баги не подставляется, ждут фиксов в ллвм, чтобы потом опять включить эту оптимизацию.
А вы дальше советуйте restrict
. Пусть люди стреляют себе в ногу.
Хотя немножечко неточно — тесты не при прочих равных, т.к g++ != LLVM, надо было сравнивать одинаковый тулчейн.
Позиция одного из ключевых разработчиков раста (и автора The Rust Programming Language) — в расте статический GC.
Потому что языки, «разрывающие циклы» — платят за это высокую цену. Как мне кажется… слишком высокую цену.
Потому что в таких языках вы не знаете когда и как вы будете «платить» за выделение и освобождение памяти, можете только догадываться — опираясь на конкретную реализацию GC конкретно вот в конкретной версии вашего runtime. А завтра — всё может быть совсем по-другому.
О том что это важно можно заметить по количеству статей, посвящённой этому вопросу на Хабре (да и на любом сайте где обсуждается C#, Java, JavaScript и тому подобные языки). Рано или поздно об этом приходится задумываться всем, кто на этих языках работает.
А вот при обсуждении C++ тонкости общения с jemalloc'ом или tcmalloc'ом итересуют очень малый процент разработчиков… потому что если вы не пытаетесь «разрывать циклы», то вы всё ещё не можете сказать точно какую цену вы заплатите за выделение и освобождение памяти… зато можете сказать точно — когда эта цена будет нулевой… и оказывается что для 99.99% случаев на практике — этого достаточно.
Речь про циклические зависимости. Например:
void Foo()
{
var a1 = new A();
var a2 = new A();
a1.Next = a2;
a2.Next = a1;
}
В С++ подобный код вызовет утечку памяти (если использовать рефкаунтинг), в расте оно в таком виде не скомпилится, а при попытке переписать на рефкаунтинг будет поведение как в плюсах, а в языках с трассирующим гц он спокойно обнаружит цикл и удалит оба объекта.
То есть это правильное использование рефкаунтинга? Почему тогда вот этот код падает? https://repl.it/@Pzixel/AcceptableInsidiousUsernames
Я проверил верно исправленный — не падает.
Посмеялся, глядя на процесс правок. Нет чтобы учебники читать, всё критикуют…
cout << (x->Next).lock()->Value;
так нельзя, lock() может вернуть nullptrСпасибо, поправил.
Основная идея тут не в том, что я не умею проверять что weak_ptr.lock()
вернул нулл, а в том, что структура данных именно что должна владеть данными, а не просто ссылаться. В некоторых случаях типа деревьев weak_ptr может помочь, но и в расте точно так же получится Weak использовать.
Речь про полноценный ГЦ была именно в том что разработчику там не надо забивать себе голову — он всегда делает сильную ссылку, а с циклом разберется рантайм. И это действительно помогает. А еще это помогает делать структуры вроде "массив и ссылка на первый элемент". Self-referential structures огромная морока в расте, для которой целый Pin изобрели, в плюсах я не слышал ни про какое известное решение, а в языках с ГЦ это вообще не проблема.
так нельзя, lock() может вернуть nullptr
Ну вот, так и знал что мне про это напишут...
P.S. Честно говоря, рефкаунтинг сам по себе и не способен бороться с циклическими зависимостями. Поэтому в питончике вдобавок к рефкаунтингу добавили (отключаемый) gc.
Поэтому в питончике вдобавок к рефкаунтингу добавили (отключаемый) gc.В третьей версии вроде как уже неотключаемый.
Но споры на тему «а насколько это нужно» будут вечными. Потому что тут речь же не о теоретической оценке чего либо, а о том «насколько сложно руками разрывать циклы» в сравнении с «насколько сложно бороться с GC, который, внезапно, начинает собирать мусор тогда, когда нам это невыгодно».
С учётом того, что одна из самых популярных в мире платформ (iOS) обходится без «полноценного GC»… похоже что Rust идёт в верном направлении…
Язык с ручным управлением памятью быстрее, чем язык с автоматическим при прочих равных. Ваш К.О.Вот только прочие — неравные. Ваш К.О.
Собственно многие программы до сих пор написаны на Fortran, потому что аналогичные же программы на C++ — медленнее. А медленнее они как раз потому что «Язык с ручным управлением памятью» — не всегда быстрее.
Разработчикам компиляторов C++ проблемы алиасинга реально часто мешают порождать оптимальный код. И у Rust'а тут вполне себе есть преимущество.
P.S. И да: тут как раз вопрос лежит в практической плоскости. Теоретически любую программу на Fortran вдумчивой работой с типами и
restrict
можно довести до более быстрой программы на C++. А вот практически — у реально существующих разработчиков — это не выходит… Такие дела.И кстати, я немного отстал от жизни и не помню наличия работы с хипом в Фортране.
Собственно тезис строго наоборот.
unique_ptr — вовсе не подсчет ссылок. shared_ptr — это подсчет ссылок. Но в хорошей кодовой базе его почти не должно быть. Он в идеале нужен только для shared ownership (внезапно), а оно нужно очень редко.
К слову, обычно буква a
в arc
означает atomic
shared_ptr не атомарный счетчик ссылок
В блоке управления он вполне себе атомарный
Note that the control block of a shared_ptr is thread-safe: different std::shared_ptr objects can be accessed using mutable operations, such as operator= or reset, simultaneously by multiple threads, even when these instances are copies, and share the same control block internally.
Советую читать Мейерса
*За исключением юзкейса, когда два потока пытается без синхронизации менять один и тот же инстанс shared_ptr. Тогда реальное число инстансов может рассинхронизироваться со значениями счетчиков. Для этого существует набор атомарных операций над shared_ptr, а также в стандарте будет std::atomic_shared_ptr.
Советую читать МейерсаХороший совет — он в книжке как раз обо всём этом рассказывал.
Компилятору очень мало чего о семантике всего этого известно.
В Rust же все ограничения заложены в язык, попадают в IR и далее в оптимизатор… где они, в настоящее время, особо не используются, так как оптимизатор заточен, в первую очередь, под C/C++.
Так что насчет неверного утверждения про Фортран? Пока что оно подтверждает моё
Пока что оно подтверждает моёНе очень понимаю о чём вы там говорите.
Так что насчет неверного утверждения про Фортран?А что там с фортраном не так? SciPy уже без него собиратеся? Нет? Ну значит пока — реально существующий код написанный на Fortran всё ещё быстрее реально существующего кода, написаного на C++.
То что в Fortran «сливает» в Benchmak Game — это нормально: это достаточно ограниченный язык, некоторые алгоритмы на него ложатся плохо. Однако в своей области компетенции — он быстрейший… о чём, собственно, и речь.
Rust претендует на то, что будет быстрее C/C++, но при этом ещё и всегда, а не только при работе с матрицами, как Fortran… поживём, увидим.
Пойнт был в том, что стандартные либы точно также можно проверить, как и язык, как минимум не сильно лучше.
Поскольку мы обсуждаем скорость, то это делает все синтаксические конструкции, предоставляемые стандартной библиотекой бессмысленными с точки зрения оптимизаций.
С точки зрения компилятора C++ — это язык с ручным управлением памяти. А Fortran, опять-таки, с точки зрения компилятора — язык с ручным управлением памяти (то, что вы назвали «отсутствие работы с хипом», как раз).
Я предпочитаю иметь свою т.зр. в своих программах:
1. Использую new/delete — ручное управление
2. Использую unique_ptr — RAII
3. Выбираю shared/weak_ptr — ARC
4. Подключаю boehm — оппа — программа с GC
5. Беру в расход только автоматическую и статическую память — вообще красота — никаких нежданок, никакого управления…
Ну да, конечно — это мое ручное управление всей подсистемой памяти, компилятор то не знает =)
Но в современном C++11 и новее рекомендуется таки пп2,3 — которые относятся к автоматическим
Ну да, конечно — это мое ручное управление всей подсистемой памяти, компилятор то не знает =)Именно так. Это не смешно, а довольно грустно. Например потому, что любая манипуляция с память через указатель на
char
может, потенциально, изменить значение любой переменной, адрес которой куда-то, когда-то, хоть раз передавался.При этом «стрелять» это может самым неожиданным способом. Сравните, например, foo и bar — и подумайте почему случае с
bar
компилятор смог показать всю свою изворотливость (и развёрнутый цикл и векторизация и вообще всё круто), а вот foo — осталась, фактически, неоптимизированной.Но в современном C++11 и новее рекомендуется таки пп2,3 — которые относятся к автоматическимДа — это облегчает работу программисту, но… увы и ах — не компилятору. Более того — эти все чудесные обёртки в системной библиотке зачастую усложняют работу компилятору… ну вот простейший (хотя и довольно-таки патологический) пример… внушает, да?
Именно за счёт этого языки с автоматическим управлением памяти (не путать с трассирующим GC) могут иногда выигрывать у C/C++… а иногда — и довольно-таки заметно выигрывать…
При этом «стрелять» это может самым неожиданным способом. Сравните, например, foo и bar — и подумайте почему случае с bar компилятор смог показать всю свою изворотливость (и развёрнутый цикл и векторизация и вообще всё круто), а вот foo — осталась, фактически, неоптимизированной.
Компилятору можно и подсказать. Ну да, char* в C/C++ — это «особенный» тип, ничего не попишешь. Но средствА есть.
Так что если Rust будет показывать сравнимую производительность на идеоматичном коде без специальных забот обо всех этих тонкостях (типа «разворачивания»
std::unique_ptr
для возврата значения из функции и заворачивания его обратно в std::unique_ptr
в месте получения) — то это будет разумный довод в пользу перехода.Потому что сейчас ситация такая: на C++ можно сделать так, чтобы твоя программа была максимально быстрой… но умеют делать это один человек из ста — и то требуется много возни и чуть ли не изучение каждой функции в дизассемблере, чтобы не перепутать куда какие костылики и как вбивать… неудобно.
unique_ptr
, увы, зашит.А поскольку это затрагивает «священную корову» — обратную совместимость — то шансов на исправление, мягко говоря, немного.
типа «разворачивания» std::unique_ptr для возврата значения из функции и заворачивания его обратно в std::unique_ptr в месте полученияABI в обе стороны «играет» — сегодня на коне раст, а завтра плюсы. Хотя казалось бы… А еще ABI не имеет значения при инлайнинге функций
Потому что сейчас ситация такая: на C++ можно сделать так, чтобы твоя программа была максимально быстрой… но умеют делать это один человек из стасобственно точно так же и в расте и примерно любом другом ЯП.
ABI в обе стороны «играет» — сегодня на коне раст, а завтра плюсы. Хотя казалось бы…А что именно «казалось бы»?
Rust имеено потому и не имеет аналога Itanium C++ ABI, что не хочет, чтобы «замороженный» раз и навсегда ABI приводил к тому, что подобные вещи нельзя было бы исправить.
А вот у C++ — приоритеты другие.
Потому «на дальней дистанции» Rust должен бы иметь преимущество.
собственно точно так же и в расте и примерно любом другом ЯП.Ну вот эти «игры» и помогают понять насколько нужно отказываться от «типичных идиом» ради скорости.
Core Guidelines прямо говорит: используйте
unique_ptr<T>
и создавайте эти переменные с помощью make_unique
… но на самом-то деле нужно использовать new и возвращать gsl::owner
, если мы хотим «эффективности как в C»!Вот и интересно понять — насколько реально быстрый код на Rust отличается от «идеоматичного», «рекомендуемого»…
Ну на нашей практике максимально тупой идиоматичный код выдал практически максимальный перфоманс, судя по профайлеру и загрузке процессора. Еще в 2 раза выжали, пройдясь vtune'ом и расставив стратегические префетчи где надо, убрав работу с памятью.
И судя по всему, это не у нас одних.
Говорить что "думать вообще не надо" не буду, ограничусь "идиоматичный код получается весьма быстрым, иногда быстрее оптимизированного си".
А что именно «казалось бы»?а вы глянули в мои примеры? Изменение кажется незначительным, но переворачивает всё с ног на голову.
Rust имеено потому и не имеет аналога Itanium C++ ABI, что не хочет, чтобы «замороженный» раз и навсегда ABI приводил к тому, что подобные вещи нельзя было бы исправить.
…
Потому «на дальней дистанции» Rust должен бы иметь преимущество.
пока rust не стабилизирует ABI, ни о какой «дистанции» речи и быть не может. А дальше будет иметь значение компромиссы какого из ABI лучше применимо к конкретным приложениям.
пока rust не стабилизирует ABIА почему вы считаете, что он это, вообще, должен делать?
Движение вообще в другую сторону идёт: clang умеет отказываться от ABI для
static
функций, к пример, чтобы быстрее было.Это и gcc/msvc уже очень давно делает, и не только для статик, но и для всех при lto/pgo. ABI он наружу, чтобы статические либы и dll\so не протухали каждые несколько месяцев.
ABI он наружу, чтобы статические либы и dll\so не протухали каждые несколько месяцев.Тем не менее C++14 ABI и C++17 ABI несовместим, так что с некоторой частотой это всё равно происходит.
Я уже не говорю про разные компиляторы.
В общем в отношении Rust'а моё отношение всё ещё немного скептическое… но уже совсем немного…
Ну это да. И это порождает определенные проблемы, из-за этого даже вводят всякие _GLIBCXX_USE_CXX11_ABI, и оно даже реально пригождается в реальной жизни. И вот представим что на это забьют и это все станет регулярным. Раз в 3 месяца пересобираем весь мир, к примеру (а проблем с пересборкой конечно же ни у кого не возникнет). А проприетарщина вообще, кому она нужна? Давайте исходники, либы не принимаем.
А почему вы считаете, что он это, вообще, должен делать?представьте, что вы хотите поставлять вашу библиотеку без исходников — совершенно адекватный коммерческий юзкейс. И вам наверно захочется чтобы эту библиотеку можно было использовать не только из-под ubuntu 18.1.2 и rustc 1.39.16 на компьютерах с intel i7 8-ого поколения, которым это всё барахло компилируется у вас. Точнее, компилировалось у вас вчера. А сегодня вы обновили компилятор до rustc 1.40.1 (nightly) чтобы посмотреть очередную классную фичу, а еще обновили librustrt.so, и отныне всем вашим клиентам нужно будет воспроизвести эти шаги чтобы работать с вашими новыми библиотеками.
представьте, что вы хотите поставлять вашу библиотеку без исходников — совершенно адекватный коммерческий юзкейс.Прикрутите к ней API на C — и поставляйте.
Попытки «заморозить» C++ ABI показали, что пользоваться этим всё равно умеют единицы, а проблемы это вызывает у всех.
То-есть пользоваться растом исключительно через другой язык? Где таки постарались и нарушения ABI редки?
И чем умеют пользоваться, и какие проблемы у всех возникают? C++ ABI умеют пользоваться единицы и у всех с ним проблемы возникают?
Ну так а вы предлагаете еще чаще эти проблемы сделать, а пользоваться умеют все — скомпилил и какой-то ABI получил. И его желательно не замечать, только чинить если у кого-то где-то сломается, а не ломать все время. Вот чинить да, может и единицы умеют. Посмотрите как это в firefox решается для stdc++, и это при редких изменениях, а при нестабильном — будет вообще кошмар.
Ломать его, конечно, приемлимо в некоторых случаях, например если ни либы ни dll\so не нужны. Но в остальном — чем реже тем лучше.
Где таки постарались и нарушения ABI редки?Проблема не в том, что «там постарались и нарушения ABI редки», а в том, что если вы делаете «широкий», «быстрый» интерфейс, то сделать его эффективно — очень тяжело. А если у вас там сплошные «хенлы» и внутренняя реализация скрыта — то ничего, кроме того, что предоставляет C — не нужно.
Посмотрите как это в firefox решается для stdc++, и это при редких изменениях, а при нестабильном — будет вообще кошмарТри или четыре раза её меняли полностью несовместимым образом. Несколько раз обнаруживали несовместимость и чнили. Заметьте: libstd++ — это библиотека, разработчики которой прилагают титанические усилия для поддержания совместимости — и всё равно, как вы говорите: это проблема.
Но в остальном — чем реже тем лучше.Это теория. А на практике — libicu имеет 60 несовместымых версий. Boost — сравнимое число. И это ведь — «золотой стандарт», библиотеки, которыми пользуются огромное количество народу! А в более мелких народ вообще не задумывается о том, чтобы была хоть какая-то совместимость.
Если требуется совместимость — то C++ интерфейс всё равно заворачивается в C интерфейс (как в libandroidicu) и потом «с другой стороны» на него снова вешается C++ обёртка.
Ну и? Нафига козе баян, все сложности поддержания стабильного ABI — если люди всё равно этим не пользуются? Не проще ли признать, что мы не умеем делать быстрые и стабильные интерфейсы и просто дать возможность разработчикам выбирать?
Все так: glibc — мало проблем, libstd++ — больше, Boost — еще больше; libicu — не сталкивался. Но это не значит что им не пользуются.
У steam runtime, например, достаточно стабильное API/ABI, в пределах дистра благодаря этому куча кода шарится, даже у flatpak/snap идут ссылки на свои core для этих целей. Есть куча более стабильных библиотек что таких проблем не вызывают. И это будет еще только больше проблем если весь этот мир пересобирать раз в несколько месяцев.
Что вы предлагаете — по сути вообще отказаться от ABI, ну так пожалуйста, линкуйте с lto — нужен будет компилер именно той версии, вот оно полностью сломанное ABI уже есть. Это же не значит что нет случаев, когда надо компоненты делать совместимыми, и хочется чтобы эта система пожила подольше и требовала меньше поддержки.
К слову про производительность. Все последние изменения в stdc++ abi они из-за фич языка, не из-за производительности. Производительность да, это хорошо, и над этим думать надо. Но это редко надо, бесконечность итераций тут не надо для одной архитектуры для стабилизации.
Все последние изменения в stdc++ abi они из-за фич языка, не из-за производительности.Потому что изменения для увеличения производительности потребуют изменить ABI — а он священен.
Но это редко надоТам где это не нужно — есть Java, Python… и C интерфейс.
У steam runtime, например, достаточно стабильное API/ABI, в пределах дистра благодаря этому куча кода шарится, даже у flatpak/snap идут ссылки на свои core для этих целей.Если вы всё собираете одним компилятором из неизменных исходников — то не вижу проблем.
Проблема тут следующая: Я не собираю игру тем же компилером что steam runtime. И когда пакую ее во flatpak, я не собираю core, но ссылку на него указываю. Понятия не имею, что там за компилер использовался, но игра работает.
Ну то-есть ABI нужен везде, где надо что-то с чем-то совместить, компоненты, где твое может быть только часть. Поставить third party, собрать что-то один раз и использовать либу, сделать dll\so и экономить юзеру память, плагины к какому-то софту итд. Когда все это не надо — ну да, тут эффективнее lto и статическая линковка, ктож спорит.
Проблема не в том, что «там постарались и нарушения ABI редки», а в том, что если вы делаете «широкий», «быстрый» интерфейс, то сделать его эффективно — очень тяжело. А если у вас там сплошные «хенлы» и внутренняя реализация скрыта — то ничего, кроме того, что предоставляет C — не нужно.вы предлагаете даже не пытаться поддерживать совместимость и писать сишные обертки? Вы же должны понимать, что это хуже буквально любой альтернативы?
Даже какой-нибудь MFC — он вместе с приложением идёт и вам нужна ровна та версия, которая будет совместима ровно с тем компилятором, которым вы это приложение собираете…
Как это не видели? В винде msvcp*, это хоть и redist и его обычно с приложением поставляют (хотя от старых студий уже вроде в комплекте, от 6й точно), но оно будет шариться между всеми использующими эту стабильную версию приложениями, и ему нужен ABI. Так же directx redist включают, COM интерфейс это тоже не C.
Макось? Полно не сишных интерфейсов, и что печально — сишные opengl и openal депрекейтят.
На лине тоже firefox использует stdc++ из системы, причем там при сборке конфигурится, какую минимально поддерживать. Есть куча других либ с не сишными интерфейсами.
Но вы абсолютно правы, что си интерфейс — наиболее ABI стабильный. А с++ и другие ломаются куда чаще. Именно потому его все любят, когда нужна совместимость. Именно потому — часто ломать ABI плохо, неважно stdc++, rust или go. Это вызывает проблемы в определенных случаях и их придется решать одним из способов.
Именно потому — часто ломать ABI плохо, неважно stdc++, rust или go.И именно поэтому в ядре Linux специально ломают ABI, ага.
Понимаете — мантра «часто ломать плохо» работает, когда альтернатива не слишком сложна. Если же ABI всё равно регулярно ломается — то проще не делать вид, что его можно как-то «застабилизировать», а признать, что его нельзя использовать, если вы два компонента независимо собирать хотите.
Это вызывает проблемы в определенных случаях и их придется решать одним из способов.Это не вызывает ровно никаких проблем если у вас нет никакого простого способа запустить приложение вместе с библиотекой, собранной другой версией компилятора. Вы просто знаете что нужно всё пересобрать.
Вы опять про все пересобрать. Вы понимаете, что если вы можете все пересобрать, — оно вам не надо. А если Far собран С++, а плагин на дельфи, и его создатель не имеет контроля над сборкой фара, и всех его версий у всех, — то ABI нужно. Да, оно кода-нибудь сломается, но ABI тут всё равно нужен, так что обратная мантра тоже не работает. И чем дольше оно не ломается — тем лучше. В вашем варианте мы просто не делаем плагины фару, всегда пересобираем из какой-то репы те что у вас есть.
И какой вид ни делай, если плагины ломаются очень редко — молодцы; часто ломаются — ну не молодцы, но может не сами девелоперы, а девелоперы их языка и возможно действительно переключиться на сишную прослойку хорошая идея. В общем это плохо как ни крути, может быть оправданным, но плохо и делать это стоит как можно реже. Кейсов где ABI нужен полно.
А если Far собран С++, а плагин на дельфи… то использовать как C++-строки, так и Delphi-строки в ABI вы не можете. И вам придётся, скорее всего, использовать C-style ABI.
И чем дольше оно не ломается — тем лучше.Конечно. POSIX-api скоро полвека стукнет — а оно всё ещё «не ломается»
Кейсов где ABI нужен полно.А где я с этим спорил? Продуманный ABI может жить десятилетиями… А вот ABI, который получился у вас, когда вы завели три класса и десять функций, а потом всё это проэкспортировали — нет.
А если вы уже разрабатываете ABI — то его можно и в C-функциях и в GBus, какой-нибудь, превратить… а если о нём не думать — то ничего хорошего не выйдет.
Нет, на винде ABI у дельфи для dll`ок вполне совместим. Ну то есть вы согласны что ABI иногда нужен, и что когда он долго не ломается — это хорошо?
Вы просто к тому что у других языков это не получится и надо всегда делать обертки на си? Ну хорошо, это же не противоречит тому, что ломать ABI плохо. Просто язык значит никогда не будет самостоятельным и требовать си для кейсов когда нужен ABI, именно по этой причине. И чем чаще происходит поломка ABI — тем более это верно.
Вот только не все спешат бежать делать си обертки для своих библиотек. А как миниум С++ и ObjC/Swift в реальной жизни вполне используют. Под виндой какой-то из стабильных msvcp, под линем stdc++ от какой-то версии и получают шаринг между приложениями и приемлимое время жизни приложения.
Отказ от поддержки ABI по сути будет перекладывание работы с разработчика языка на разработчиков библиотек\приложений для кейсов когда он нужен и он никогда не сможет стать именно заменой C/C++.
Ну то есть вы согласны что ABI иногда нужен, и что когда он долго не ломается — это хорошо?Конечно. Но такой у Rust уже есть.
Вы просто к тому что у других языков это не получится и надо всегда делать обертки на си?Зачем вам «обёртки на си». В C++ есть
extern "C"
, в Rust тоже extern
есть.Если вы свой ABI продумываете — этого достаточно. А если нет — то у вас и не получится стабильного ABI, даже если в языке он, формально, имеется.
Ну тут я согласен. Можно тогда язык использовть через си ABI, раз своего нет. Но конкретно у раста это потребует всякие #[repr(C)]
и libc::*, а не все это делают. Язык получается не самостоятельный и это все равно минус. Свой стабильный ABI — это хорошо, или сделать чтобы все нужные конструкции типа #[repr(C)]
были автоматом при экспорте в либы\dll\so. Но постойте-ка, это и получился бы совой стабильный ABI.
А иначе — все таки перекладывание этой проблемы с языка на разработчика. Не рассчитана либа на си — придется решать. А если все либы рассчитаны на си (как вы предлагаетте, всем так делать) — ну так вот он, свой стабильный ABI, стандартизируем как его свой и проблема решена. А пока этого нет — будут делать и так и так и проблема останется.
А иначе — все таки перекладывание этой проблемы с языка на разработчика.А стабильный API без разработчика никак не сделать.
Простейший пример прям из жизни: работаете вы с Xml и назвали вы детей
childs
(не native speaker, бывает). Потом, в следующей версии, пришёл грамматей и поправил. Стало вместо childs
, как и положено childred
.И? Как вы эту проблему на уровне языка решать собрались?
А если все либы рассчитаны на си (как вы предлагаетте, всем так делать)Нет. Большинство библиотек — вообще не требуют стабильного API. Они поставляются в исходниках и собираются так, как нужно.
А вот «большие» компоненты, поставляемые отдельно — да, здесь C ABI достаточно.
P.S. На самом деле самый разумный и, главное, практически отлично работающий подход к стабильному ABI — у разработчиков ядра Linux. Все знают, что внутри ядра — нет никакого стабильного ABI. Но не все осознают насколько, при этом, стабильно ABI на уровне системных вызовов. Windows отдыхает. Но да — это даром не даётся. Это нужно думать, планировать, моделировать, описывать. Делать это для каждой мелкой библиотеки — глупо и ненужно. Но если вы хотите поставлять библиотеку без исходников… то у вас нет выбора.
#[repr(C)]
— это не костыль, а механизм, позволяющий отделить ABI от деталей реализации
Все верно, это не костыль, а совместимость с си. Просто предлагается в либах и там где нужен стабильный ABI — все это использовать всегда. Но не все это в либах делают, и не всем хочется. Потому будут делать по-разному и проблема останется.
Подавляющее большинство либ на Rust (а на текущий момент — так вообще все) линкуются статически, на кой им стабильный ABI?
Так я про это и говорю: если не нужен ABI, не надо модулей, плагинов, шаринга кода/rdata между независимыми приложениями, удобного интерфейса к JIT и прочего — лучше линковать lto, это эффективнее и сделает каждой функции совой оптимальный ABI.
Речь же про кейсы когда он нужен, а для системного языка — это маст хев. Для потенциального заменителя C/C++ — тоже.
Ну так в этих редких кейсах и надо использовать #[repr(C)]
.
Это не редкие кейсы. Механизм совмещения — фича очень серьезная и используется повсемесно. Пирчем разные модули разрабытывают разные люди/команды/кампании. Связать все это очень важно. Например либу делает одини люди, а используют — другие, вы гарантируете что все либы это сделают, как предлагает khim? Получается я беру либу — проблема, еще беру — еще проблема. И что мне делать? А если все реально начнут делать #[repr(C)]
и прочее необходимое, и других вариантов не будет — так проблемы тогда нет, тогда это и будет стабильный ABI для раста.
Вот вам еще пример: вот скомпилил я Qt под asan/tasn. Вот они у меня лежат и я их иногда использую. Мне вот очень не понравиться если все минорные обновления компилера начнут его ломать. Или если я не смогу слинковаться с либой из дистриба немного другим компилером, у мнея их вообще 2, clang и gcc, а в репе собиралось одним (причем точно не ими, потмоу что вот они обновились, а все остальное — нет). Понимаете масштаб проблемы?
Как это всё относится к Rust? В Rust вы не будете компилить какой-нибудь qt-rs отдельно от своей программы, а значит и ломаться тут нечему.
Я вам пытаюсь показать кейс, когда ABI нужен, вы пытаетесь показать кейс где не нужен. Как это к rust относится? Я же конкретно про него пишу — не все либы используют и будут использовать совместимость с си, потому проблема ABI останется. Надо или на си ABI перейти для любой внешней комоновки, или свой стабильный сделать, и это реально очень надо.
Просто возьмите кейс где он нужен, а не статическую линковку qt-rs только к совему приложению. Например задейтесь целью зашарить его загрузку между несколькоми независимыми приложениями, контроля над которыми вы не имеете.
То есть у вас есть три сторонних программы, которые статически слинкованы с одной и той же библиотекой, а вы пытаетесь её оттуда выдрать и сделать динамической?
Я правильно понимаю ваш кейс?
Не вы, у разработчиков приложений появится такая возможновть, если либа поддержит динамическую линковку. А если динамическия линковка — почти всегда нужен ABI. Вот представьте: обновился в репе компилер — и хоба, прилетает вместе все 40гиг установки, потому что все надо собирать именно с этой версией. Ставите компилер из тестинг репы потому что вам надо? Ваши проблемы. А так, ментейнер возьмет либу, возьмет приложения, скомпилит в пакадж, проверит что все работает. И весь мир в апдейтах прилетать не будет. Вы можете эти приложения локально собрать, подцепив либу из репы, компилер будет другим наверняка, но работать будет и память будет шариться. Это все благодаря ABI и возможности совмещать модули. Все это разношерстная кампания, разные люди, у них разные системы, разные версии и им вот просто очень сильно надо уметь совмещаться внешне без пересборки мира.
К стаитческой линковке это тоже относится — иногда оно тоже надо, пример с Qt я выше приводил (забыл упомянуть что статически собирал). В дистрах статические либы тоже присутствуют. С ними точно так же желательно не ломаться как можно дольше.
А так, ментейнер возьмет либу, возьмет приложения, скомпилит в пакадж, проверит что все работает.Не возьмёт и не проверит. Все попытки продвижения GNU/Linux на десктоп, так или иначе, в это упираются.
Да, есть небольшое количество библиотек, которые «следят за гигиеной» и выпускают совместимые версии: libstdc++, Qt, в KDE кой-чего, может ещё пяток можно вспомнить… остальные же на это всё равно забивают.
Отсюда — все эти WinSxS, докеры и прочее. Люди всё равно испльзуют программы с «проверенными» библиотеками.
Все это разношерстная кампания, разные люди, у них разные системы, разные версии и им вот просто очень сильно надо уметь совмещаться внешне.Не нужно. Разработчики дистрибутивов уже лет двадцать пытаются продвинуть эту «нирванну»… а воз и ныне там: разработчики всё равно пакуют библиотеки с приложением. Посмотрите на какой-нибудь Acrobat Reader: он всё равно тащит с собой ICU и OpenSSL, libORBit и libbonobo, libcurl и libJP2K… потому что это — самый просто способ уменьшить количество головной боли у техподдержки.
Да, можно говорить, что это плохо, неправильно и так далее… а толку?
Все верно, я и сам пакую и тащу все статически, для универсальной сборки, которую можно скачать, и запускать неизвестно где. Для винды тоже так делаю, даже с redist не люблю связываться. Вот для flatpak/snap можно указать core вполне, с проблемами пока не сталкивался. А вот в репе — можно и собрать, тянув все динамически. Если говорить именно про ABI — оно просто не соберется иначе, проблему сразу будет видно. И в репах таких приложений тоже хватает.
Если говорить именно про ABI — оно просто не соберется иначе, проблему сразу будет видно.Если мы уже начинаем говорить про «оно просто не соберется» — значит речь уже не про бинарники, значит и стабильный ABI особо уже не нужен…
Ну так в этих редких кейсах и надо использовать #[repr©].вы сами то пробовали так делать? Просто на дистанции в пару десятков лет намного проще победить пару изменений ABI, нежели тянуть сишный интерфейс.
Как это не видели? В винде msvcp*
msvcp — это не интерфейс ОС, а рантайм С++.
COM интерфейс это тоже не C
Но это и не С++. Кстати, кто-нибудь знает, он там патентами какими-то закрыт или как? Удобная же вещь, а за пределами продуктов Microsoft почти никем не поддерживается...
Да я вкурсе что такое msvcp. Речь про то что он ставится в систему и будет шариться между всеми приложениями, как stdc++ под линем. Чем не интерфейс не на си? Вот WINAPI — да, там везде си, в .net — уже нет, на маке тоже в основном нет. Но это все не к тому, что си интерфейс нажеднее все равно никто не спорит. Это как раз наглядно показывает что это нужная фича, и сишные интерфейсы часто из-за этого выбирают.
Что COM не С++ я тоже вкурсе, но это и не си, хоть из си и можно работать, из си вообще с чем угодно можно работать.
Речь про то что он ставится в систему и будет шариться между всеми приложениями, как stdc++ под линем.
В данном случае, это не достоинство msvcp, а скорее недостаток линя. Или недостаток винды, зависит от точки зрения.
Так-то и stdc++ можно в теории в нескольких копиях в систему поставить...
Все так. На винде решают проблему, поставляя redist c приложением и держа несколько копий мажорных версий в системе. На лине — поддерживая какую-то минимальную версию stdc++.
Так же на лине теперь доступен вариант и как в винде — указывая разные версии core в flatpak/snap, будем попадать на разные версии stdc++. В системе их тоже может стать несколько, и все еще будем их шарить между приложениями.
Но это обход проблемы стабильного ABI, а не её решение.
Не совсем, это просто менее стабильный ABI нежели си и один из вариантов решения возникаемой от поломок пробелмы. Но благодаря поддержке его стабильности в определенных пределах — люди впринципе этом могут этим пользоваться, и пользуются. Это впринципе зачем нужен ABI. Если он совсем часто ломается — это так-себе ABI, поддержка усложнится и пользваться станут реже, или станут от чего-то отказываться, это однозначная проблема для кейсов где он нужен. Если же впринципе отказаться от поддержки стабильности ABI — ну так это равноценно тому что его нет, соответственно кейсы где он нужен просто не применимы.
Если он совсем часто ломается — это так-себе ABI, поддержка усложнится и пользваться станут реже, или станут от чего-то отказываться, это однозначная проблема для кейсов где он нужен.В Python ABI подерживается, грубо говоря год (в каждой минорной версии он свой). Много народу уже отказались?
Так-то и stdc++ можно в теории в нескольких копиях в систему поставить...Более того — нужно. Если вы старые приложения хотите использовать. А если вы используете библиотеки, собранные gcc 4.x и gcc 5+… получите «массу удовольствия» даже несмотря на одну версию libstdc++…
Это то, что всё равно по итогу делается. Я не знаю ни одной «живой» операционки, предоставляющей из коробки C++ интерфейс хоть к чему-нибудь.а как же винда, знатная часть интерфейсов которой торчит наружу в виде COM-объектов, имеющих сишный и плюсовый интерфейсы?
Прикрутите к ней API на C — и поставляйте.для такой цели c++ подходит лучше раста
Попытки «заморозить» C++ ABI показали, что пользоваться этим всё равно умеют единицы, а проблемы это вызывает у всех.что вы понимаете под «пользоваться ABI»? И какие «проблемы» у всех это вызывает? Что вы вообще понимаете под «ABI»? Вот например вот здесь:
Тем не менее C++14 ABI и C++17 ABI несовместим, так что с некоторой частотой это всё равно происходит.вам известно о каких-то breaking changes о которых не известно никому другому?
вам известно о каких-то breaking changes о которых не известно никому другому?Почему «неизвестно никому другому»? Известно. Ну вот вам простейший пример:
test.h:
struct Foo {
int value;
};
struct Bar {
static constexpr Foo foo{42};
};
test1.cc:#include "test.h"
constexpr Foo Bar::foo;
const Foo* baz() {
return &Bar::foo;
}
test2.cc:#include "test.h"
const Foo* qux() {
return &Bar::foo;
}
main.cc:int main() {
}
В C++14 всё Ok:
$ g++ -c -std=c++14 test1.cc -o test1.o $ g++ -c -std=c++14 test2.cc -o test2.o $ g++ main.cc test1.o test2.o
В C++17 всё Ok:
$ g++ -c -std=c++17 test1.cc -o test1.o $ g++ -c -std=c++17 test2.cc -o test2.o $ g++ main.cc test1.o test2.o
А смешивать — нельзя:
him@khim1:/tmp/1$ g++ -c -std=c++14 test1.cc -o test1.o khim@khim1:/tmp/1$ g++ -c -std=c++17 test2.cc -o test2.o khim@khim1:/tmp/1$ g++ main.cc test1.o test2.o /usr/bin/ld: test2.o:(.rodata._ZN3Bar3fooE[_ZN3Bar3fooE]+0x0): multiple definition of `Bar::foo'; test1.o:(.rodata+0x0): first defined here collect2: error: ld returned 1 exit status
Получите, распишитесь.
Поэтому, в частности, у нас переход на C++17 происходил через flag day.
Эта несовместимость — известна, собственно никто её и не скрывает.
что вы понимаете под «пользоваться ABI»?Ну вот то, про что вы пишите: выпустить либу — а потом новую версию без пересборки бинарника, который этой либой пользуется. Хорошо работает в случае C интерфейса (как зачастую и делают, даже если «внутри» там сплошной C++), отвратительно — в случае с C++.
Да, это возможно… но надёжнее — интерфейс на C.
Получите, распишитесь.вот только какое отношение проблемы линковки одного бинаря могут иметь к совместимости ABI между разными бинарями?
Увы, единственный способ не поиметь проблем — это использовать C и «узкие» интерфейсы. Всё остальное — от лукавого.
А для этих целей есть разнообразные fabi-version (обратите внимание на 12ю и 13е версии)«corrects», «corrects» и еще раз «corrects»…
По сути, единственная причина почему сишный ABI не меняется в том, что сам язык практически не развивается. А вот в плюсах иногда ABI приходится расширять по мере изменения языка.
Ну или всякие чудеса типа вот такого.с каких пор __int128 является стандартным плюсовым типом, обязанным иметь одинаковую реализацию в разных компиляторах?
с каких пор __int128 является стандартным плюсовым типом, обязанным иметь одинаковую реализацию в разных компиляторах?С тех пор, как он был добавлен в psABI?
А вот в плюсах иногда ABI приходится расширять по мере изменения языка.А зачем? Мои можно сразу выделить «переносимое подмножество» — а остальное объявить внутренним делом реализации языка.
Вот вы тут COM вспоминали… Много там появилось за последние 10 лет? А C++ таки прилично поменялся…
С тех пор, как он был добавлен в psABI?1. psABI != стандарт языка с++
2. __int128 не является обязательным к реализации согласно psABI
3. psABI почти никак не описывает эту реализацию
А зачем? Мои можно сразу выделить «переносимое подмножество» — а остальное объявить внутренним делом реализации языка.оно и так выделено. extern «C» называется. Но еще раз: решать проблемы бинарной совместимости через си — самый болезненный из возможных вариантов.
Вот вы тут COM вспоминали… Много там появилось за последние 10 лет? А C++ таки прилично поменялся…Ну так благодаря бинарной совместимости COM и не меняется
Ситуация с noexcept в чём-то лучше: можно создать библиотеку, которая работает и с C++14 клиентами и с C++17.
А вот с этими переменными — засада полная: сделать так, чтобы модули C++14 и C++17 не конфликтовали нельзя вообще. Никак. Во-всяком случае мне решения найти не удалось…
Достаточно ведь вспомнить, что в C++17 noexcept — часть типа функции (и участвует в мэнглинге), а в C++14 — нет.Я тоже так думал, но простейшая проверка опровергает это утверждение.
Ну вот смысл в том, чтобы без этой подсказки код не компилировался, а если вы эту подсказку импользовали чтобы компилятор проверил что вы нигде её не нарушили.
Удобно ведь? Чем раст и занимается, по сути.
Ну вот смысл в том, чтобы без этой подсказки код не компилировался
Нет уж, спасибо :) Может быть, мне действительно нужно получить алиасинг в этом месте для того, чтобы получить byte representation некоего объекта. Не надо за меня думать.
Тогда вы обернете в UnsafeCell или указатель сырой передадите. Таким образом явно пометите что знаете что происходит. Тогда компилятор отменит оптимизацию, но вы в коде будете видеть, что тут и оптимизации не будет, и алиасинг возможен, ну и как-то эту информацию сможете учесть, например, не станете "тупой компилятор" принудительно заставлять тут что-нибудь векторизовать.
При обычном программировании ситуация "массив байт — это реально массив байт" встречается куда чаще, чем ситуация "массив байт — это byte representation некоего объекта".
Любая строка (std::string) — это скрытый массив байт. Который заведомо не является byte representation некоего объекта, просто потому что строка владеет этим массивом.
Мне вот интересно, сколько костылей пришлось вставить авторам стандартной библиотеки, чтобы компилятор знал, что содержимое строки не алиасится с другими объектами? И знает ли вообще компилятор об этом?
godbolt.org/z/XiA6YZ
:) Приватные поля классов, пусть даже и char* — это вам не аргумент функции, который может придти ХЗ откуда, в том числе из другого модуля.
А что насчёт вот такого примера? Он что, тоже искусственный? Вроде бы совершенно стандартная ситуация, когда надо посимвольно обойти переданную по ссылке строку...
Обратите внимание, что компилятор даже сам указатель "c"
вынужден считывать на каждом шаге цикла, потому что не уверен что тот не алиасится с владеющей им же структурой!
UPD Нашёл пост на Хабре, откуда я узнал об этом эффекте: https://habr.com/ru/company/pvs-studio/blog/475636/
godbolt.org/z/7oPNmw
Тоже вполне прилично разворачивается. Но да, тут &i тоже не может придти хз откуда. Но такой сценарий все-таки куда более частый, чем ваш.
Но такой сценарий все-таки куда более частый, чем ваш.
Ну, это как бы ваш сценарий, вы же первый придумали переменную i
по ссылке передавать для проверки алиасинга...
Карма не позволяет писать ответы слишком часто, поэтому отвечу здесь же и на второй ваш комментарий:
Да, но если эту функцию РЕАЛЬНО ВЫЗВАТЬ, скажем, в main():
godbolt.org/z/BtFJDr
то опять же в месте вызова все развернется более-менее прилично, если компилятор знает, откуда что берется.
Всю программу, к сожалению, в main не заинлайнить; всё равно где-то компилятору придётся проводить границы между функциями...
Там смысл был в том, что оба аргумента могут придти ХЗ откуда.
Но ведь приватные данные класса тоже могут ссылаться хз на что, и даже на второй аргумент который мы передаваем.
Карма не позволяет писать ответы слишком часто
Подправил этот момент.
Но ведь приватные данные класса тоже могут ссылаться хз на что, и даже на второй аргумент который мы передаваем.
Поэтому в плюсах очень любят инлайнинг, по крайней мере в каких-то вот таких базовых вещах типа строк :) Во-первых, это само по себе оптимизация, а во-вторых, компилятор в этом случае хорошо видит, что откуда берется, и что с чем может алиаситься. Но да, в общем случае это таки не панацея.
Подправил этот момент.
Благодарю! :)
Ну вот смысл в том, чтобы без этой подсказки код не компилировался, а если вы эту подсказку импользовали чтобы компилятор проверил что вы нигде её не нарушили.проблемы начинаются когда я хочу чтобы без этой подсказки код компилировался и работал. Так, как я его написал.
Когда одни Умные программисты встречают код написанный другими Умными программистами, обычно они не одну ночь дебажатся, а потом пишут в рабочие чатики "смотрите какую хрень я нашел в нашем проекте".
А вот когда вы лет через 10 заглянете в rust проект, переживший пару сотен «умных программистов», десятки тысяч «срочная фича, дедлайн вчера» и ни одного рефакторинга («всё ж работает!»), но с кучей unsafe костылей (которые неизбежно будут появляться везде, где борроучеккер будет препятствовать изменениям кода), вот тогда вы будете не одну ночь дебажиться, подавляя желание переписать всё с нуля (ибо времени не выделено), и напишете в рабочий чатик «смотрите какую хрень я нашел в нашем проекте». И вам там ответят «да, это надо переписать, там где-то был тикет на это, лет 5 назад, но Вася Пупкин уволился и делать его некому».
Покажите проект с кучей unsafe-костылей. Или вы скажете что они все маленькие и плохие? Даже в rustc ансейфа очень мало, назвать его маленьким язык не повернется. При том что это блин компилятор, с кучей низкоуровневой магии.
Если вернуться чуть ближе к миру смертных и посмотреть на типичную библиотеку вроде actix то там 54 ансейфов на 54750 строк кода. Учитывая что почти все ансейфы — однострочные, получаем сколько? 0.1% ансейф кода? Ну да, это явно можно назвать кучей ансейф костылей.
Возьмем serde_json, 15к строк кода, пять ансейфов
Возьмем parity-ethereum, 200к строк кода, 68 ансейфов.
Продолжать?
Я говорю про типичный прод типичного бизнеса, который практически не заинтересован в качестве кода. Тот самый плюсовый код, который вы критикуете, обитает преимущественно в них. И таких проектов на расте сейчас раз-два и обчелся, он попросту слишком молод.
Даже в rustc ансейфа очень мало, назвать его маленьким язык не повернется. При том что это блин компилятор, с кучей низкоуровневой магии.куча низкоуровневой магии там в миддленде и бекенде. Тех, которые не на расте.
а что, популярные открытые плюсовые библиотеки плохо написаны? Типа буста? Или компиляторы?
молод.
Парити — это буст или компилятор?
куча низкоуровневой магии там в миддленде и бекенде
Простите, а MIRI тогда это что?
Задача раста — компилировать в LLVM. С чем он и справляется, будучи написан полностью на расте.
И таких проектов на расте сейчас раз-два и обчелся, он попросту слишком молод.
На них хватает проектов, просто вы о них возможно не слышали.
Решил наш собственный микросервис растовый проверить, 10к строк, два ансейфа.
В общем, порядок виден. десятые доли процента в худшем случае, если мы говорим про прикладные крейты.
Взял вот наш микросервис растовый, 10к строк, два ансейфа.
возьмите коммерческий растовый проект хотя бы на 10 миллионов строк, тогда и поговорим.
Что-то мне этот разговор напоминает
Ну и даже если — какая разница? 99.99% людей никогда не пишут проекты на миллионы строк кода. Даже если там что-то вдруг пойдет не так (почему? Какой механизм за этим стоит), это затронет жалкую горстку людей. Остальные могут пользоваться бенефитами, которые гарантированно есть на проектах до миллион строк.
Кстати, насчёт молодого раста — ему сейчас столько же лет, сколько было сишарпу в 2006, когда им уже активно пользовались.
Ну, во-первых даже если влепят, когда всё развалится по ним можно будет грепнуть.
А во-вторых написать сейф вариант часто короче и быстрее написать чем ансейф, посмотрите тот же пример выше с get_unchecked
против []
, поэтому ленивый разработчик скорее словит панику чем уб.
Ну и в-третьих парити не бодишоп конечно, но типичная айтишная контора с типичными приоритетами, так что даже я там косяки находил и фиксил. Но ничего криминального с точки зрения нарушения языковых инвариантов не нашел.
Что-то мне этот разговор напоминаетодумайтесь. Вы сравниваете заведомо плохой код на плюсах с заведомо хорошим кодом на расте и приводите это в аргумент того, какие плюсы плохие. До этого вы сравнивали раст с плюсами приводя в пример вообще сишный код.
Остальные могут пользоваться бенефитами, которые гарантированно есть на проектах до миллион строк.да вы бы хоть такой пример привели. Просто любой большой проект, который не энтузиасты в свободное время делают, и не разработчики языка, а обычные программисты из тех самых 99.9%, которые перекладывают json'ы, закрывают тикеты и у которых нет времени рефакторить код на каждую хотелку.
Хотя впрочем я не считаю, что без ансейфа было бы существенно медленнее. Разве что громкого заголовка могло не получиться )
Да и вообще, Расту надо доказывать не производительность кода (тут сомнений мало), а производительность написания.
Для того, чтобы подобного ужаса не случилось, в Rust достаточно одного правила: в основном проекте не должно быть никакого unsafe! Если требуется что-то хитрое — пиши абстрактную библиотеку.
В С++ аналогичный набор правил намного сложнее.
ну вот простейший (хотя и довольно-таки патологический) пример… внушает, данет. в случае с make_unique дополнительно выполняется явная инициализация (0 для инт, вызвался бы конструктор для пользовательского типа)
Я предпочитаю иметь свою т.зр. в своих программах:то, что язык поддерживает RAII а библиотеки с его помощью автоматизируют ручное освобождение памяти (и иных ресурсов, btw), никак не отменяет того факта, что управление памятью в с++ ручное.
1. Использую new/delete — ручное управление
2. Использую unique_ptr — RAII
3. Выбираю shared/weak_ptr — ARC
4. Подключаю boehm — оппа — программа с GCзачем?
зачем?пример просто для полноты картины
Посмотрел первый тест reverse-complement. Прямо с самого начала идут различия.
В Расте:
stdin.lock().read_to_end(&mut data).unwrap();
В С++:
// read line-by-line, according to game rule. Should be replaced by fread()
while (fgets_unlocked(cur_pos, remainbytes, stdin) != 0)
Разве это можно напрямую сравнивать на большом количестве строк? Дальше там еще больше различий идет. Если omp c thread::spawn еще можно сравнить, то в треде там совсем по разному обработка идет.
Вы же не думаете, что когда вы читаете в С++ «до конца строки», стандартная библиотека не буферизирует чтение?
Более того, С++ версия вычисляет общий размер ввода, что формально нарушает правила.
По поводу этих условий есть определенная дискуссия. Предлагается по меньшей мере переформулировать их для ясности.
Честно говоря, пока все, что я вижу — это набор реализаций, которые не придерживаются оговоренных правил, ну или придерживаются их отчасти: какие-то больше, какие-то меньше. Я не в курсе, есть ли там какая-то модерация, но по-хорошему эти реализации не должны были ее пройти. Полностью согласен с ilynxy что эти бенчмарки сравнивают непонятно что непонятно с чем.
Из внутренней дискуссии, конкретному по тому вопросу, который вы подняли:
The description has said «read line-by-line» since Dec 2004, in-spite of the fact that even back then we understood that what seemed to be «read line-by-line» in-say CPython defaults to a buffered read.
I think «read line-by-line» actually meant don't write a program that will fail when the workload increases 40x.
Насколько я вижу по реализациям – большинство программ читают таким образом, каким им это удобно.
Создатель этой игры не смог сформулировать что значит line by line строго поэтому забанил половину решений как ему захотелось. И это только один момент почему этот бенчмарк ниочем.
Данный бенч (или его владелец) очень непоследователен в правилах: половина решений в некоторых тестах использует ассемблерные вставки. Что в итоге мы этим показываем: как крут ассемблер или как крут тестируемый язык?
В теории у мемори-сейф языков есть некоторое преимущество в оптимизации, но в алгоритмах выбранных для бенчмарков это преимущество вряд ли может себя проявить.
В исходниках, кстати, есть идиоматические реализации алгоритмов без хитрых трюков, но так не победить =)
Ну интринзики можно было бы и в C++ и в Раст добавить, было бы понятно как модифицировать другой пример чтобы сравнять, сравнивались бы уже сами компиляторы. Хотя clang это и C++ и Раст и так =)
Про преимущества оптимизации у мемори-сейф, ну так в С++ тоже есть всякие restrict и расширения, которые теоретически эту разницу нивелируют.
Тут же Раст прямо нарушает условия, так что С++ даже нельзя в ответ модифицировать.
godbolt.org/z/TGuECo
В случае с restrict он вычисляет результат в compile time.
Ну интринзики можно было бы и в C++ и в Раст добавить, было бы понятно как модифицировать другой пример чтобы сравнять, сравнивались бы уже сами компиляторы. Хотя clang это и C++ и Раст и так =)См. исходники — интринсики используются и в Расте, кстати почему то их вызов unsafe.
Еще раз — gcc это не clang — а в этом тесте C/C++ использует gcc
Да, я видел. Я пишу немного на расте и я к тому что если ипользоваь clang — у меня всегда выходило примерно "сравнять" оптимизации. Разница была лиш в том что раст вставляет проверки, а компилер по сути один и тот-же.
Это правда могло вызывать дополнительный спиллинг регистров, что на асме может сильно сказаться. Так же на расте иногда сложно сравнять даже с интринзиками, в специфических случаях, например из-за alloca https://github.com/rust-lang/rfcs/issues/618 https://github.com/rust-lang/rust/issues/48055, асм вставка тут не поможет т.к. надо со стеком корректно играться (ну или памятью пожертвовать, если там не гигабайты в худшем случае). Когда Unsized Rvalues допилят — одна из пробелм уйдет. А вот в обратную сторону — на С/С++ можно сделать все что позволяет AST clang`а.
Поэтому конкретно эти бенчмарки сравнивают непонятно что с непонятно чем.Почему же непонятно? Эти бенчмарки сравнивают сильно заоптимизированный код на различных языках программирования. Таким образом, результаты отвечают на вопрос, насколько сильно ограничением в производительности будет именно язык, а не прямота рук программиста.
Как правило, 99% производительности зависит от 0.1% кода. Считайте что бенчмарки – ответ на вопрос, на чем и как могут быть написаны эти 0.1%.
В исходниках, кстати, есть идиоматические реализации алгоритмов без хитрых трюковБолее того, на сайте прямо говориться – сравнивайте не только результаты измерений, но и исходники.
Обычно у языка есть декларируемая сфера применения и практическая (историческая). Rust, если я правильно понимаю, предназначен для тех же целей, что и C++. Не вижу никаких причин быть медленней любого другого языка, кроме молодости. Опять же, компилируемый язык, позволящий использовать особенности целевой архитектуры (лучше неявно) будет претендовать на звание быстрейшего. Однако скорость это не главное, хотя и важное. Главное чтобы эта скорость была доступна искаропки средней руки программисту. Этак можно скомпилировать программу на C компилятором C++ (ониж совместимы) и заявить, что C++ рвёт Rust.
А в соревновании кто напишет код ближе к Си победителей не будет.
Ну то есть пишем либу на Си и вызываем её функции из <язык по вкусу>. Тем самым демонстрируя прямоту рук.Внезапно, Rust зависит от libc, а половина стандартной библиотеки Python – обертки на сишными либами. Так что в реальном мире все именно так и работает, только вот скорость C теряется на переходах FFI и из-за всяких GIL.
Rust же претендует на то чтобы быть быстрее С заменяя его, а не выступая лишь тонкой прослойкой для вызова сишных функций.
Не вижу никаких причин быть медленней любого другого языка, кроме молодости.И тем не менее, Ada, Fortran и Pascal почему-то заметно медленнее C/C++/Rust.
Почему же непонятно? Эти бенчмарки сравнивают сильно заоптимизированный код на различных языках программированияи какой смысл сравнивать, скажем, написанный на интринсиках код на си и написанный на интринсиках код на с++, если их отличия будут минимальны, но при этом ни один ни второй код не будет отражать 99.9% кейсов реального программирования на языке? А если я просто напишу asm-вставку, это будет считаться?
Разве это можно напрямую сравнивать на большом количестве строк? Дальше там еще больше различий идет.Сравниваются наиболее эффективные подходы, а не идентичные реализации, переписанные с одного языка на другой. Если вы посмотрите на различные реализации в одних и тех же языках, то увидите, что бывают очень разные подходы, особенно в вопросах распараллеливания. Для сравнения языков выбирается наиболее эффективная реализация для каждого языка.
Ну это понятно, я уже выше писал что принципиально разные omp и thread::spawn — это можно сравнивать, будем сравнивать эффективность crt. Но там где сравнивается участки с принципиально большим количеством действий (fgets + strlen) и когда их можно привести к одному виду (там даже есть комментарий про fread) — так сравнивать нельзя. Как я понимаю в С++ используется не эффективный подход из-за правил, а в Раст коде правило нарушено.
Сравниваются наиболее эффективные подходы
Очевидно, в данном случае это не так. В абсолютно любом языке чтение всего файла целиком — более эффективно, чем построчное чтение.
Для сравнения языков выбирается наиболее эффективная реализация для каждого языка.
Как мы только что выяснили, не наиболее эффективная, а произвольная.
Если вы посмотрите на различные реализации в одних и тех же языках, то увидите, что бывают очень разные подходы, особенно в вопросах распараллеливания.
И что такие сравнения должны продемонстрировать? Ну вот есть цифры бенчмарков произвольных реализаций произвольных алгоритмов, решающих одну и ту же задачу на двух разных языках. О чем эти цифры говорят? О том, что язык А быстрее языка Б? Нет, алгоритмы же разные использовались. О том, что алгоритм А быстрее алгоритма Б? Нет, реализации же разные. Может хотя-бы о том, что реализация А быстрее реализации Б? Нет, компиляторы же разные, с разным набором оптимизаций.
Хоть убейте, не пойму, как из таких бенчмарков с "наиболее эффективными подходами" можно делать какие-то выводы о скорости языков. Не меряют они скорость языка. Они меряют непонятно что.
Как я уже писал выше – не столь важно, как именно вы прочитаете 1MB данных из stdin на фоне 2 секунд вычислений. Тем более, что мы даже не можем дать гарантий, что компилятор не с оптимизирует «построчное» чтение. Тем более, что stdin все равно буферизируется ОС.
Ну вот есть цифры бенчмарков произвольных реализаций произвольных алгоритмов, решающих одну и ту же задачу на двух разных языках. О чем эти цифры говорят?О том, на каком языке можно достигнуть наилучшую производительность, решая одну и ту же задачу, используя весь арсенал доступных оптимизаций.
Ну и что показывает факт что си быстрее си++ если си код скорее всего можно скопировать g++? Признайтесь, заголовок желтый, написали бы "раст показывает производительность на урвоне си и си++". В результате на чем писать simd инструкции не важно, лишь бы язык поддерживал в любом случае приходим к ручной возне с указателями и unsafe. А если сравнивать с наворотами которые помогают, то в расте пока не хватает например ручного unroll поскольку нету const generics.
Признайтесь, заголовок желтыйНе признаю, потому что я вполне сознательно прямо в заголовке указал «согласно результатам Benchmarks Game». Так что читатель вправе доверять утверждению в заголовке ровно настолько, насколько доверяет бенчмарку, и это нормально.
В результате на чем писать simd инструкции не важно, лишь бы язык поддерживал.Если бы это было так, Rust и C показывали очень близкие результаты, а для некоторых задач это совсем не так. SIMD можно использовать и в Python, но вряд ли вам удасться добиться результатов близких к С/Rust и вряд ли вам захочется переписывать каждый цикл так, чтобы SIMD использовался.
в любом случае приходим к ручной возне с указателями и unsafeВы не правы. Только для 3 задач из 10 самые быстрые реализации на Rust используют unsafe.
Ну да как раз скрывать unsafe абстракции за safe интерфейсом и есть киллер фича раста, так что в других 7 там уровнем ниже :)
А по поводу заголовока вы укавите, т.к. Benchmark Game нигде не утверждает что язык а превосходит язык б по производительности. О чем у вас написано в тексте. Но это обычное дело для заголовков ;)
Хотя я согласен с вами что в приведенном месте есть проблема с формулировкой задачи, я считаю что вы слишком придираетесь там где это не вполне оправданно.а почему вы считаете что проблема в формулировке задачи, а не в реализации её на расте? Которая по факту нарушила правила конкурса, даже если такого правила не должно было существовать.
То C++ решение нарушает другое правило. Оно читает размер файла и выделяет буфер нужного размера сразу. Этого тоже нельзя делать. Но если хочется, то можно. Ведь автор правил писал одно а подразумевал, оказывается, совсем другое.
Так что проблема все таки не в расте и не в решениях на нем, а в самом конкурсе, где нет четких требований, а те требования что есть — каждый понимает как хочет.
используя весь арсенал доступных оптимизаций.
И снова нет. "Используя весь арсенал доступных оптимизаций" программы на C++ были бы копипастом программ на C и работали бы со скоростью программ на C. А в случае, если бы какой-то язык обогнал си (неважно, Rust это будет или, скажем, Python) — все решения на языках, поддерживающих asm вставки, превратились бы в одну большую asm вставку с дизассемблированным кодом этого более быстрого решения.
То есть используется как минимум не "весь арсенал". И мы снова приходим к тому, что меряется в этой игре бенчмарков непонятно что. И результаты этих измерений между собой не сравнимы.
Но вот смувать массив частично — уже задача нетривиальная.
По вашему хардкорному примеру трудно понять, какого поведения вы хотите добиться. Можете объяснить словами?
Как известно, для массивов реализация IntoIterator
бланкет-реализация обычного Iterator
, из-за чего iter
аналогичен into_iter
и производит итерацию только по ссылкам.
Эта реализация соответственно занимается тем, что позволяет обходить массив по значению, мувая каждый элемент.
Иными словами, есть такой код:
let x = vec![F(1),F(2),F(3)];
let squardVec: Vec<i32> = x.into_iter().map(|x| F(x*x)).collect();
где реализацию F можно увидеть по ссылке. Задача — сделать так, чтобы оно работало для массива let x = [F(1),F(2),F(3)];
. Вторую строчку трогать нельзя.
А если мне нужно пройтись по массиву элементов и сделать некоторые операции каждого с каждым?
Напишете for:
fn main() {
let mut arr = [1,2,3];
println!("{:?}", arr);
for i in arr.iter_mut() {
*i += 10;
}
println!("{:?}", arr);
}
тогда делаете for i in 0..arr.len()
и дальше как в старом добром си по индексам пишете что хотите.
pub fn foo(v: &mut Vec<f32>) {
Главное не перепутать =)Не успел написать) Но да, не путайте изменяемую ссылку на неизменяемый вектор, и ссылку на изменяемый вектор:
Ну во-первых так уже лучше, а во-вторых почему сравниваем неэквивалентный код?
Забавно что в расте он развернул цикл на 2, но только в unsafe случае. Интересно, с чем это связано?
Скорее всего увидел, что итерации независимы. Раньше была зависимость в виде паники ведь. А теперь итерации можно делать полностью свободно.
Я нашел, вот так код 1 в 1 получается: https://godbolt.org/z/vitnPr
Почему-то .size() у вектора сбивает анализ, как и проверка в сейф версии раста, хотя по идее это не должно влиять. Кажется небольшой недочет в оптимизаторе.
а во-вторых почему сравниваем неэквивалентный код?Код реализует одинаковый алгоритм и не существует таких входных данных на которых его выход будет отличаться. Вопрос: почему он неэквивалентен?
Ну во-первых так уже лучшеа вот это уже неэквивалентный код
Код реализует одинаковый алгоритм и не существует таких входных данных на которых его выход будет отличаться. Вопрос: почему он неэквивалентен?
Ну потому что в плюсах у вас используется get_unchecked, а в расте — get_checked.
Неэквивалентность в том, что оператор []
делает разные вещи в разных языках, и это надо учитвать.
Точно так же, как Foo<T>
в С++ и в расте означает разные вещи.
Эквивалент для сишного []
это растовый get_unchecked/get_unchecked_mut
, поэтому если и писать "эквивалент", то надо их использовать, а не то, что "выглядит похоже".
Вот здесь я написал вариант, который компилятор векторизовал даже лучше решения на индексах. При этом гарантия не выхода за границы массива столь же сильная.
Эквивалент для сишного [] это растовый get_unchecked/get_unchecked_mut, поэтому если и писать «эквивалент», то надо их использовать, а не то, что «выглядит похоже».у вас постоянно скачет мнение — то вы доказываете что эквивалентный код на расте безопасен, то утверждаете что безопасный код на расте неэквивалентен. Определитесь уже.
По сути, неэквивалентность в вашем понимании состоит только в наличии заведомо ненужной проверки — i и (v.len() — 1 — i) всегда будут попадать в диапазон 0..v.len(), по которому идет итерация.
Неэквиалентность в моем понимании состоит в том, что оператор []
делает разные вещи в разных языках. Если проверка "ненужная", то пишите код который проверку не делает. Если нужная — делайте проверку в обоих случаях.
Пытаться сравнить внешне похожие, но совсем разные по сути операции, выглядит как манипуляция.
Вот здесь я написал вариант, который компилятор векторизовал даже лучше решения на индексах. При этом гарантия не выхода за границы массива столь же сильная.
Растовый оптимизатор точно так же оптимизирует форыч и фор от 0 до len. Если вы заметили, я их оставил как есть, и компилятор вывел что там проверять ничего не нужно. Для len() - i - 1
он такого вывести не смог, ну что ж, когда-нибудь ему этому научат. Но это мало чего меняет: если мы сравниваем операции без проверки, пишем в обоих случаях без проверки. Сравниваем с проверкой — пишем так в обоих случаях. Писать с проверкой в расте а потом говорить что он генерирует плохой код — ну извините.
у вас постоянно скачет мнение — то вы доказываете что эквивалентный код на расте безопасен, то утверждаете что безопасный код на расте неэквивалентен. Определитесь уже.
Видимо, стоит определить понятие эквивалентности. Для меня это код, который выполняет одну и ту же задачу с одними и теми же гарантиями.
Разница в поведении в таком варианта действительно незаметна (в оригинальном сравнении). Но если мы воспользуемся какими-нибудь мутационными тестами, которые — 1 на + 1 заменять например то разница не заставит себя ждать.
Видимо, стоит определить понятие эквивалентности. Для меня это код, который выполняет одну и ту же задачу с одними и теми же гарантиями.приведенные мной варианты кода выполняют одну и ту же задачу с одними и теми же гарантиями.
Но если мы воспользуемся какими-нибудь мутационными тестами, которые — 1 на + 1 заменять например то разница не заставит себя ждать.тогда мы будем сравнивать не код A с кодом B, а код X с кодом Y. По сути, этот ваш аргумент — чистейшая контрпродуктивная демагогия.
Я не понимаю о чем спор.
- Можно ли написать на расте один в один как на си? Ну да, обмазываемся ансейфом и имеем то же самое с той же производительностью. Получится чуть длиннее из-за ансейфов и того, что функции намеренно длинными названы.
- Можно ли написать на расте так же коротко как на си? Ну да, можно, но тогда компилятор будет расставлять проверки что никто не стреляет по ногам.
- Умеет ли компилятор раста элидить все проверки которые мы как люди видим не нужны? Ну, видимо не умеет, надо ждать пока научится. А пока не умеет — либо писать руками, либо забить на наличие этой проверки.
В чем утверждение-то? Что код без проверок получается быстрее? Ну получается, что в расте, что в си. Так что, что доказывали — непонятно.
Сколько кода я должен дописать, чтобы получить рабочий и безопасный эквивалент без ущерба в производительности?
Вот столько:
pub fn foo(v: &mut [f32]) {
for i in 0..v.len() {
v[i] = v[i] * unsafe {v.get_unchecked(v.len() - 1 - i)};
}
}
Можно ли написать на расте один в один как на си? Ну да, обмазываемся ансейфом и имеем то же самое с той же производительностью...вы готовы признать что безопасность и производительность раста часто противоречат друг другу? То есть то, что вы только что написали, но в однозначной формулировке. Мне лично это принципиально — это утверждение стоило мне всей кармы
Вот столько:а аналог вот этого кода?
вы готовы признать что безопасность и производительность раста часто противоречат друг другу? То есть то, что вы только что написали, но в однозначной формулировке.
Иногда — противоречит, конечно. Иногда — наоборот, помогает. Какая пропорция помощи и противоречия не готов сказать.
Мое утверждение было что не всегда более безопасный код медленнее, иногда он такой же или быстрее. То что более безопасный код всегда быстрее — очевидно ложное утверждение, и я его не делал.
Мне лично это принципиально — это утверждение стоило мне всей кармы
Лично моя позиция по этому вопросу такая
Я вообще в карму никогда не минусую, если и несогласен, ограничиваюсь комментарием.
а аналог вот этого кода?
В сейф расте такой код написать невозможно.
Ну во-первых так уже лучше, а во-вторых почему сравниваем неэквивалентный код?Прекрасный пример, который даёт однозначный ответ на вопрос голосовалки =)
Ждём Verona от Microsoft.
Они же говорили, что проект совершенно о другом и в зачаточном состоянии.
Конечно, было бы неплохо если бы они выпустили крутой раст с исправленными ошибками, он не уверен что это будет оно. Плюс фрагментация экосистемы.
Будем посмотреть, но не надо завышать ожидания. Лучше наоборот, их занизить, а потом порадоваться что ошиблись.
Тут .NET открыт под самой свободной лицензией и поддерживает Linux, MacOS, Android и iOS. Да, нам тут тоже сразу не верилось.это сколько лет спустя? И на чем основана ваша уверенность что завтра не появится какой-нибудь Rust/CLI?
И на чем основана ваша уверенность что завтра не появится какой-нибудь Rust/CLI?Почему я должен переживать, что появится Rust/CLI?
Мне очень любопытно услышать мнение людей, которые считают что раст не сможет догнать си в ближайшее время.
Я думаю одно только исправление бага LLVM связанным с NOALIAS поднимет раст на первую строчку. Что до реальных приложений — то там есть еще и ограничение по времени. Написать быстрый корректный код на расте имхо займет куда меньше времени чем на си.
Про noalias не знаю, зависит от задачи (в C, как выше справедливо написали, тоже есть restrict)
Не встречал си разработчиков которые в программе расставляют restrict у каждого указателя, которых могут быть миллионы. Я понимаю правило про 80/20, но всё же.
а вот что касается реальных приложений, то тут немаловажную роль ещё играет возможность пользоваться готовыми библиотеками, написать к которым раст-обёртку может оказаться нетривиальной задачей из-за их «неидиоматичности» с точки зрения раста.
Судя по примерам, что я видел, даже управление микроконтроллерами где нужно конкретные байтики по конкретным адресам руками писать чтобы что-то делать умудряются вполне идиоматично обернуть.
Не говоря уж о том, что из-за чрезмерных требований раста к идиоматичности подхода даже с растокодом как таковым вполне может получиться бида. Так вижу (С) :)
На самом деле это больше желание коммьюнити иметь чистый код, но никаких обязательств тут нет. Судя по тому что я видел, довольно часто люди применяют подход "генерирует раст код через c2rust (или похожую тулзу), а потом постепенно рефакторим и переписываем". Подход, успешно применяющийся на TS/JS проектах.
Так вижу (С) :)
Благодарю. С вами приятно вести диалоги.
Впрочем, на практике 30% потерь это ни о чем, и даже до 100% еще терпимо, так что спор скорее теологический.
В Си нет никаких накладных расходов на абстракции, т.к. нет абстракций. С этим невозможно конкурировать.
Во-первых в си есть абстракции.
Во-вторых у си есть семантика которую компилятор вынужден соблюдать даже когда выгоднее было поступить иначе. Gcc выполняет поистине титаническую работу чтобы заставить код работать быстро на железе, семантика которого сильно отличается от PDP-11.
Абстракции часто помогают ускорить программу, а не замедлить. Плюсовики со стажем думаю могут рассказать, как магия на шаблонах оказывается в рантайме быстрее голого си.
Да поэтому вопрос в том как можно сгенерировать оптимальный асм код за оптимальное время разработки и поддержки. Так то в бенчмарке можно вообще влепить асм вставку а потом говорить что язык самый быстрый :)
Gcc выполняет поистине титаническую работу чтобы заставить код работать быстро на железе, семантика которого сильно отличается от PDP-11Хотелось бы увидеть примеры. Насколько я знаю asm pdp-11, от x86 принципиально не отличается.
Абстракции часто помогают ускорить программу, а не замедлить. Плюсовики со стажем думаю могут рассказать, как магия на шаблонах оказывается в рантайме быстрее голого си.Абстракция абстракции рознь. Шаблоны — zero-cost абстракция, чистый inline, а вот лямбды — нет.
Не уходим с темы С — какие абстракции в С есть?
А с фига ли лямбды — не zero-cost? Или вы путаете лямбды и std::function?
Но вот в C# это уже не так. Значит зависит от языка и надо смотреть каждый случай.
Хотелось бы увидеть примеры. Насколько я знаю asm pdp-11, от x86 принципиально не отличается.
Ну три самых очевидных:
- с точки зрения сишного кода регистров 8, на практике — намного больше
- с точки зрения сишного кода память линейная, на практике — иерархическая
- с точки зрения сишного кода исполнение инструкций последовательное, на практике это не так.
Раст тут ничем не поможет, в этом плане он копирует семантику сишки, но в некоторых случаях — нет:
- сложение знаковых числел не является UB
- уникальные ссылки не алиасятся
- итераторы позволяют безопасно и бесплатно делать преобразования коллекций
- ...
Не уходим с темы С — какие абстракции в С есть?
Отвечу цитатой из статьи которая как мне кажется более чем подходит в данной ситуации:
You may have heard another slogan when talking about C: “C is portable assembler.” If you think about this slogan for a minute, you’ll also find that if it’s true, C cannot be how the computer works: there are many kinds of different computers, with different architectures. If C is like a version of assembly language that works on multiple computers with multiple architectures, then it cannot function exactly how each of those computers work simultaneously. It must hide details, or else it wouldn’t be portable!
That said, I think this fact is sort of irrelevant, because I don’t think people mean the phrase “C is how the computer works” literally. Before we can talk about that, let’s talk about the C abstract machine, and why many people don’t seem to understand this aspect of the C language.
В итоге он не может не быть абстракцией, иначе он не мог бы быть портабельным между архитектурами.
Но вот в C# это уже не так. Значит зависит от языка и надо смотреть каждый случай.
Ну так не надо переносить опыт оптимизации одного языка на другой, тем более когда они совершенно разного уровня и для разных целей.
Хотелось бы увидеть примеры. Насколько я знаю asm pdp-11, от x86 принципиально не отличается.А причём тут ASM? Отличие в архитектре процессора, а не в ASM.
Вернее на RISC — и в ARM, тоже, но на x86 — отличие скрыто… что не значит, что его нет.
Обращение в память на современных процессорах занимает в сотни раз (если зыбыть про кеши и прочий пайпланинг) или десятки раз (если про кеши и пайпланинг вспомнить) больше ресурсов, чем обращение к регистру.
Обрашение же в память на PDP-11 было достаточно быстрым для того, чтобы для большинства инструкций перенос на регистры замедлял их менее, чем вдвое.
Соотвественно весь язык построен, снизу доверху, на идее, что все данные лежат в памяти и только иногда, в особых случаях, переносятся в регистры (с помощью особой пометки
register
— и тогда у таких переменных нельзя брать адрес и т.д.).Именно так, кстати, работали ранние компиляторы для 8086 (какой-нибудь славный Turbo C 2.01 или Microsoft C 5.0).
Однако на современных процессорах это даст отвратительную производительность — потому да, разработчики из кожи вон лезут, чтобы автоматически перенести данные из памяти в регистры (чего изначальный C не предполагал). А пометку
register
вообще вроде в C++17 отменили (вроде — потому что компиляторы всё равно её ещё поддерживают… и игнорируют).Сейчас разработчики процессоров совместно с разработчиками компиляторов добиваются производительности собранных программ. Но как это должно влиять на язык и вообще должно ли — вот вопрос?
Соотвественно весь язык построен, снизу доверху, на идее, что все данные лежат в памятиИМХО язык привязан к логической архитектуре, для квантовой нужен новый. Но пока что у нас фон-неймановская и она внезапно с однородной памятью и последовательным выполнением команд.
С трактовкой register не согласен. Это скорее был хинт компилятору, т.к. оптимизаторов тогда еще не было.
ИМХО язык привязан к логической архитектуре, для квантовой нужен новый.
Хватит пожалуйста лепить слово "квантовый" на всё без разбору. "не как в си" и "квантовый" это не одно и то же.
Если "как в си" для вас кажется эквивалентным "единственно логичное" то вас пора немного оглянуться и посмотреть на мир вокруг.
Но пока что у нас фон-неймановская и она внезапно с однородной памятью и последовательным выполнением команд.
Что с последовательностью команд и фон-неймановскостью какого-нибудь SQL? Да, язык для запросов делался, но как пример пойдет.
пока что у нас фон-неймановская и она внезапно с однородной памятью и последовательным выполнением команд
Лол. Привезите мне сычуанский соус из прошлого в котором вы оказались пожалуйста.
Но пока что у нас фон-неймановская и она внезапно с однородной памятью и последовательным выполнением команд.Выключите кеши и после этого будете рассказывать сказки про «фон-неймановскую» архитектуру.
Фон-Неймановская архитектера у нас была полвека назад. Какой-нибудь C64 — можно считать фон-неймановским с той или иной степенью натяжки.
И вот уже 386й с кешом — это уже не фоннеймановская архитектура, двуядерный проц — тем более.
И да, 90% ресурсов в IT уходят на то, чтобы поддерживать иллюзию фон-неймановской архитектуры с однородной память и последовательным выполнением команд. Хотя ни того, ни другого и в помине нет.
С трактовкой register не согласен. Это скорее был хинт компилятору, т.к. оптимизаторов тогда еще не было.Это он в XXI веке хинтом стал. В оригинальной версии это не было хинтом. Это было требование.
Аналогично, про register советую сначала почитать в оригинале стандарт С89
3.5.1 Storage-class specifiers....A declaration of an identifier for an object with storage-class specifier register suggests that access to the object be as fast as possible. The extent to which such suggestions are effective is implementation-defined.[49]Какие то утверждения у вас… безосновательные.
Просто я Си учил как раз по оригиналу K&R
Наличие кэша не влияет на тип архитектуры, см определение.А вы сами-то на него пробовали смотреть?
Вот на этот вот пассаж, к примеру:
Команды и данные хранятся в одной и той же памяти и внешне в памяти неразличимы. Распознать их можно только по способу использования; то есть одно и то же значение в ячейке памяти может использоваться и как данные, и как команда, и как адрес в зависимости лишь от способа обращения к нему. Это позволяет производить над командами те же операции, что и над числами, и, соответственно, открывает ряд возможностей. Так, циклически изменяя адресную часть команды, можно обеспечить обращение к последовательным элементам массива данных.
Да, далее там написано «такой приём носит название модификации команд и с позиций современного программирования не приветствуется». Но проблема не в том, что «этот приём не фенфуен», проблема в том, что у нас архитектура не Фон-Неймановкая и для пересылки данных между кешом данных и команд требуется использования явных инструкций синхронизации.
Аналогично, про register советую сначала почитать в оригинале стандарт С89А C89 — в каком году вышел? В названии год вам о чём-нибудь говорит? А ничего, что Wikipedia пишет про C «появился в 1972»?
Изначально
register
было требованием, но когда Unix (или может он тогда был ещё Unics) начали портировать на разные архитектуры — это требование отменили, так как нельзя было заранее сказать сколько где регистровых переменных доступно (на MS-DOS их обычно было всего лишь две).Просто я Си учил как раз по оригиналу K&RЭто прекрасно, но если вы про знаменитую книжку — то это уже 1978й год. То есть уже после того, как было решено «отвязать» Unix (и, соответсвенно C) от архитектуры PDP (Interdata 8/32 была закуплена AT&T как раз с этой целью в 1977м году).
Если вы всё ешё судите о прошлом по принципу «ну я же там был, всё же помню, зачем мне книжки» — то советую от него отказаться. Ибо, увы и ах, человеческая память — это аналоговая DRAM. Считываение воспоминания уничтожает его, а на это место записывается «воспоминание о воспоминании». Через 3-5-7 лет то, что вы помните о событии и то, что там происходило в реальности — начинают отличаться довольно-таки изрядно. А о вещах полувековой давности если вы чего и вспомните — то это будет больше похоже на легенду, чем на быль… даже если «вы лично там были и всё видели».
Ссылка из K&R про регистровые. Что было до K&R не знаю, кажется BCPL
Воды много, а вот пруфов мало.
Пробовал. Читать надо абзац «Принципы фон Неймана»Ну да, «тут читаем, тут не читаем, тут рыбу заворачивали». Натягивание совы на глобус продолжается.
Что было до K&R не знаю, кажется BCPLДо K&R были Комментарии Лайонса к 6-й версии UNIX, с исходным кодом. Включающая в себя, помимо всего прочего, и описание и исходники C-компилятора.
И вот в этом самом конкретном исходном коде есть конкретная такая функция
decl1
в конкретном файле c03.c
которая конкретно так включает вот такой вот конкретный участок: } else if (dsym->hclass==REG) {
if ((type&TYPE)>CHAR && (type&XTYPE)==0
|| (type&XTYPE)>PTR || regvar<3)
error("Bad register %o", type);
dsym->hoffset = --regvar;
}
Который конкретно так ругается на конкретную такую попытку завесьти четвёртую регистровую переменную в одной функции (regvar
инициализируется в файле c02.c в функции с названием cfunc
и его начальное значание равно 5).Воды много, а вот пруфов мало.Будем дальше тянуть сову на глубус или признаем, наконец, что облажались?
Или опять будете петь, что то, на чём писали Unix его разработчики — это не C, а так, поделка, раз оно священной водой комитета по стандартизации не окроплено?
Сеанс некромантии это конечно интересно, хотя я уже и сказал что не в курсе тех времен в С. Завтра посмотрю, а пока просто замечу, что в PDP-11 было 6 общих регистров, а не 3 как вы тут утверждаете.
а пока просто замечу, что в PDP-11 было 6 общих регистров, а не 3 как вы тут утверждаете.Где я это утверждаю, извините?
Именно потому что в PDP-11 было 6 общих регистров
regvar
и устанавливается изначально в 5
. Потому что R6 и R7 трогать было нельзя. А вот чтобы регистровые переменные не заняли все регистры и осталось что-то для работы с обычными переменными — их количество и ограничивается тремя… жёстко ограничивается заметьте, никаких хинтов тут нету… это уже когда Unix начали портировать с PDP-11 оно хинтом стало…Сеанс некромантии это конечно интересноМы сейчас спутились во времена когда я ещё не родился, на самом деле. Ну люблю я это дело просто. Когда разбираешься в разных вещах в IT, то заглянуть в историю (настоящую, а не то, что в ваших воспоминаниях осталось) часто бывает очень полезно.
Многие вещи, которые вызывают недоумение — становятся вдруг резко понятными, почти очевидными. Потому что в те далёкие времена, когда они были сделаны — они действительно имели смысл!
А сегодня, очень часто — они просто мешают…
Моё мнение полностью опрокинул вот этот доклад:
youtu.be/PDSvjwJ2M80
Написать быстрый корректный код на расте имхо займет куда меньше времени чем на си.Меня удивило, что конкретно в Benchmarks Game исходники на Расте не короче, чем на С++. Потому я сильно сомневаюсь в этом утверждении.
Вроде же можно в Найтли флагом включить альясинг и таки проверить насколько он круто перфоманс поднимает.
И да, и нет.
На самом деле, просто нет. Особенно на Benchmarks Game
Вот например, угадайте язык программирования:
_mm_sub_pd(
_mm_mul_pd(distance, _mm_set1_pd(1.5)),
_mm_mul_pd(
_mm_mul_pd(_mm_mul_pd(_mm_set1_pd(0.5), dsquared), distance),
_mm_mul_pd(distance, distance),
),
)
Правильный ответ: почти все бенчмарки «n-body» на этом сайте имеют этот кусок кода.
К несчастью в бенчмарках все языки на горячем пути как правило только вызывают C функции (см _mm_* и unsafe). Поэтому бенчмарки сравнивают как оптимизаторы разных версий GCC и Clang умеют оптимизировать C код, вместо того чтобы действительно сравнивать языки.
Ну и ничего удивительного, что bleeding edge версия clang-9 (2019), побеждает GCC-9 (2018) на числодробильных задачах.
Насколько я понимаю у Rust есть в перспективе преимущество с алиайсингом. Сейчас Rust может выигрывать в задачах потому как к этому языку есть интерес скиловых разработчиков в плане свободного времени. Однако на мой взгляд в плане разработки Rust имеет ряд тенденций создание менее производительного кода чем скиловый C++ разработчик. Вот что мне не хватало:
- отсутствие placement new
- отсутствие указателей на методы
- отсутствие возможности использовать константы в растовых шаблонах (generics)
- interior mutability чтоб написать safe код — это дополнительная runtime проверка
- борьба с borrow checker решается при помощи clone() или штук вроде Rc
- встроенная проверка выхода за границы массива
Вообще borrow checker с одной стороны крутая штука, с другой стороны она довольно слабая в плане того что очень много вещей ей не поддаются на этапе компиляции и чтобы писать идиоматический rust приходится использовать штуки которые имеют runtime-cost либо делать их unsafe (а это вроде как считается стрёмным). Если использовать unsafe то вроде принципиально Rust и C++ должны быть теоретически одинаковыми. Не знаю как на это влияет тот факт что нарушение aliasing является UB в расте. С одной стороны знания об aliasing позволяет компилятору лучше оптимизировать, с другой стороны наличие мутабельных ссылок на один и тот же объект может давать преимущества в производительности?
Не знаю что за задачки у benchmark game, если это числодробительные алгоритмы, то это не очень показательно. Допустим у вас проект 1 миллион строк кода. Мое мнение что производительность Rust будет "распухать" там, поскольку вы вряд ли будете сильно оптимизировать как число-дробилки, так что код будет содержать дополнительные runtime-косты равномерно размазанные по проекту. С другой стороны у вас таком большом проекте появится большая уверенность в memory safety, частично за счет compile-time, частично за счет рантайм.
отсутствие placement new
Вроде же ещё в прошлый раз разобрались, что это не placement new отсутствует, а возможность статического выделения памяти для не-Sized объектов.
отсутствие указателей на методы
А куда они делись?
борьба с borrow checker решается при помощи clone() или штук вроде Rc
С borrow checker не надо "бороться", им надо пользоваться. И при чём тут вообще clone()
? С чем вы боретесь в плюсах когда передаёте структуру по значению?
Rust превосходит по производительности C++ согласно результатам Benchmarks Game