Comments 125
Поэтому сравнивать производительность этих двух языков в принципе некорректно
Неочевидно.
Если нужно сравнить эффективность, то это делается просто - пишем две программы, выполняющие одну и ту же задачу (то же перемножение матриц), берем какой-нибудь performance explorer и прогоняем через него обе программы.
Смотрим на время выполнения, потребление ресурсов (процессор, память...) и делаем выводы о том что нам в данном конкретном случае больше подходит.
Мне казалось, что вся статья обосновывает это. Хорошо, давайте так, у нас есть вроде один язык с++, но одна и тажа программа скомпилированная msvc, gcc, clang и т.д. будет выполняться за разное время. Но если мы возьмем и видоизменим для каждого компилятора, то сможем добиться равной производительности. Здесь ситуация схожа.
Уже сколько раз это проходили. В итоге заканчивается тем что начинают измерять скорость двух ассемблерных вставок. Сравнивать нужно только идиоматический с точки зрения конкретного языка код.
Речь не только о скорости. Хороший Performace Explorer покажет всю картинку - и время выполнения и потребление ресурсов с полной разблюдовкой по функциям

И там сразу видны узкие места.
Когда надо сравнить, например, два разных способа решения одной задачи - вполне годный инструмент для понимания что более приемлемо в каждом конкретном случае.
В данном случае сравниваем два разных языка с разными внутренними механизмами. И сравнение может быть весьма и весьма интересным. Главное - найти подходящий инструмент.
Да, но корректное сравнение может проводить на двух средних, хотя бы, проектах, имеющих туже функциональность и не испытавших сильного влияния друг друга. На малых тестовых примерах можно сравнить разве что работу компиляторов.
Естественно. Тут или пишутся специальные тесты (как правило, там прогоняются заведомо большие объемы), если речь идет о каком-то алгоритме или подходе к решению задачи, или это все проводится в рамках нагрузочного тестирования конкретной поставки и тогда там просто выносится вердикт "все ок, внедрение согласовано", или "вот тут у вас кто-то слишком много потребляет, нужно оптимизировать". Бывали случаи когда внедрение согласовано, но сам видишь что вот тут многовато лишних вызовов, которых можно избежать и скроить процентов 5-10 процессорного времени.
Я просто не знаю есть ли подобное на других платформах (у нас все-таки узкоспецифичная среда), но какие-то профайлеры же должны быть...
Ну так я не говорю, что в rust нет умного указателя, более того, я сам упоминаю об std:rc. Я говорю, что в коде на rust удалось от него избавиться.
А, тогда видимо не так понял. Прошу прощения.
А мне не понятен другой вопрос: раз уж вы избавились от него в коде на Rust, то почему в C++ не перешли на std::unique_ptr?
Потому что все они разделяемые ресурсы, да, во всех случая, кроме ReferenceFrame можно перейти вообще на сырые ссылки, но последний точно shared_ptr. Но у каждого программиста есть свои шаблоны в голове, когда он пишет код.
И моя основная мысль, что Rust помогает эти шаблоны немного облегчить.
наличие shared ptr вне многопоточного кода(и то в редких сценариях) это почти гарантированная ошибка проектирования.
И rust наоборот форсит шаред поинтеры, это просто факты и всё. Модель владения в расте прямо таки требует вставлять бесполезные Arc где ни попадя. И даже так вы не сможете менять его из двух разных мест(без мьютекса), компилятор заставит вас написать этот бесполезный мьютекс
Здесь автор хочет выразить мысль, что в *его коде* на Rust нет std::Rc. Потому что в такой конструкции отпала надобность.
Я так и не понял, кто же быстрее.
Кстати, раз уж решили писать на Расте, то советую активировать побольше всяких проверок при помощи clippy: там довольно много чего полезного есть, ну а еще поизучать стиль написания кода, принятый в сообществе, чтобы все было в как можно более едином стиле выдержано в соответствии с принятым в стандартной библиотеке и популярных проектах с crates.io
https://rust-unofficial.github.io/patterns/
https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/style.md
И вот сюда еще можно поглядывать (но проще всего врубить clippy pedantic и дальше отбирать те линты что реально понравились)
https://rust-lang.github.io/rust-clippy/master/
Спасибо, за ссылки. Не сомневаюсь, что будет очень полезно.
Можно не писать кучу default и просто "размазывать" всё что идёт по-умолчанию
// вместо
let mut result = DriverStateCashed::<'reference>{
reference_frame: frame,
frame_code: code,
cartesian: CartesianVariables::default(),
p: BTreeMap::default()
};
// делаем
let mut result = DriverStateCashed::<'reference>{
reference_frame: frame,
frame_code: code,
.. Default::default()
};
И с преобразованием типов у вас какая-то дичь. Вместо имлементации трейта From у вас какие-то unsafe, указатели и три этажа transform функций. Это такая мина отложенного действия, от которой лучше избавиться на ранней стадии, даже если это планируется отдавать как интерфейс для библиотеки.
Вместо имлементации трейта From у вас какие-то unsafe, указатели и три этажа transform функций.
Можно поподробнее. Если вы про это
unsafe{
let field_ptr = field as *const CashedVariables as *mut CashedVariables;
..........
}
То это используется здесь. Задача: нужно выполнить вычисления, но только в случае их надобности и закешировать. И мне тут не удается обойтись без того, что из &self сделать &mut self. Если подскажите как это сделать, буду только рад.
Удалять иммутабельность с самого себя плохая идея и необходимо использовать std::cell примитивы для подобного.
По-хорошему, если вы преобразуете один тип в другой, то нужно имплементировать трейт From<T1> for T2
. Сейчас у вас таким преобразованием занимается функция transform
, перегоняющая один тип координат в другой. По идее это должно превратиться во что-то вроде
self.variables = reference_frame.into()
// или
self.variables = Cartesian::from(reference_frame);
Да и выглядит, что кажется имеет смысл сделать в этой функции &mut self
, раз у вас там в нескольких местах функции всё меняется.
Помимо этого кажется не хватает тестов, покрывающих все эти функции.
Помимо этого кажется не хватает тестов, покрывающих все эти функции.
Тесты да, не хватает, буду писать. В свое оправдание могу сказать,
По-хорошему, если вы преобразуете один тип в другой, то нужно имплементировать трейт
From<T1> for T2
.
Это да, но мне кажется, что это должно быть что-то вроде "0 as f64" т.е. информация не меняется. А вот в преобразованиях переменных она меняется.
Причем для обсуждаемой функции причина, вынуждающая меня удалить иммутабельность - кеширование результата, т.к. меняется не только и не сколько тип переменных, сколько система координат (а вот тут могут сидеть значительные вычислительные сложности)
Удалять иммутабельность с самого себя плохая идея и необходимо использовать std::cell примитивы для подобного.
Если я правильно разобрался, то cell вызывает clone, т.е. это доп расходы, которые хочется избежать.
не сколько тип переменных, сколько система координат
Собственно про эти типы и речь. Бегло пробежался по коду и в большинстве случаев матрица 3х3 лежит просто во вложенных массивах, вместо чего-то типизированного. Как отличать в таком случае полярные координаты, от прямоугольных и галактических - мягко говоря непонятно. По-хорошему такое разруливается такой штукой как new type, когда некоторый простой тип оборачивается в более строгий тип (бесплатно, ибо zero cost abstraction)? пишутся имплы для перевода из одной системы в другую и уже на месте при необходимости вызывается from/into как я показывал выше. На самом деле уже есть несколько крейтов (из самых известных ndarray), которые предоставляют матричные типы и операции над ними, причём с оптимизациями и распараллеливанием, при необходимости.
Если я правильно разобрался, то cell вызывает clone, т.е. это доп расходы, которые хочется избежать.
Нет, RefCell позволяет использовать указатель на мутабельные данные. Что-то похожее на std::unique_ptr
в плюсах. Фактически cell часть кончается после borrow_mut. В примере clone()
используется потому что сигнатура функции требует возврата полного вектора, а не ссылки. То бишь значение копируется из кэша, а не перемещается из него. Ко всему компилятор очень неплохо оптимизирует иммутабельные данные, как в примере, так что избегание клонирования данных есть преждевременная оптимизация, лучше писать регрессионные бенчмарки для проверки и использовать инструменты типа flamegraph для поиска узких мест. При размерах вашей библиотеки пока нет смысла настолько беспокоиться за производительность, чтобы страдать от последствий неидиоматического кода.
Бегло пробежался по коду и в большинстве случаев матрица 3х3 лежит просто во вложенных массивах, вместо чего-то типизированного.
Постойте-постойте, в массовом виде матрицы 3х3 используется у меня в порте liberfa\sofa, там я так делаю для простоты портирования и дальнейшего поддержания в актуальном состоянии. В других ;t местах пользуюсь nalgebra. Да, чтобы отличить сферические от декартовых тут точно нужен новый тип. Да, Rust позволяет их обернуть бесплатно. Тут тоже хорошо. Но следующих ход уже ошибочен, так можно костей не собрать, если соберемся что-то менять. Не раз это проходил.
Нет, RefCell позволяет использовать указатель на мутабельные данные. Что-то похожее на
std::unique_ptr
в плюсах. Фактически cell часть кончается после borrow_mut.
Посмотрю на него повнимательней, похоже я что-то недопонял.
Ко всему компилятор очень неплохо оптимизирует иммутабельные данные, как в примере, так что избегание клонирования данных есть преждевременная оптимизация
Да, если компилятор знает что данные не меяется, то там целый ворохо оптимизаций. Но на инфраструктуру полагаться полностью не стоит. Она может поменяться, а потом разгребать это очень сложно.
Спасибо за конструктивное обсуждение, буду читать и думать.
Я давненько не брал в руки раст, но насколько я помню иметь одновременно иммутабельную и мутабельную ссылки на одно и то же - это не просто "не потокобезопасно", а самое натуральное UB по меркам раста, а это похоже именно то, что у вас происходит при работе с этим field_ptr
- есть одновременно иммутабельная ссылка через field
на структуру (которую вы потом используете для возврата результата) и мутабельная через field_ptr
+mut_var
на поле этой структуры.
Тогда тоже самый cell допускает ub. Т.к. этот трюк я взял из его кода.
Cell
работает через посредство UnsafeCell
, и очень может быть, что компилятор "подкручен" запрещать определенные оптимизации в случае доступа к данным, хранящимся в UnsafeCell
, а что позволено Юпитеру, то может быть не позволено быку. В общем, все это выглядит крайне сомнительно ИМХО и может сломаться в любой момент.
Если походить по всей стандартной библиотеке, то такое там сплошь и рядом. Юпитер, он конечно Зевс, но не до такой же степени....
Магия есть, но не чёрная. Это конкретный lang item, для которого (и только) компилятор знает об interior mutabilty.
Высокоуровневые же примитивы для interior mutabilty строятся на его основе.
Можно завернуть поле p
в std::cell::Cell
. Вроде только оно изменяется, но если нет то и другие изменяемые поля завернуть.
Если я правильно понимаю, там оно меняется через копирование.
Правильно. Если хочется избежать копирование можно использовать RefCell
, но с ним уже нужно быть внимательным: он переносит проверки подсчёта ссылок из компиляции в рантайм и можно словить панику создав 2 ссылки на содержимое если одна из них мутабельная. Например так:
let val = std::cell::RefCell::new(42);
let a = val.borrow();. // ок
let b = val borrow();. // ок
let c = val borrow_mut(); // паника
Еще раз спасибо за clippy это действительно очень полезный инструмент. Даже ошибки выгребает.
Кстати, а еще очень советую приделать Miri, ну вот у меня тут пример есть. Он позволяет выгребать кучу ошибок в unsafe коде
https://github.com/alekseysidorov/static-box/blob/master/.github/workflows/ci.yml#L58
Работает он с nightly тулчейном и xargo тулзой, но в нем там самом все описано.
https://github.com/rust-lang/miri#running-miri-on-ci

Пожалуйста не называйте это кодом на С++, спасибо.
Синтаксически он. А то, что стиль из учебника C 70-х, так C++ и так позволяет писать до сих пор. :)
Хорошо Си, но хрен редьки не слаще.
Плохонький код на С++ много с чем угодно можно сравнивать с негативным для первого результатом. Вот только сначала стоило бы навести порядок в плюсовом коде.
Ну, этот код из liberfa, очень популярной астрономической библиотеки.

Если что, С++ никак не связан с С, это максимально разные языки
К слову, в Си есть вот такой зверь - https://en.cppreference.com/w/c/language/restrict
Нужно для реализации примерно того же, что Вы показываете как достоинство Жравого
Как это здесь поможет? Необходимость в копии это не уберёт. А если её не делать, то просто ошибочную работу поменяем на ошибочную работу + UB. Сомнительное удовольствие.
Ой, начинается No True Scotsman. Хоть бы переписали его на "настоящий" С++ для начала, чтобы показать в чём автор ошибается (если вам так кажется).
возвращаешь не void, а матрицу
проблем больше нет
Для начала, не изобретать велосипед, а взять готовый
https://www.boost.org/doc/libs/1_81_0/libs/numeric/ublas/doc/overview.html
или
https://www.boost.org/doc/libs/1_81_0/libs/qvm/doc/html/index.html#_quaternions_vectors_matrices
Но если хочется стрелять по ногам, обернуть себе std::array
в матрицу class Matrix : public std::array<double, 9>{};
и реализовать всё на ссылкахvoid rxr(Matrix const& a, Matrix const& b, Matrix & atb)
А можно реализовать move-конструктор и оператор перемножения - и оставить работу компиляторуMatrix(Matrix && other) = default;
Matrix operator* (Matrix const& other) const;
/*...*/
Matrix a,b,с;
Matrix atb(a*b);
atb = std::move(atb*c);
Вам не для того дали сильно типизированный язык с оптимизирующим компилятором, чтобы вы всё на примитивных типах и ассемблерных вставках делали.
А в 23 стандарте можно даже перегрузить operator[](int i, int j)
М-да, даешь пример, чтобы подсветить идею, а тут начинается...
Вопрос не в том, как оптимальнейшим образом организовать перемножение матриц, а в том, что даже в относительно простых вещах можно ошибиться.
Если же хотите говорить о перемножении матриц, то возьмите eigen; реализация в boost медленнее.
Ну и матрицы можете все не 3x3, а 512x512, хотя бы.
Вопрос не в том, как оптимальнейшим образом организовать перемножение матриц, а в том, что даже в относительно простых вещах можно ошибиться.
Можно промахнуться даже мимо кнопки "сделать хорошо". Ошибиться можно и в Rust'е, там тоже полно подводных камней и неявных правил. Вам уже сверху накидали примеров. Константность по умолчанию важна только если ты впервые язык видишь и вообще ничего не читал. У меня сейчас на автомате const& пишется везде, а потом думается в другую сторону.
М-да, даешь пример, чтобы подсветить идею, а тут начинается...
Пардонте, нужно было пример лучше выбирать. На каком-нибудь 03 или 07 стандарте я бы согласился, что у плюсов всё очень не просто с выразительностью или с безопасностью. В 20+ к таким мелочам докапываться - это просто оскорбительно.
вы на основе этого делаете выводы о дизайне языков, где вам что то легче, где лучше производительность и проч. Как можно делать выводы написав какую-то фигню на сишке - непонятно
Ну вот, добавили овердофига бойлерплейта, а сам метод, к которому предъявлялись претензии, так и не реализовали, ну как же так xD
Кстати, как вы думаете, move конструктор будет как-то осмысленно отличаться от копирования в данном случае?
В общем, как же должен выглядеть "правильный идиоматичный код на С++", который бы не имел тех же проблем (из описанных в посте), что и текущий пример, так и осталось неясным.
Ну вот, добавили овердофига бойлерплейта, а сам метод, к которому предъявлялись претензии, так и не реализовали, ну как же так xD
Месье любит несвежие портянки?
Кстати, как вы думаете, move конструктор будет как-то осмысленно отличаться от копирования в данном случае?
Хм... А вот сейчас я задумался. У std::array
вообще есть конструктор перемещения? Вроде, нет. Ладно, уел)) А std::vector точно просто обменивает указатели при перемещении.
В общем, как же должен выглядеть "правильный идиоматичный код на С++", который бы не имел тех же проблем (из описанных в посте), что и текущий пример, так и осталось неясным.
Конкретный пример некорректен. Почему нельзя матрицу умножать на себя и поместить результат в себя же? Потому что какой-то DancingOnWater решил, что конкретно этот случай он не будет реализовывать? Тогда обложись assert'ами или поменяй интерфейс функции.
Но если ты хочешь при этом интерфейсе работоспособность оставить, тогда, конечно, нужна обвязка вроде такой.
Претензию к тому, что язык допускает передачу одной и той же переменной и как константную ссылку, и как не константную, и что разработчик 99% продолбится в глаза мимо этого случая, принимаю.
Все же хотите тащить код из стекфверфлоу в продакшен, ладно.
Представим что у нас матрицы, скажем, элементов по 25 тысяч и их перемножение происходит в цикле. Ваше решение с std:move в коечном итоге постоянно выделяет и убивает на куче эти 25 тысяч, сама по себе это операция не дешевая, так и память по-тихоньку фрагментируется и спустя какое-то время все становится совсем забавно.
Решение комментатора выше с std::move
- это по сути альтернатива копированию, потому что применяется только в случае, если выходной параметр и один из входных - это одно и то же. Что будет быстрее - обменяться указателями в конструкторе перемещения (или в соответствующем операторе присваивания) и доверить аллокатору все остальное ("все остальное" по сути - это пометить некий участок в памяти как свободный, для этого вовсе необязательно его целиком "пробегать", а фрагментация памяти при выделении кусков одного и того же размера в цикле весьма маловероятна, аллокатор переиспользует только что освобожденный) или все равно выделять временную матрицу, как в вашем примере на C, а потом ее еще и копировать - как думаете?
все остальное" по сути - это пометить некий участок в памяти как свободный, для этого вовсе необязательно его целиком "пробегать", а фрагментация памяти при выделении кусков одного и того же размера в цикле весьма маловероятна, аллокатор переиспользует только что освобожденный
В общем случае в цикле будет такая ситуация: аллокация выходного массива ->начало цикла->другие вычисления->аллокация выходного->перемножение матриц->обмен->высвобождение->новый виток.
Так вот в ходе других вычислений, как и здесь, тоже может выделяться память, вот именно в этот момент фрагментируется память.
Ну вот и вопрос, что будет дешевле - копирование матрицы целиком, как это происходит в вышеприведенном коде на C, или реаллокация. Лично я бы поставил на реаллокацию, потому что более-менее современные аллокаторы довольно хорошо работают с повторяющимися паттернами аллокации (переиспользуют, беря куски разных размеров из разных пулов, и применяют всякие другие эвристики). Это нужно мерять.
P.S. Да, при создании временной матрицы для последующего копирования тоже будет аллокация с последующим освобождением при выходе из функции, так что ответ "что дешевле" ИМХО очевиден :)
Да, согласен предложенный вариант iCpu быстрее, но в потенциале приводит к очень неприятной вещи.
Либо вам нужно иметь возможность перезаписывать одну из входных матриц, либо нет. И если это таки нужно, то в расте вы опять столкнетесь с приседаниями с борроу чекером, потому что он вам так просто перезаписать эту матрицу не даст, ибо если у вас есть мутабельная ссылка, то других быть не может, а C++ может предложить вполне доступный для понимания, логичный, эффективный и инкапсулированный в реализацию выбор без всяких приседаний.
В C++ к слову можно определить оператор *= и получить специализированную реализацию для этого случая.
Не, ну в расте ты тоже можешь реализовать трейт MulAssign
, но... Но. Большое такое но. Ты теряешь возможность обработки ошибок - даже те довольно слабые, что есть в других случаях. Исключений нет, а Option
или Result
вернуть некуда. Как по мне, это прямо-таки зияющий архитектурный просчёт, но любители раста пожимают плечами и говорят "просто не используйте перегрузку операторов" :)
А можно для тупых: а какие ошибки тут могут быть?
В перегруженных операторах-то? Да любые. Вот например пытаешься ты выделить память под временную матрицу, которую потом планируешь переместить в self
, а памяти-то и не хватает. В C++ ты выбросишь std::bad_alloc
, и сможешь обработать где-то выше, если захочешь, а тут? Как просигнализировать "наверх" о проблеме из выражения типа matrix *= another_matrix
?
Другой пример. Предположим, что матрица - это некий генерик, который умеет работать с любыми типами - хошь int
, хошь float
, и вот нужно поработать с неким типом, который, скажем, умеет ловить арифметические переполнения. В C++ - легко, если тип видит переполнение при какой-то там операции с собой, он выбрасывает исключение, оно проходит насквозь через матричныйoperator*=()
, и все довольны. Как подобный функционал организовать в расте?
Паники - те же исключения, так же можно поймать панику на OoM. Если важно отслеживать переполнения, то просто используешь checked_add. Арифметические операторы, бросающие исключения - сомнительная затея.
Паники - те же исключения
Смешно.
Арифметические операторы, бросающие исключения - сомнительная затея.
Да, это я уже слышал. Можете не трудиться.
Смешно.
А можно раскрыть мысль? Да, в раст (и го) сообществе принято говорить, что мол исключений в языке нет, но так-то паника не особо от них отличается: можно перехватить, узнать тип, пробросить дальше. Да это несколько более многословно, чем в "языках с исключениями", использовать панику для обработки ошибок не принято, плюс в библиотеках не стоит полагаться на выбранную стратегию обработки паники, но принципиальных отличий не вижу.
но так-то паника не особо от них отличается
A panic in Rust is not always implemented via unwinding, but can be implemented by aborting the process as well.
Более того, паники - это не исключения. У паник нет никаких гарантий по раскрутке стека, это запросто может быть просто вызов abort()
из libc.
В такой формулировке звучит как будто поведение чуть ли не случайно. На уровне исполняемого файла (то есть, не библиотеки) гарантии есть: поведение будет такое, какое мы захотим и по умолчанию это как раз раскрутка стека. Библиотекам действительно на это полагаться не стоит, но вообще есть костыль как на уровне библиотеки можно вызывать ошибку компиляции, если стратегия обработки паники не такая, как нам нужно.
Библиотекам действительно на это полагаться не стоит
О чем и речь - нет никаких гарантий, что на это можно полагаться.
есть костыль как на уровне библиотеки можно вызывать ошибку компиляции
Костыль есть, да. Но единого поведения нет, более того, одна библиотека может хотеть одну стратегию, а другая - другую. В целом забавно, что в принципе растаманы (не органчики-евангелисты, которые просто повторяют где-то услышанные тезисы, а думающие люди) уже понимают, что что-то тут не так, и городят костыли. Но костыли - это решение только наполовину, так сказать.
О чем и речь — нет никаких гарантий, что на это можно полагаться.
Если мы пишем не библиотеку или внутреннюю библиотеку, то есть.
Но костыли — это решение только наполовину, так сказать.
Я бы наоборот сказал, что раз это до сих пор костыль и нет ни ишью ни рфц, то не сильно оно и востребовано.
то не сильно оно и востребовано
Ну окей.
Понимаю как это звучит, но так-то завести ишью — ничего не стоит. Продумать и предложить RFC — несколько затратнее, но посильно и одному человеку и да, это может сделать любой. Если никто не собрался, то видимо действительно никому не мешает.
Вдогонку: так-то в С++ тоже можно собрать код с -fno-exceptions
и он может оказаться к этому не готов.
А давайте ещё представим что у нас многопоток? И не простой, а помесь OpenMP и MPI. Не будем расслабляться!
А потом давайте представим, что у нас могут быть MatrixView с частично пересекающимися областями, и что сравнения указателей не хватит. А чо нет-то?
Что ещё бы представить? Можно ещё разряженные матрицы. Или потоки матричных преобразований с оптимизатором. Или что-нибудь ещё в этом духе. Красиво жить не запретишь!
А, может, не будем представлять? Это такая редкая операция, что вы на неё положили болт 50ой резьбы. Вы её просто отбросили как невозможную в расте. Хотя, на деле, ничего невозможного в ней нет: прилетит ссылка через четвёртые руки - и всё равно заломает программу, так или иначе.
Хотите специальных оптимизаций - делайте специальные реализации. Пишите кастомный аллокатор, делайте статические переменные. Или добавляйте ссылку на буфер непосредственно в класс.
Нет-нет, вы стали критиковать код приведенный для примера (хотя и взятый из реального проекта), как код идущий на продакшен. Поэтому и ваше решение будет рассматриваться так же.
И да, в расте это возможно, но если где-то четвертые руки переходят на небезопасное подмножество.
Вы указали на безошибочный код с лишним копированием и сказали: "А на Rust мне язык не даёт так сделать."
Я показал корректный код, в котором лишнее копирование никогда не происходит. При этом, то же самое можно сделать через простую переменную и копирование, просто тогда всегда будет оверхед по памяти.
Но вы не довольны. Хотя код на расте явно дырявый: компилятор не может бесконечно глубоко в дерево уходить, на каком-нибудь пуле объектов всё закончится. И даже если у вас попытку перезаписи поймает менеджер памяти, где он это сделает и куда вывалится? У вас есть там нужная обработка? Нет? Ой-ой-ой...
Но если хотите, почему нет? Проведите тесты производительности в оптимистичном (частота помещения в себя 0%), обычном (5%), пессимистичном (30%) и терминальном (100%). В один и несколько потоков. Запишите график использования и фрагментации. Потом повторите, запихнув между умножениями другие операции. Не забываем проверять с разными ключами компилятора: не только с -O, но и c SSE\AVX, с необычными оптимизациями разных компиляторов. Мы же взрослые дяди? Можно ещё и необычные std подключать, реализация от микрософта или gcc далеко не всегда и не во всём топчик. И тестить на всех системах, от древних Windows до новых Android, промежая сборками под микроконтроллеры и одноплатники.
Посмотрим на фрагментацию памяти во всех режимах!
Это же вы хотите всё делать как в продакшене? Прошу! Доказывайте, как серьёзный дядя, что такая реализация хуже, по сути, никакой в Расте.
Если вы считаете, что код на расте, как вы говорите, "явно" дырявый - так покажите это на простом и явном примере. Вот возьмите и напишите его.
Ну, то есть, спора по поводу явной большей семантической корректности Сишного и Плюсового кода нет? Ок.
Я на расте не писатель. И не собираюсь. Но я точно знаю, что дерево поиска ошибок у любого компилятора конечно. Нам просто нужно попасть на один уровень глубже.
Потому что & и &mut на расте - это прямое переиспользование сишного restrict. И способы обхода этого ключевого слова на Си известны.
Ну, то есть, спора по поводу явной большей семантической корректности Сишного и Плюсового кода нет?
А я разве спорил с этим? Указанная мной проблема характерна для обоих языков.
Ставя эквивалент между restrict и ссылками раста вы делаете ошибку.
Ладно, согласен, неправильно выразился. llvm использует код для сишного restrict при оптимизации использования ссылок. Из-за этого в определённый момент такая оптимизация не работала.
А можно поинтересоваться, где в астродинамической библиотеке используются такие матрицы?
В астродинамике подобные матрицы возникают, например, при уточнении орбиты.
Имеется в виду уточнение орбит для большого количества измерений? Или траекторные измерения?
Что-то я не соображу, где там именно умножение матриц. Можно ссылку на какие-то материалы с описанием этого дела? Можете спокойно переходить на слэнг, я в теме (поэтому и пришёл в статью).
Как я понял, вы собираетесь писать Open Source проект, а значит от того, сможете ли вы привлечь единомышленников и помощников, зависит будущее проекта. С этой точки зрения я бы С++ вообще не рассматривал.
Поцчему?
Не особо мне верится, что есть большое количество желающих им заниматься в 2023м, но я могу ошибаться. Хотя вот по статистике гитхаба типа на 4м месте. Ну не знаю...
Ну конечно питончик или яваскрипт его обходят, что в общем-то неудивительно, но он несравненно популярнее раста :) Понятно, что условные веб-разработчики (процент которых среди "разработчиков вообще" довольно велик) этим не заинтересуются, но для них ли этот проект вообще, и будут ли они в нем полезны?... Вопросы, вопросы...
Раст на хайпе, и сейчас делать на нём небольшой open source выгоднее, в плане привлечения разработчиков. Чем пользуются крестовые проекты, теряющие активных разрабов - переходят на раст.
Что-то я не припомню каких-либо более-менее известных плюсовых проектов, "перешедших на раст". "Перейти на раст" можно разве что тогда, когда ещё практически ничего нет, ну или проект очень простой, который можно взять и переписать по сути с нуля в одно рыло, иначе заниматься этим смысла вообще нет. От людей со стороны, которые раст "изучили" на хайпе вчера (а скорее всего - только изучают), а проект увидели сегодня, как баран - новые ворота, при таком "переходе" вряд ли будет большой толк: наступят на все старые уже пройденные грабли, и добавят к ним новые. Это все иллюзии.
P.S. Вот этот комментарий к прошлой статье был очень правильный. И сейчас автор вместо построения и реализации мат модели с переменным успехом сражается с борроу чекером. Все-таки "изучить язык" и "сделать продукт" - это очень разные задачи.
Да, я потратил время на освоение языка, но не считаю, что это было сделано зря. Я уже бывал в ситуации, что неверный выбор языка и фреймворка в начале, приводил к слишком большим трудностям потом.
Плюсы не без недостатков, как по мне, раст позволит двигаться быстрее, тратя меньше времени на отладку. А еще будет больший шанс, что проект будет использоваться в бортовом коде.
А еще будет больший шанс, что проект будет использоваться в бортовом коде.
Тогда уж стоит на MISRA затачиваться, а не на раст. Раст вряд ли в обозримом будущем будет для чего-то такого где-либо сертифицирован, уж больно он... in flux.
Не веду список проектов, переползающих на раст, однако они попадаются в ленте. Из последнего, что помню: Fish собираются портировать на раст. Интересно будет осенью посмотреть на их прогресс.
...при таком "переходе" вряд ли будет большой толк: наступят на все старые уже пройденные грабли, и добавят к ним новые. Это все иллюзии.
Это из серии "некогда точить топор, надо рубить". Конечно, при изучении нового инструмента, немалое количество времени уйдёт на базу.
Ну с большой помпой рекламировавшаяся в свое время попытка портировать emacs на раст загнулась. Что случилось с рыночной долей файрфокса пока они "точили топор" думаю все помнят - падение до единиц процентов на десктопе и до долей процента на мобилке. Они точили топор, пока основной конкурент ускорял работу и уменьшал энергопотребление. Про этот fish я честно говоря впервые слышу. Руководствоваться только пионерскими лозунгами про модность и современность, и громкие заявления типа "никто не любит xxx", которыми являются по сути 3 из 4 аргументов в пользу riir по вашей ссылке (только один аргумент из четырёх технический, про облегчение concurrency) - это не инженерный подход как по мне. Впрочем как хотят, у каждого свои... развлечения.
Смотря какой проект. Вон люди пишут эмуляторы и проблем с привлечением людей нет. С другой стороны, если хочется какой-нибудь сервер или веб-фреймворк забацать, но на чём-то быстром, то тут уже кресты не самым лучшим выбором с такой точки зрения будет, да.
.
Но вот чтоб при равных гарантиях безопасности достичь равность скорости исполнения, то для C++ надо приложить больше усилий.
Кстати, раз уж речь выше зашла о явной недостаточности (если не применить тут другое слово) механизмов обработки ошибок в расте, вот еще информация к размышлению - на тему "больше усилий для равной скорости исполнения". Все знают, что в C++ есть создание объектов in-place, т.е. ты можешь написать так (утрированный пример):
struct S
{
S(SomeFile & f) : buf(f), [...] {}
SomeBuf buf;
[...]
};
std::vector<S> v;
while(...) {
v.emplace_back(f);
}
Что здесь происходит, точнее, не происходит? Не происходит лишних копирований. emplace_back()
сразу использует буфер, выделенный вектором, экземпляр S
и его полеbuf
инициализируются in-place, читая данные из SomeFile
, если что-то случилось, то выбрасывается исключение, которое будет где-то выше по стеку обработано со всеми RAII гарантиями. Все работает очевидным и естественным образом.
В расте ничего аналогичного нет, и быть не может без коренных изменений в механизмах обработки ошибок. В расте чтение из SomeFile
"по канону" должно вернуть Result
, который нужно сначала проверить, то есть ты пишешь что-нибудь эдакое:
match f.read() {
Err(why) => [...],
// Не помню точно, будет ли тут работать shorthand syntax, предположим, что да
Ok(buf) => v.push(S{buf, [...]}),
}
Здесь экземпляр S
будет сначала создан на стеке, прочтенный buf
будет скопирован в него, затем экземпляр S
будет скопирован из стека в Vec
.
Что же мешает в расте сделать все более эффективно, и почему это до сих пор не сделано? По моему мнению, главным образом мешает недостаточность механизмов обработки ошибок. Ведь если конструировать объект in-place, как push()
(точнее, его in-place аналог) сможет сообщить об ошибке, скажем, при чтении из файла в буфер? Ведь никаких механизмов для этого нет, а паниковать на каждый чих (здравствуйте, mission-critical и functional safety systems) устраивает не всех, далеко не всех.
Если вы намекаете на исключения, то у них есть одна неприятная особенность: повсеместное их использование приводит к тому, что вся обработка сводится к тому, чтобы перехватить исключение, дабы она не завалил программу.
Не знаю зачем вам такая структура, но первое что напрашивается - это создать trait, а потом его имплимитировать для SomeFile.
Опять же не настаиваю, но я уже не раз замечаю, что привычны и наработанные шаблоны создают плохой код на Раст и надо их перестраивать.
вся обработка сводится к тому, чтобы перехватить исключение, дабы она не завалил программу
Ошибка ввода-вывода - не повод валить программу. Это может быть и должно быть корректно обработано.
Опять же не настаиваю, но я уже не раз замечаю, что привычны и наработанные шаблоны создают плохой код на Раст и надо их перестраивать.
Покажите пример "хорошего идиоматичного кода на Раст", чтобы в аналогичной ситуации объект создавался сразу в области памяти вектора, ну и ошибки пробрасывались вверх и обрабатывались. Words are cheap. Или это тоже "не нужно"?
Покажите пример "хорошего идиоматичного кода на Раст", чтобы в аналогичной ситуации объект создавался сразу в области памяти вектора, ну и ошибки пробрасывались вверх и обрабатывались. Words are cheap. Или это тоже "не нужно"?
Я же сказал, что не знаю, какую задачу вы решаете, но я бы пробовал вариант:
let mut v: Vec<SomeFile>;
......
while ...
{
match f.read() {
Err(why) => [...],
Ok(buf) => v.push(buf),
}
}
И что здесь, buf
читается сразу в область памяти, выделенную в векторе? Нет, этого тут не происходит. Это вообще ерунда какая-то.
Повторюсь, я даже приблизительно не знаю логику вашей задачи, но можно пробовать такие варианты:
let mut v: Vec<SomeBuf>;
......
for buf in v.iter_mut
{
if Err(why) = f.read(buf){
.....
}
}
Здесь не поддерживается целостность инварианта. Хочу обратить ваше внимание на то, что в оригинальном коде на C++ SomeBuf
- это всего лишь часть другого объекта, и этот объект либо создаётся целиком в случае успешного чтения в буфер, либо не создаётся вовсе. Здесь это не так: во-первых, внешнего объекта нет вовсе, а во-вторых у SomeBuf
есть промежуточное состояние "создан, но не инициализирован". Процесс заполнения вектора неинициализированными экземплярами SomeBuf
также опущен.
В третий раз говорю, что я не знаю вашей задачи. Немногим выше вы использовали emplace_back, т.е. добавляли в конец контейнера новый элемент. Потом выяснилось, что нужно использовать область памяти уже выделенную вектором.
Теперь новые вводные, которые уже вовсю опираются на ту логику, что скрыта от меня. Здесь я не могу помочь.
Но еще раз хочу написать: раст заставляет задуматься о шаблонах в своей голове.
Немногим выше вы использовали emplace_back, т.е. добавляли в конец контейнера новый элемент. Потом выяснилось, что нужно использовать область памяти уже выделенную вектором.
Вы не знаете, как работает emplace_back
, верно? Так прочтите. Он именно что использует область памяти, уже выделенную вектором.
Но еще раз хочу написать: раст заставляет задуматься о шаблонах в своей голове.
Чтобы сравнивать шаблоны в разных языках, нужно эти шаблоны знать. Иначе сравнивать будет нечего, вы будете не в состоянии оценить ни плюсы, ни минусы, и не сможете сделать осознанный выбор (не основанный просто на евангелист-пропаганде).
Appends a new element to the end of the container. The element is constructed through std::allocator_traits::construct, which typically uses placement-new to construct the element in-place at the location provided by the container. The arguments
args...
are forwarded to the constructor as std::forward<Args>(args)...
Взял отсюда. Т.е. это вот совсем не гарантированное поведение.
Т.е. это вот совсем не гарантированное поведение.
Если аллокатор предоставляет метод construct()
, то будет вызван он (с теми параметрами, что были переданы в emplace_back
), и аллокатор сам решит, что ему делать. Если метод construct()
у аллокатора отсутствует, то будет использован placement new напрямую. Стандартный аллокатор создает объект сразу на месте, но вы можете использовать нестандартные аллокаторы, при желании. Все в ваших руках.
Оки доки. Но вторая схема принципиально не меняется. Обработка ошибок может быть именно такой, без match.
Во второй схеме вы сначала должны будете заполнить вектор экземплярами класса, правильно? Покажите, пожалуйста, как вы это делаете без их предварительного создания на стеке а затем копирования в вектор, очень интересно будет взглянуть. На всякий случай напоминаю, что vec![T; n]
использует std::vec::from_elem
, для работы которого T
должен иметь трейт Clone
, то есть вы накопируете пустых (точнее, незаполненных/неинициализированных) буферов из стека в вектор (кстати, интересно, откуда вы заранее знаете, сколько их там надо накопировать, ну да ладно), а потом будете их заполнять, вместо заполнения буфера на стеке, а потом опять же копирования его в вектор. Те же яйца, только в профиль, как полагаете?
Моя больная голова наконец сообразила, чего вы хотите.
В безопасном, сиречь идиоматическом, Раст это сделать нельзя, т.к. при возникновении ошибки у вас окажется мусор в памяти. Но можно сделать все через небезопасное подмножество.
Это уже не будет "идиоматический раст", ну и цена ошибки будет высока, потому что небезопасный раст, так сказать, "ещё небезопаснее", чем C или C++, ибо требования, предъявляемые к unsafe коду, выше, и не всегда очевидны, как уже тут обсуждалось - можно даже взять код из растового std
, а потом выяснить, что он работает лишь потому, что компилятор подпилен конкретно под это место в std
и ни под какое другое :)
Все может быть, но и приведенный вами код на плюсах отнюдь не безопасен. Если исключение обрабатывается внутри s, то вы получите, мусор в памяти, а у внешнего кода возникают трудности узнать об ошибке. Если же конструктор прокидывает исключение, то можете получить невалидный контейнер, а можете не получить. В зависимости от реализации std.
Кстати, хорошая иллюстрация почему в соглашении Qt прописано, чтобы конструктор не кидал исключений.
Если исключение обрабатывается внутри s
Тогда поддержание собственного инварианта - его задача. Но в той форме, что я написал в оригинальном комментарии, это не так.
Если же конструктор прокидывает исключение, то можете получить невалидный контейнер, а можете не получить. В зависимости от реализации std.
Нет. Сам по себе emplace_back
гарантирует, что если именно конструируемый элемент выбросит исключение, то последствий не будет. Если std
не реализует такое поведение, то это std
не соответствует стандарту :) Но как водится в векторах добавление элемента может вызвать реаллокацию, и вот при реаллокации, если конструктор перемещения у T не noexcept
и выбросит исключение при перемещении какого-то уже добавленного в вектор элемента из старой области памяти вектора в новый, то вектор действительно не будет знать, что делать. Тут гарантий никаких. Поэтому их и рекомендуют делать noexcept
и не особо замороченными - свап-свап-свап.
Но даже в этом случае вектор будет стараться поддерживать инвариант: если он увидит, что конструктор перемещения не noexcept
, то он будет использовать конструктор копирования, если он есть, и откатит операцию, если что-то пошло не так (просто уничтожит свой новый буфер с уже скопированными элементами). Если же его нет, то да, будет использовать не noexcept
перемещение без гарантий.
Можете поиграться. Допишите к конструктору перемещения noexcept
и наблюдайте результат.
Кстати, хорошая иллюстрация почему в соглашении Qt прописано, чтобы конструктор не кидал исключений.
Qt - это вообще "C с классами". Им можно. Они никакие exception guarantee не считают нужным реализовывать ни в контейнерах, нигде.
Всё там безопасно, если в любом методе вектора кидается исключение, то вектор остается таким же каким был до вызова метода. Это базовая гарантия всех классов в стандартной библиотеке С++
Строго говоря наблюдаемое состояние вектора остаётся тем же.
На мусор в памяти C++ вообще кладёт большой болт и затягивает его гайкой, но это в его природе. Тут встаёт вопрос только когда надо не допустить утечки в "мусорную" память чувствительных данных.
На мусор в памяти болт кладут все. Те же аллокаторы (в том числе и в расте) не имеют привычки забивать освобождённые области памяти нулями или ещё чем (за исключением специальных отладочных режимов), потому что в общем случае это только гробит производительность без какого-либо профита. Когда стек двигается туда-сюда, там тоже нигде ничего специально не затирается.
Такое впечатление, что мы про разные вещи говорим.
Ситуация раз: что-то больше не нужно и должно быть освобождено (как в случае с не сумевшим себя создать элементом вектора). Ситуация два: мы получили кусок памяти под переменную и должны будем с ним сейчас работать.
То, что по вашей ссылке - это про ситуацию два, чтобы инициализация локальных переменных не выполнялись и в них остался мусор после ситуации раз. На ситуацию раз это никак не влияет.
.
Rust и C++ при создании астродинамической библиотеки