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