Rust и C++ при создании астродинамической библиотеки

от автора

Моя прошлая статья заканчивалась тем, что у меня возникла проблема выбора на чем писать и я говорил, что в следующей части продолжу свое изложение как сравнение 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();       ................     } }

В чем принципиальное различие:

  1. В коде на Rust нет std::Rc, аналога std::shared_ptr из мира C++, мы оперируем только ссылками, при этом не теряя в надежности.

  2. В коде на 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *