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

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

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров6.9K

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

Кстати, вот еще небольшой пример на тему изменяемых и неизменяемых ссылок. Ниже код перемножающий две матрицы 3x3:

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.

Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+5
Комментарии125

Публикации

Истории

Работа

QT разработчик
8 вакансий
Программист C++
133 вакансии
Rust разработчик
10 вакансий

Ближайшие события