Моя прошлая статья заканчивалась тем, что у меня возникла проблема выбора на чем писать и я говорил, что в следующей части продолжу свое изложение как сравнение Rust и C++. Но проблема на тот момент заключалось в том, что с первым из них я был знаком шапочно, и чтобы нести ахинею нужно было узнать его поглубже. И как оказалось этот процесс не очень простой.
Изначально я планировал еще привести сравнение производительности, но сейчас понимаю, что будет это не совсем корректно. «Почему?» – спросите Вы меня. Давайте разбираться вместе. Да, пока не начали, оговорюсь сразу, что в данной статье я решил не рассказывать о смысле приводимого кода, т.к. это сразу усложнит восприятие.
Вот есть такой код на плюсах (весь проект здесь):
class DriverState { public: ............................ const ISCPhaseStateVector& Variables() const{ return *StateVariables; } std::shared_ptr<const CartesianVariables> ToCartesianVariables( const RefFrameTransform::IReferenceFrame& referenceFrame) const = 0; virtual void SetState(std::shared_ptr<const ISCPhaseStateVector> buffer ,std::shared_ptr<const RefFrameTransform::IReferenceFrame> referenceFrame){ StateVariables = buffer; ReferenceFrame = referenceFrame; SetStateInternal(); } protected: virtual void SetStateInternal() = 0; std::shared_ptr<const ISCPhaseStateVector> StateVariables; std::shared_ptr<const RefFrameTransform::IReferenceFrame> ReferenceFrame; };
А вот то, что у меня получилось на Rust (весь проект здесь):
pub trait DriverState<'reference> { ......................... fn to_cartesian_var(&self) ->&CartesianVariables; fn to_cartesian_var_in_ref_frame(&self, reference_frame: &dyn IReferenceFrame, ) ->&CartesianVariables; fn set_state(&mut self, variables: &dyn ISCPhaseStateVector, reference_frame: &'reference dyn IReferenceFrame); } pub struct DriverStateCashed<'reference>{ reference_frame:&'reference dyn IReferenceFrame, cartesian: CartesianVariables, .................. } impl<'reference> DriverState<'reference> for DriverStateCashed<'reference> { fn set_state(&mut self, variables: & dyn ISCPhaseStateVector, reference_frame: &'reference dyn IReferenceFrame) { self.reference_frame = reference_frame; self.frame_code = self.reference_frame.hash_code(); ................ } }
В чем принципиальное различие:
-
В коде на Rust нет std::Rc, аналога std::shared_ptr из мира C++, мы оперируем только ссылками, при этом не теряя в надежности.
-
В коде на Rust отсутствует аналог метода const ISCPhaseStateVector& Variables(), и как следствие размер структуры уменьшается.
Почему так получается? Ответы разные, хотя и связанные. В первом случае все дело в том, чтоб работал метод ToCartesianVariables, необходимо помнить об referenceFrame, передающиеся в методе SetState.
В ходе проектирования и создания интерфейса не очень понятно стоимость копирования объекта (хотя из опыта можно и предположить, что минимальна, но это неточно). А раз так, то возникает передавать и указатель, единственный способ гарантировать в плюсах, что за вашим указателем что-то есть – это умные указатели и в частности shared_ptr.
Дальше в голове начинает крутится следующая мысль: «Нам все уши прожужжали, что сырые указатели это плохо, давай все передавать через shared_ptr, тем более один уже есть». Формируется эффект домино, и, вуаля, уже работаете только с ними.
В Rust все обстоит иначе. В нем есть такое понятие как «время жизни». Судя по статьям, у тех кто пытается изучает язык, это вызывает боль и недопонимание. Но это инструмент и его можно и нужно использовать. Например, здесь из архитектуры взаимодействия известно, что информация циркулирующая в DriverState никак не может пережить referenceFrame. И у нас есть возможность это указать:
pub trait DriverState<'reference>{ ................... fn set_state(&mut self, variables: &dyn ISCPhaseStateVector, reference_frame: &'reference dyn IReferenceFrame); }
Теперь все, кто реализует эту черту, должны содержать ограничение на время жизни и вот так это используем:
pub struct DriverStateCashed<'reference>{ reference_frame:&'reference dyn IReferenceFrame, .......... }
Т.е. теперь у нас есть гарантия, что за ссылкой что-то есть. И снова начинается эффект домино, но уже другой: «Слушай, всем или подавляющему большинству потребителей нужно будет только читать информацию, хранить ее в этом же им ни к чему, да и менять тоже, так что давай отдавать все по ссылкам». Существенных возражений, я придумать не смог.
Второй же случай возникает для C++ тоже возникает в ходе падения доминошек и мысля там такая: «Ну коль все передаем по умным указателям, так давай и этой. Ну, а коль передали, так давай хранить, так, на всякий случай».
В Rust все хорошо, пока не натыкаемся на сценарий использования:
fn propagate(&self, current_variables: &mut KeplerVariables, driver: &mut dyn PropagatorDriver)->bool { ..................................................... //driver_state_buffer реализует DriverState let driver_state_buffer= Rc::get_mut(&mut driver_state_rc).unwrap(); while driver.next_interation(&mut iteration, driver_state_buffer) { /// Вот здесь происходит измнение current_variables self.propagate_private(current_variables, iteration.d_time_sec, mean_motion, div_mean_motion, iteration.accuracy); driver_state_buffer.set_state(current_variables, frame); } ................ }
И если мы здесь попробуем исполнить что-то типа такого
pub struct DriverStateCashed<'reference>{ reference_frame:&'reference dyn IReferenceFrame, variables: &dyn ISCPhaseStateVector, .......... } impl<'reference> DriverState<'reference> for DriverStateCashed<'reference> { ......................... fn set_state(&mut self, variables: & dyn ISCPhaseStateVector, reference_frame: &'reference dyn IReferenceFrame) { self.reference_frame = reference_frame; self.variables = variables; ....................... } ..................... }
То нас будет ждать встреча с компилятором и его ошибками. Дело в том, что нельзя захватить неизменяемую ссылку, если есть изменяемая. И в попытках решить проблему ко мне пришло понимание (не сразу), что всем потребителям даже даром не сдался возвращаемый результат Variables(), они просто не знают что с ним делать.
Кстати, вот еще небольшой пример на тему изменяемых и неизменяемых ссылок. Ниже код перемножающий две матрицы 3×3:
Rust:
pub fn rxr(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3], atb: &mut[[f64; 3]; 3]) { for i in 0..3 { for j in 0..3 { let mut w = 0.0; for k in 0..3 { w += a[i][k] * b[k][j]; } atb[i][j] = w; } } }
C++:
void eraRxr(double a[3][3], double b[3][3], double atb[3][3]) { int i, j, k; double w, wm[3][3]; for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { w = 0.0; for (k = 0; k < 3; k++) { w += a[i][k] * b[k][j]; } wm[i][j] = w; } } // Здесь происходит копирование из w в atb eraCr(wm, atb); }
В С\С++ приходится создавать промежуточную матрицу, а потом из нее копировать, т.к. возможна ситуация что выходной массив совпадает с одним из входных, в Rust это невозможно. Но в случае однократного использования вы врядли заметите прирост производительности, т.к. в Rust перед вызовом мы должны проинициализировать, но в случае многократного уже должны.
Ладно, а если все-таки нам надо чтобы конструкция подобная случаем с Variables() работала, то что делать? В Rust есть небезопасное подмножество, переход к которому, к слову, не стоит ровным счетом ничего, в отличии от С# или Java. В нем, в частности можем перейти к сырым указателям и использовать подобную конструкцию:
unsafe{ let field_ptr = field as *const CashedVariables as *mut CashedVariables; .......... }
Но уже тут сам программист должен гарантировать, что при исполнении смертельного трюка факир не начудит.
Вернемся же к тезису некорректности сравнения. Практически все те улучшения, которые были сделаны на Rust можно сделать и на плюсах без ущерба безопасности, а те что нельзя, гарантировать их работу можно и неязыковыми способами. Поэтому сравнивать производительность этих двух языков в принципе некорректно.
Но вот чтоб при равных гарантиях безопасности достичь равность скорости исполнения, то для C++ надо приложить больше усилий.
Да я осознаю, что тест маленький (впрочем как и все тест), а жизнь большая и быть может в будущем я найду много случаев, когда последнее утверждение неверно, но сейчас я выбираю, что дальше развивать я свой проект на Rust.
ссылка на оригинал статьи https://habr.com/ru/post/720538/
Добавить комментарий