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

Комментарии (125)


  1. SpiderEkb
    00.00.0000 00:00
    +1

    Поэтому сравнивать производительность этих двух языков в принципе некорректно

    Неочевидно.

    Если нужно сравнить эффективность, то это делается просто - пишем две программы, выполняющие одну и ту же задачу (то же перемножение матриц), берем какой-нибудь performance explorer и прогоняем через него обе программы.

    Смотрим на время выполнения, потребление ресурсов (процессор, память...) и делаем выводы о том что нам в данном конкретном случае больше подходит.


    1. DancingOnWater Автор
      00.00.0000 00:00
      +5

      Мне казалось, что вся статья обосновывает это. Хорошо, давайте так, у нас есть вроде один язык с++, но одна и тажа программа скомпилированная msvc, gcc, clang и т.д. будет выполняться за разное время. Но если мы возьмем и видоизменим для каждого компилятора, то сможем добиться равной производительности. Здесь ситуация схожа.


    1. menstenebris
      00.00.0000 00:00
      +6

      Уже сколько раз это проходили. В итоге заканчивается тем что начинают измерять скорость двух ассемблерных вставок. Сравнивать нужно только идиоматический с точки зрения конкретного языка код.


      1. SpiderEkb
        00.00.0000 00:00
        +1

        Речь не только о скорости. Хороший Performace Explorer покажет всю картинку - и время выполнения и потребление ресурсов с полной разблюдовкой по функциям

        И там сразу видны узкие места.

        Когда надо сравнить, например, два разных способа решения одной задачи - вполне годный инструмент для понимания что более приемлемо в каждом конкретном случае.

        В данном случае сравниваем два разных языка с разными внутренними механизмами. И сравнение может быть весьма и весьма интересным. Главное - найти подходящий инструмент.


        1. DancingOnWater Автор
          00.00.0000 00:00
          +2

          Да, но корректное сравнение может проводить на двух средних, хотя бы, проектах, имеющих туже функциональность и не испытавших сильного влияния друг друга. На малых тестовых примерах можно сравнить разве что работу компиляторов.


          1. SpiderEkb
            00.00.0000 00:00

            Естественно. Тут или пишутся специальные тесты (как правило, там прогоняются заведомо большие объемы), если речь идет о каком-то алгоритме или подходе к решению задачи, или это все проводится в рамках нагрузочного тестирования конкретной поставки и тогда там просто выносится вердикт "все ок, внедрение согласовано", или "вот тут у вас кто-то слишком много потребляет, нужно оптимизировать". Бывали случаи когда внедрение согласовано, но сам видишь что вот тут многовато лишних вызовов, которых можно избежать и скроить процентов 5-10 процессорного времени.

            Я просто не знаю есть ли подобное на других платформах (у нас все-таки узкоспецифичная среда), но какие-то профайлеры же должны быть...


  1. daniilpashin
    00.00.0000 00:00
    +1

    В коде на Rust нет std::Rc, аналога std::shared_ptr из мира C++

    Есть и есть


    1. DancingOnWater Автор
      00.00.0000 00:00
      +4

      Ну так я не говорю, что в rust нет умного указателя, более того, я сам упоминаю об std:rc. Я говорю, что в коде на rust удалось от него избавиться.


      1. daniilpashin
        00.00.0000 00:00
        +2

        А, тогда видимо не так понял. Прошу прощения.


      1. funny_falcon
        00.00.0000 00:00

        А мне не понятен другой вопрос: раз уж вы избавились от него в коде на Rust, то почему в C++ не перешли на std::unique_ptr?


        1. DancingOnWater Автор
          00.00.0000 00:00
          +1

          Потому что все они разделяемые ресурсы, да, во всех случая, кроме ReferenceFrame можно перейти вообще на сырые ссылки, но последний точно shared_ptr. Но у каждого программиста есть свои шаблоны в голове, когда он пишет код.

          И моя основная мысль, что Rust помогает эти шаблоны немного облегчить.


          1. Kelbon
            00.00.0000 00:00
            -3

            наличие shared ptr вне многопоточного кода(и то в редких сценариях) это почти гарантированная ошибка проектирования.

            И rust наоборот форсит шаред поинтеры, это просто факты и всё. Модель владения в расте прямо таки требует вставлять бесполезные Arc где ни попадя. И даже так вы не сможете менять его из двух разных мест(без мьютекса), компилятор заставит вас написать этот бесполезный мьютекс


    1. OldFisher
      00.00.0000 00:00
      +10

      Здесь автор хочет выразить мысль, что в *его коде* на Rust нет std::Rc. Потому что в такой конструкции отпала надобность.


  1. dyadyaSerezha
    00.00.0000 00:00

    Я так и не понял, кто же быстрее.


    1. DancingOnWater Автор
      00.00.0000 00:00
      +3

      Если вкратце - никто. Ни тот, ни тот язык не имеют внутренних ограничений. Но на практике Rust дает больше шансов оставить в продакшане более быстрый код.


  1. Gorthauer87
    00.00.0000 00:00
    +7

    Кстати, раз уж решили писать на Расте, то советую активировать побольше всяких проверок при помощи 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/


    1. DancingOnWater Автор
      00.00.0000 00:00

      Спасибо, за ссылки. Не сомневаюсь, что будет очень полезно.


      1. domix32
        00.00.0000 00:00
        +1

        Можно не писать кучу 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 функций. Это такая мина отложенного действия, от которой лучше избавиться на ранней стадии, даже если это планируется отдавать как интерфейс для библиотеки.


        1. DancingOnWater Автор
          00.00.0000 00:00

          Вместо имлементации трейта From у вас какие-то unsafe, указатели и три этажа transform функций.

          Можно поподробнее. Если вы про это

          unsafe{     
            let field_ptr = field as *const CashedVariables as *mut CashedVariables;     
            .......... 
          }

          То это используется здесь. Задача: нужно выполнить вычисления, но только в случае их надобности и закешировать. И мне тут не удается обойтись без того, что из &self сделать &mut self. Если подскажите как это сделать, буду только рад.


          1. domix32
            00.00.0000 00:00
            +3

            Удалять иммутабельность с самого себя плохая идея и необходимо использовать std::cell примитивы для подобного.

            По-хорошему, если вы преобразуете один тип в другой, то нужно имплементировать трейт From<T1> for T2. Сейчас у вас таким преобразованием занимается функция transform, перегоняющая один тип координат в другой. По идее это должно превратиться во что-то вроде

            self.variables = reference_frame.into()
            // или
            self.variables = Cartesian::from(reference_frame);

            Да и выглядит, что кажется имеет смысл сделать в этой функции &mut self, раз у вас там в нескольких местах функции всё меняется.

            Помимо этого кажется не хватает тестов, покрывающих все эти функции.


            1. DancingOnWater Автор
              00.00.0000 00:00

              Помимо этого кажется не хватает тестов, покрывающих все эти функции.

              Тесты да, не хватает, буду писать. В свое оправдание могу сказать,

              По-хорошему, если вы преобразуете один тип в другой, то нужно имплементировать трейт From<T1> for T2

              Это да, но мне кажется, что это должно быть что-то вроде "0 as f64" т.е. информация не меняется. А вот в преобразованиях переменных она меняется.

              Причем для обсуждаемой функции причина, вынуждающая меня удалить иммутабельность - кеширование результата, т.к. меняется не только и не сколько тип переменных, сколько система координат (а вот тут могут сидеть значительные вычислительные сложности)

              Удалять иммутабельность с самого себя плохая идея и необходимо использовать std::cell примитивы для подобного.

              Если я правильно разобрался, то cell вызывает clone, т.е. это доп расходы, которые хочется избежать.


              1. domix32
                00.00.0000 00:00
                +3

                 не сколько тип переменных, сколько система координат

                Собственно про эти типы и речь. Бегло пробежался по коду и в большинстве случаев матрица 3х3 лежит просто во вложенных массивах, вместо чего-то типизированного. Как отличать в таком случае полярные координаты, от прямоугольных и галактических - мягко говоря непонятно. По-хорошему такое разруливается такой штукой как new type, когда некоторый простой тип оборачивается в более строгий тип (бесплатно, ибо zero cost abstraction)? пишутся имплы для перевода из одной системы в другую и уже на месте при необходимости вызывается from/into как я показывал выше. На самом деле уже есть несколько крейтов (из самых известных ndarray), которые предоставляют матричные типы и операции над ними, причём с оптимизациями и распараллеливанием, при необходимости.

                Если я правильно разобрался, то cell вызывает clone, т.е. это доп расходы, которые хочется избежать.

                Нет, RefCell позволяет использовать указатель на мутабельные данные. Что-то похожее на std::unique_ptr в плюсах. Фактически cell часть кончается после borrow_mut. В примере clone() используется потому что сигнатура функции требует возврата полного вектора, а не ссылки. То бишь значение копируется из кэша, а не перемещается из него. Ко всему компилятор очень неплохо оптимизирует иммутабельные данные, как в примере, так что избегание клонирования данных есть преждевременная оптимизация, лучше писать регрессионные бенчмарки для проверки и использовать инструменты типа flamegraph для поиска узких мест. При размерах вашей библиотеки пока нет смысла настолько беспокоиться за производительность, чтобы страдать от последствий неидиоматического кода.


                1. DancingOnWater Автор
                  00.00.0000 00:00
                  +2

                   Бегло пробежался по коду и в большинстве случаев матрица 3х3 лежит просто во вложенных массивах, вместо чего-то типизированного.

                  Постойте-постойте, в массовом виде матрицы 3х3 используется у меня в порте liberfa\sofa, там я так делаю для простоты портирования и дальнейшего поддержания в актуальном состоянии. В других ;t местах пользуюсь nalgebra. Да, чтобы отличить сферические от декартовых тут точно нужен новый тип. Да, Rust позволяет их обернуть бесплатно. Тут тоже хорошо. Но следующих ход уже ошибочен, так можно костей не собрать, если соберемся что-то менять. Не раз это проходил.

                  Нет, RefCell позволяет использовать указатель на мутабельные данные. Что-то похожее на std::unique_ptr в плюсах. Фактически cell часть кончается после borrow_mut.

                  Посмотрю на него повнимательней, похоже я что-то недопонял.

                  Ко всему компилятор очень неплохо оптимизирует иммутабельные данные, как в примере, так что избегание клонирования данных есть преждевременная оптимизация

                  Да, если компилятор знает что данные не меяется, то там целый ворохо оптимизаций. Но на инфраструктуру полагаться полностью не стоит. Она может поменяться, а потом разгребать это очень сложно.

                  Спасибо за конструктивное обсуждение, буду читать и думать.


          1. KanuTaH
            00.00.0000 00:00
            +1

            Я давненько не брал в руки раст, но насколько я помню иметь одновременно иммутабельную и мутабельную ссылки на одно и то же - это не просто "не потокобезопасно", а самое натуральное UB по меркам раста, а это похоже именно то, что у вас происходит при работе с этим field_ptr - есть одновременно иммутабельная ссылка через field на структуру (которую вы потом используете для возврата результата) и мутабельная через field_ptr +mut_var на поле этой структуры.


            1. DancingOnWater Автор
              00.00.0000 00:00

              Тогда тоже самый cell допускает ub. Т.к. этот трюк я взял из его кода.


              1. KanuTaH
                00.00.0000 00:00
                +1

                Cell работает через посредство UnsafeCell, и очень может быть, что компилятор "подкручен" запрещать определенные оптимизации в случае доступа к данным, хранящимся в UnsafeCell, а что позволено Юпитеру, то может быть не позволено быку. В общем, все это выглядит крайне сомнительно ИМХО и может сломаться в любой момент.


                1. DancingOnWater Автор
                  00.00.0000 00:00

                  Если походить по всей стандартной библиотеке, то такое там сплошь и рядом. Юпитер, он конечно Зевс, но не до такой же степени....


                  1. PROgrammer_JARvis
                    00.00.0000 00:00
                    +2

                    Магия есть, но не чёрная. Это конкретный lang item, для которого (и только) компилятор знает об interior mutabilty.

                    Высокоуровневые же примитивы для interior mutabilty строятся на его основе.


          1. 0x1b6e6
            00.00.0000 00:00

            Можно завернуть поле p в std::cell::Cell. Вроде только оно изменяется, но если нет то и другие изменяемые поля завернуть.


            1. DancingOnWater Автор
              00.00.0000 00:00

              Если я правильно понимаю, там оно меняется через копирование.


              1. 0x1b6e6
                00.00.0000 00:00
                +1

                Правильно. Если хочется избежать копирование можно использовать RefCell, но с ним уже нужно быть внимательным: он переносит проверки подсчёта ссылок из компиляции в рантайм и можно словить панику создав 2 ссылки на содержимое если одна из них мутабельная. Например так:

                let val = std::cell::RefCell::new(42);
                let a = val.borrow();.    // ок
                let b = val borrow();.    // ок
                let c = val borrow_mut(); // паника
                


    1. DancingOnWater Автор
      00.00.0000 00:00
      +1

      Еще раз спасибо за clippy это действительно очень полезный инструмент. Даже ошибки выгребает.


      1. Gorthauer87
        00.00.0000 00:00
        +1

        Кстати, а еще очень советую приделать 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


  1. Kelbon
    00.00.0000 00:00
    +2

    Пожалуйста не называйте это кодом на С++, спасибо.


    1. Melirius
      00.00.0000 00:00
      +6

      Синтаксически он. А то, что стиль из учебника C 70-х, так C++ и так позволяет писать до сих пор. :)


    1. DancingOnWater Автор
      00.00.0000 00:00

      Хорошо Си, но хрен редьки не слаще.


      1. a-tk
        00.00.0000 00:00
        +2

        Плохонький код на С++ много с чем угодно можно сравнивать с негативным для первого результатом. Вот только сначала стоило бы навести порядок в плюсовом коде.


        1. DancingOnWater Автор
          00.00.0000 00:00
          +2

          Ну, этот код из liberfa, очень популярной астрономической библиотеки.


          1. Kelbon
            00.00.0000 00:00
            +2

            Если что, С++ никак не связан с С, это максимально разные языки


          1. a-tk
            00.00.0000 00:00

            К слову, в Си есть вот такой зверь - https://en.cppreference.com/w/c/language/restrict

            Нужно для реализации примерно того же, что Вы показываете как достоинство Жравого


            1. flashmozzg
              00.00.0000 00:00
              +1

              Как это здесь поможет? Необходимость в копии это не уберёт. А если её не делать, то просто ошибочную работу поменяем на ошибочную работу + UB. Сомнительное удовольствие.


              1. a-tk
                00.00.0000 00:00

                Вход и выход - разные указатели. Просто отдаёте выходные значения в уникальный буфер и забываете, что они могут наложиться. Если не выдержал условия - ССЗБ UB


                1. flashmozzg
                  00.00.0000 00:00
                  +2

                  Так это же справедливо и для версии без restrict, просто вместо UB будет просто фигня в результате.


        1. flashmozzg
          00.00.0000 00:00
          +3

          Ой, начинается No True Scotsman. Хоть бы переписали его на "настоящий" С++ для начала, чтобы показать в чём автор ошибается (если вам так кажется).


          1. Kelbon
            00.00.0000 00:00
            +1

            1. возвращаешь не void, а матрицу

            2. проблем больше нет


          1. iCpu
            00.00.0000 00:00
            +1

            Для начала, не изобретать велосипед, а взять готовый
            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);

            Вам не для того дали сильно типизированный язык с оптимизирующим компилятором, чтобы вы всё на примитивных типах и ассемблерных вставках делали.


            1. iCpu
              00.00.0000 00:00

              А в 23 стандарте можно даже перегрузить operator[](int i, int j)


            1. DancingOnWater Автор
              00.00.0000 00:00
              +1

              М-да, даешь пример, чтобы подсветить идею, а тут начинается...

              Вопрос не в том, как оптимальнейшим образом организовать перемножение матриц, а в том, что даже в относительно простых вещах можно ошибиться.

              Если же хотите говорить о перемножении матриц, то возьмите eigen; реализация в boost медленнее.

              Ну и матрицы можете все не 3x3, а 512x512, хотя бы.


              1. iCpu
                00.00.0000 00:00

                Вопрос не в том, как оптимальнейшим образом организовать перемножение матриц, а в том, что даже в относительно простых вещах можно ошибиться.

                Можно промахнуться даже мимо кнопки "сделать хорошо". Ошибиться можно и в Rust'е, там тоже полно подводных камней и неявных правил. Вам уже сверху накидали примеров. Константность по умолчанию важна только если ты впервые язык видишь и вообще ничего не читал. У меня сейчас на автомате const& пишется везде, а потом думается в другую сторону.

                М-да, даешь пример, чтобы подсветить идею, а тут начинается...

                Пардонте, нужно было пример лучше выбирать. На каком-нибудь 03 или 07 стандарте я бы согласился, что у плюсов всё очень не просто с выразительностью или с безопасностью. В 20+ к таким мелочам докапываться - это просто оскорбительно.


              1. Kelbon
                00.00.0000 00:00

                вы на основе этого делаете выводы о дизайне языков, где вам что то легче, где лучше производительность и проч. Как можно делать выводы написав какую-то фигню на сишке - непонятно


            1. flashmozzg
              00.00.0000 00:00
              +1

              Ну вот, добавили овердофига бойлерплейта, а сам метод, к которому предъявлялись претензии, так и не реализовали, ну как же так xD

              Кстати, как вы думаете, move конструктор будет как-то осмысленно отличаться от копирования в данном случае?

              В общем, как же должен выглядеть "правильный идиоматичный код на С++", который бы не имел тех же проблем (из описанных в посте), что и текущий пример, так и осталось неясным.


              1. iCpu
                00.00.0000 00:00
                -1

                Ну вот, добавили овердофига бойлерплейта, а сам метод, к которому предъявлялись претензии, так и не реализовали, ну как же так xD

                Месье любит несвежие портянки?

                Кстати, как вы думаете, move конструктор будет как-то осмысленно отличаться от копирования в данном случае?

                Хм... А вот сейчас я задумался. У std::array вообще есть конструктор перемещения? Вроде, нет. Ладно, уел)) А std::vector точно просто обменивает указатели при перемещении.

                В общем, как же должен выглядеть "правильный идиоматичный код на С++", который бы не имел тех же проблем (из описанных в посте), что и текущий пример, так и осталось неясным.

                Конкретный пример некорректен. Почему нельзя матрицу умножать на себя и поместить результат в себя же? Потому что какой-то DancingOnWater решил, что конкретно этот случай он не будет реализовывать? Тогда обложись assert'ами или поменяй интерфейс функции.
                Но если ты хочешь при этом интерфейсе работоспособность оставить, тогда, конечно, нужна обвязка вроде такой.

                Претензию к тому, что язык допускает передачу одной и той же переменной и как константную ссылку, и как не константную, и что разработчик 99% продолбится в глаза мимо этого случая, принимаю.


                1. DancingOnWater Автор
                  00.00.0000 00:00
                  +1

                  Все же хотите тащить код из стекфверфлоу в продакшен, ладно.

                  Представим что у нас матрицы, скажем, элементов по 25 тысяч и их перемножение происходит в цикле. Ваше решение с std:move в коечном итоге постоянно выделяет и убивает на куче эти 25 тысяч, сама по себе это операция не дешевая, так и память по-тихоньку фрагментируется и спустя какое-то время все становится совсем забавно.


                  1. KanuTaH
                    00.00.0000 00:00

                    Решение комментатора выше с std::move - это по сути альтернатива копированию, потому что применяется только в случае, если выходной параметр и один из входных - это одно и то же. Что будет быстрее - обменяться указателями в конструкторе перемещения (или в соответствующем операторе присваивания) и доверить аллокатору все остальное ("все остальное" по сути - это пометить некий участок в памяти как свободный, для этого вовсе необязательно его целиком "пробегать", а фрагментация памяти при выделении кусков одного и того же размера в цикле весьма маловероятна, аллокатор переиспользует только что освобожденный) или все равно выделять временную матрицу, как в вашем примере на C, а потом ее еще и копировать - как думаете?


                    1. DancingOnWater Автор
                      00.00.0000 00:00

                      все остальное" по сути - это пометить некий участок в памяти как свободный, для этого вовсе необязательно его целиком "пробегать", а фрагментация памяти при выделении кусков одного и того же размера в цикле весьма маловероятна, аллокатор переиспользует только что освобожденный

                      В общем случае в цикле будет такая ситуация: аллокация выходного массива ->начало цикла->другие вычисления->аллокация выходного->перемножение матриц->обмен->высвобождение->новый виток.

                      Так вот в ходе других вычислений, как и здесь, тоже может выделяться память, вот именно в этот момент фрагментируется память.


                      1. KanuTaH
                        00.00.0000 00:00

                        Ну вот и вопрос, что будет дешевле - копирование матрицы целиком, как это происходит в вышеприведенном коде на C, или реаллокация. Лично я бы поставил на реаллокацию, потому что более-менее современные аллокаторы довольно хорошо работают с повторяющимися паттернами аллокации (переиспользуют, беря куски разных размеров из разных пулов, и применяют всякие другие эвристики). Это нужно мерять.

                        P.S. Да, при создании временной матрицы для последующего копирования тоже будет аллокация с последующим освобождением при выходе из функции, так что ответ "что дешевле" ИМХО очевиден :)


                      1. DancingOnWater Автор
                        00.00.0000 00:00

                        Да, согласен предложенный вариант iCpu быстрее, но в потенциале приводит к очень неприятной вещи.


                      1. KanuTaH
                        00.00.0000 00:00
                        -1

                        Либо вам нужно иметь возможность перезаписывать одну из входных матриц, либо нет. И если это таки нужно, то в расте вы опять столкнетесь с приседаниями с борроу чекером, потому что он вам так просто перезаписать эту матрицу не даст, ибо если у вас есть мутабельная ссылка, то других быть не может, а C++ может предложить вполне доступный для понимания, логичный, эффективный и инкапсулированный в реализацию выбор без всяких приседаний.


                      1. a-tk
                        00.00.0000 00:00

                        В C++ к слову можно определить оператор *= и получить специализированную реализацию для этого случая.


                      1. KanuTaH
                        00.00.0000 00:00
                        -1

                        Не, ну в расте ты тоже можешь реализовать трейт MulAssign, но... Но. Большое такое но. Ты теряешь возможность обработки ошибок - даже те довольно слабые, что есть в других случаях. Исключений нет, а Option или Result вернуть некуда. Как по мне, это прямо-таки зияющий архитектурный просчёт, но любители раста пожимают плечами и говорят "просто не используйте перегрузку операторов" :)


                      1. a-tk
                        00.00.0000 00:00

                        А можно для тупых: а какие ошибки тут могут быть?


                      1. KanuTaH
                        00.00.0000 00:00

                        В перегруженных операторах-то? Да любые. Вот например пытаешься ты выделить память под временную матрицу, которую потом планируешь переместить в self, а памяти-то и не хватает. В C++ ты выбросишь std::bad_alloc, и сможешь обработать где-то выше, если захочешь, а тут? Как просигнализировать "наверх" о проблеме из выражения типа matrix *= another_matrix?

                        Другой пример. Предположим, что матрица - это некий генерик, который умеет работать с любыми типами - хошь int, хошь float, и вот нужно поработать с неким типом, который, скажем, умеет ловить арифметические переполнения. В C++ - легко, если тип видит переполнение при какой-то там операции с собой, он выбрасывает исключение, оно проходит насквозь через матричныйoperator*=(), и все довольны. Как подобный функционал организовать в расте?


                      1. flashmozzg
                        00.00.0000 00:00
                        +1

                        Паники - те же исключения, так же можно поймать панику на OoM. Если важно отслеживать переполнения, то просто используешь checked_add. Арифметические операторы, бросающие исключения - сомнительная затея.


                      1. KanuTaH
                        00.00.0000 00:00

                        Паники - те же исключения

                        Смешно.

                        Арифметические операторы, бросающие исключения - сомнительная затея.

                        Да, это я уже слышал. Можете не трудиться.


                      1. DarkEld3r
                        00.00.0000 00:00
                        +1

                        Смешно.

                        А можно раскрыть мысль? Да, в раст (и го) сообществе принято говорить, что мол исключений в языке нет, но так-то паника не особо от них отличается: можно перехватить, узнать тип, пробросить дальше. Да это несколько более многословно, чем в "языках с исключениями", использовать панику для обработки ошибок не принято, плюс в библиотеках не стоит полагаться на выбранную стратегию обработки паники, но принципиальных отличий не вижу.


                      1. KanuTaH
                        00.00.0000 00:00

                        но так-то паника не особо от них отличается

                        A panic in Rust is not always implemented via unwinding, but can be implemented by aborting the process as well.


                      1. DarkEld3r
                        00.00.0000 00:00

                        A panic in Rust is not always implemented via unwinding, but can be implemented by aborting the process as well.

                        Так я же об этом сам упомянул. Ну да, стратегия обработки паники задаётся на уровне исполняемого бинаря.


                      1. DarkEld3r
                        00.00.0000 00:00

                        Паники — те же исключения, так же можно поймать панику на OoM.

                        Нельзя. Сейчас ООМ — это аборт, а не паника. Вероятно, в будущем ситуация изменится, но пока так.


                      1. KanuTaH
                        00.00.0000 00:00

                        Более того, паники - это не исключения. У паник нет никаких гарантий по раскрутке стека, это запросто может быть просто вызов abort() из libc.


                      1. DarkEld3r
                        00.00.0000 00:00

                        В такой формулировке звучит как будто поведение чуть ли не случайно. На уровне исполняемого файла (то есть, не библиотеки) гарантии есть: поведение будет такое, какое мы захотим и по умолчанию это как раз раскрутка стека. Библиотекам действительно на это полагаться не стоит, но вообще есть костыль как на уровне библиотеки можно вызывать ошибку компиляции, если стратегия обработки паники не такая, как нам нужно.


                      1. KanuTaH
                        00.00.0000 00:00

                        Библиотекам действительно на это полагаться не стоит

                        О чем и речь - нет никаких гарантий, что на это можно полагаться.

                        есть костыль как на уровне библиотеки можно вызывать ошибку компиляции

                        Костыль есть, да. Но единого поведения нет, более того, одна библиотека может хотеть одну стратегию, а другая - другую. В целом забавно, что в принципе растаманы (не органчики-евангелисты, которые просто повторяют где-то услышанные тезисы, а думающие люди) уже понимают, что что-то тут не так, и городят костыли. Но костыли - это решение только наполовину, так сказать.


                      1. DarkEld3r
                        00.00.0000 00:00

                        О чем и речь — нет никаких гарантий, что на это можно полагаться.

                        Если мы пишем не библиотеку или внутреннюю библиотеку, то есть.


                        Но костыли — это решение только наполовину, так сказать.

                        Я бы наоборот сказал, что раз это до сих пор костыль и нет ни ишью ни рфц, то не сильно оно и востребовано.


                      1. KanuTaH
                        00.00.0000 00:00

                        то не сильно оно и востребовано

                        Ну окей.


                      1. DarkEld3r
                        00.00.0000 00:00

                        Понимаю как это звучит, но так-то завести ишью — ничего не стоит. Продумать и предложить RFC — несколько затратнее, но посильно и одному человеку и да, это может сделать любой. Если никто не собрался, то видимо действительно никому не мешает.


                      1. DarkEld3r
                        00.00.0000 00:00

                        Вдогонку: так-то в С++ тоже можно собрать код с -fno-exceptions и он может оказаться к этому не готов.



                      1. DarkEld3r
                        00.00.0000 00:00

                        Это (пока?) только для no_std окружения. Ну и только с последней версии. Выше об этом писал, кстати.


                  1. iCpu
                    00.00.0000 00:00
                    -1

                    А давайте ещё представим что у нас многопоток? И не простой, а помесь OpenMP и MPI. Не будем расслабляться!
                    А потом давайте представим, что у нас могут быть MatrixView с частично пересекающимися областями, и что сравнения указателей не хватит. А чо нет-то?
                    Что ещё бы представить? Можно ещё разряженные матрицы. Или потоки матричных преобразований с оптимизатором. Или что-нибудь ещё в этом духе. Красиво жить не запретишь!

                    А, может, не будем представлять? Это такая редкая операция, что вы на неё положили болт 50ой резьбы. Вы её просто отбросили как невозможную в расте. Хотя, на деле, ничего невозможного в ней нет: прилетит ссылка через четвёртые руки - и всё равно заломает программу, так или иначе.

                    Хотите специальных оптимизаций - делайте специальные реализации. Пишите кастомный аллокатор, делайте статические переменные. Или добавляйте ссылку на буфер непосредственно в класс.


                    1. DancingOnWater Автор
                      00.00.0000 00:00
                      +1

                      Нет-нет, вы стали критиковать код приведенный для примера (хотя и взятый из реального проекта), как код идущий на продакшен. Поэтому и ваше решение будет рассматриваться так же.

                      И да, в расте это возможно, но если где-то четвертые руки переходят на небезопасное подмножество.


                      1. iCpu
                        00.00.0000 00:00
                        -1

                        Вы указали на безошибочный код с лишним копированием и сказали: "А на Rust мне язык не даёт так сделать."
                        Я показал корректный код, в котором лишнее копирование никогда не происходит. При этом, то же самое можно сделать через простую переменную и копирование, просто тогда всегда будет оверхед по памяти.
                        Но вы не довольны. Хотя код на расте явно дырявый: компилятор не может бесконечно глубоко в дерево уходить, на каком-нибудь пуле объектов всё закончится. И даже если у вас попытку перезаписи поймает менеджер памяти, где он это сделает и куда вывалится? У вас есть там нужная обработка? Нет? Ой-ой-ой...

                        Но если хотите, почему нет? Проведите тесты производительности в оптимистичном (частота помещения в себя 0%), обычном (5%), пессимистичном (30%) и терминальном (100%). В один и несколько потоков. Запишите график использования и фрагментации. Потом повторите, запихнув между умножениями другие операции. Не забываем проверять с разными ключами компилятора: не только с -O, но и c SSE\AVX, с необычными оптимизациями разных компиляторов. Мы же взрослые дяди? Можно ещё и необычные std подключать, реализация от микрософта или gcc далеко не всегда и не во всём топчик. И тестить на всех системах, от древних Windows до новых Android, промежая сборками под микроконтроллеры и одноплатники.

                        Посмотрим на фрагментацию памяти во всех режимах!

                        Это же вы хотите всё делать как в продакшене? Прошу! Доказывайте, как серьёзный дядя, что такая реализация хуже, по сути, никакой в Расте.


                      1. DancingOnWater Автор
                        00.00.0000 00:00
                        +2

                        Если вы считаете, что код на расте, как вы говорите, "явно" дырявый - так покажите это на простом и явном примере. Вот возьмите и напишите его.


                      1. iCpu
                        00.00.0000 00:00

                        Ну, то есть, спора по поводу явной большей семантической корректности Сишного и Плюсового кода нет? Ок.

                        Я на расте не писатель. И не собираюсь. Но я точно знаю, что дерево поиска ошибок у любого компилятора конечно. Нам просто нужно попасть на один уровень глубже.

                        Потому что & и &mut на расте - это прямое переиспользование сишного restrict. И способы обхода этого ключевого слова на Си известны.


                      1. DancingOnWater Автор
                        00.00.0000 00:00
                        +1

                        Ну, то есть, спора по поводу явной большей семантической корректности Сишного и Плюсового кода нет?

                        А я разве спорил с этим? Указанная мной проблема характерна для обоих языков.

                        Ставя эквивалент между restrict и ссылками раста вы делаете ошибку.


                      1. iCpu
                        00.00.0000 00:00

                        Ладно, согласен, неправильно выразился. llvm использует код для сишного restrict при оптимизации использования ссылок. Из-за этого в определённый момент такая оптимизация не работала.


                  1. a-tk
                    00.00.0000 00:00

                    А можно поинтересоваться, где в астродинамической библиотеке используются такие матрицы?


                    1. DancingOnWater Автор
                      00.00.0000 00:00

                      В астродинамике подобные матрицы возникают, например, при уточнении орбиты.


                      1. a-tk
                        00.00.0000 00:00

                        Имеется в виду уточнение орбит для большого количества измерений? Или траекторные измерения?

                        Что-то я не соображу, где там именно умножение матриц. Можно ссылку на какие-то материалы с описанием этого дела? Можете спокойно переходить на слэнг, я в теме (поэтому и пришёл в статью).


                      1. DancingOnWater Автор
                        00.00.0000 00:00

                        Уточнение орбит при большом количестве измерений, но я говорил про вообще существовании матриц подобного размера.

                        А так да, перемножение такого размера матриц в астродинамике на моей практике не встречалось.


  1. Stas911
    00.00.0000 00:00
    +1

    Как я понял, вы собираетесь писать Open Source проект, а значит от того, сможете ли вы привлечь единомышленников и помощников, зависит будущее проекта. С этой точки зрения я бы С++ вообще не рассматривал.


    1. KanuTaH
      00.00.0000 00:00
      +2

      Поцчему?


      1. Stas911
        00.00.0000 00:00
        +2

        Не особо мне верится, что есть большое количество желающих им заниматься в 2023м, но я могу ошибаться. Хотя вот по статистике гитхаба типа на 4м месте. Ну не знаю...


        1. KanuTaH
          00.00.0000 00:00

          Ну конечно питончик или яваскрипт его обходят, что в общем-то неудивительно, но он несравненно популярнее раста :) Понятно, что условные веб-разработчики (процент которых среди "разработчиков вообще" довольно велик) этим не заинтересуются, но для них ли этот проект вообще, и будут ли они в нем полезны?... Вопросы, вопросы...


          1. danSamara
            00.00.0000 00:00
            +2

            Раст на хайпе, и сейчас делать на нём небольшой open source выгоднее, в плане привлечения разработчиков. Чем пользуются крестовые проекты, теряющие активных разрабов - переходят на раст.


            1. KanuTaH
              00.00.0000 00:00
              +3

              Что-то я не припомню каких-либо более-менее известных плюсовых проектов, "перешедших на раст". "Перейти на раст" можно разве что тогда, когда ещё практически ничего нет, ну или проект очень простой, который можно взять и переписать по сути с нуля в одно рыло, иначе заниматься этим смысла вообще нет. От людей со стороны, которые раст "изучили" на хайпе вчера (а скорее всего - только изучают), а проект увидели сегодня, как баран - новые ворота, при таком "переходе" вряд ли будет большой толк: наступят на все старые уже пройденные грабли, и добавят к ним новые. Это все иллюзии.

              P.S. Вот этот комментарий к прошлой статье был очень правильный. И сейчас автор вместо построения и реализации мат модели с переменным успехом сражается с борроу чекером. Все-таки "изучить язык" и "сделать продукт" - это очень разные задачи.


              1. DancingOnWater Автор
                00.00.0000 00:00
                +2

                Да, я потратил время на освоение языка, но не считаю, что это было сделано зря. Я уже бывал в ситуации, что неверный выбор языка и фреймворка в начале, приводил к слишком большим трудностям потом.

                Плюсы не без недостатков, как по мне, раст позволит двигаться быстрее, тратя меньше времени на отладку. А еще будет больший шанс, что проект будет использоваться в бортовом коде.


                1. KanuTaH
                  00.00.0000 00:00

                  А еще будет больший шанс, что проект будет использоваться в бортовом коде.

                  Тогда уж стоит на MISRA затачиваться, а не на раст. Раст вряд ли в обозримом будущем будет для чего-то такого где-либо сертифицирован, уж больно он... in flux.


                  1. DancingOnWater Автор
                    00.00.0000 00:00
                    +2

                    По крайне мере у нас на предприятии требовании к наличию сертификата у языка нету, а заказчики аттестуют по своим правилам и там это тоже подобные вопросы не стоят.


                  1. flashmozzg
                    00.00.0000 00:00
                    +2

                    Ferrocene уже почти тут, в линукс он уже пролез (в отличие от), NSA уже советует переходить с С/С++ на memory-safe языки, где это возможно (и в нишах, где GC неприемлем, по сути альтернатив расту сейчас нет), так что процесс идёт.


              1. danSamara
                00.00.0000 00:00
                +1

                Не веду список проектов, переползающих на раст, однако они попадаются в ленте. Из последнего, что помню: Fish собираются портировать на раст. Интересно будет осенью посмотреть на их прогресс.

                ...при таком "переходе" вряд ли будет большой толк: наступят на все старые уже пройденные грабли, и добавят к ним новые. Это все иллюзии.

                Это из серии "некогда точить топор, надо рубить". Конечно, при изучении нового инструмента, немалое количество времени уйдёт на базу.


                1. KanuTaH
                  00.00.0000 00:00

                  Ну с большой помпой рекламировавшаяся в свое время попытка портировать emacs на раст загнулась. Что случилось с рыночной долей файрфокса пока они "точили топор" думаю все помнят - падение до единиц процентов на десктопе и до долей процента на мобилке. Они точили топор, пока основной конкурент ускорял работу и уменьшал энергопотребление. Про этот fish я честно говоря впервые слышу. Руководствоваться только пионерскими лозунгами про модность и современность, и громкие заявления типа "никто не любит xxx", которыми являются по сути 3 из 4 аргументов в пользу riir по вашей ссылке (только один аргумент из четырёх технический, про облегчение concurrency) - это не инженерный подход как по мне. Впрочем как хотят, у каждого свои... развлечения.


    1. flashmozzg
      00.00.0000 00:00

      Смотря какой проект. Вон люди пишут эмуляторы и проблем с привлечением людей нет. С другой стороны, если хочется какой-нибудь сервер или веб-фреймворк забацать, но на чём-то быстром, то тут уже кресты не самым лучшим выбором с такой точки зрения будет, да.


  1. KanuTaH
    00.00.0000 00:00

    .


  1. KanuTaH
    00.00.0000 00:00
    +1

    Но вот чтоб при равных гарантиях безопасности достичь равность скорости исполнения, то для 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) устраивает не всех, далеко не всех.


    1. DancingOnWater Автор
      00.00.0000 00:00

      Если вы намекаете на исключения, то у них есть одна неприятная особенность: повсеместное их использование приводит к тому, что вся обработка сводится к тому, чтобы перехватить исключение, дабы она не завалил программу.

      Не знаю зачем вам такая структура, но первое что напрашивается - это создать trait, а потом его имплимитировать для SomeFile.

      Опять же не настаиваю, но я уже не раз замечаю, что привычны и наработанные шаблоны создают плохой код на Раст и надо их перестраивать.


      1. KanuTaH
        00.00.0000 00:00
        +1

        вся обработка сводится к тому, чтобы перехватить исключение, дабы она не завалил программу

        Ошибка ввода-вывода - не повод валить программу. Это может быть и должно быть корректно обработано.

        Опять же не настаиваю, но я уже не раз замечаю, что привычны и наработанные шаблоны создают плохой код на Раст и надо их перестраивать.

        Покажите пример "хорошего идиоматичного кода на Раст", чтобы в аналогичной ситуации объект создавался сразу в области памяти вектора, ну и ошибки пробрасывались вверх и обрабатывались. Words are cheap. Или это тоже "не нужно"?


        1. DancingOnWater Автор
          00.00.0000 00:00

          Покажите пример "хорошего идиоматичного кода на Раст", чтобы в аналогичной ситуации объект создавался сразу в области памяти вектора, ну и ошибки пробрасывались вверх и обрабатывались. Words are cheap. Или это тоже "не нужно"?

          Я же сказал, что не знаю, какую задачу вы решаете, но я бы пробовал вариант:

          let mut v: Vec<SomeFile>;
          ......
          while ...
          {
            match f.read() {
              Err(why) => [...],
              Ok(buf) => v.push(buf),
            }
          }
          


          1. KanuTaH
            00.00.0000 00:00
            +1

            И что здесь, buf читается сразу в область памяти, выделенную в векторе? Нет, этого тут не происходит. Это вообще ерунда какая-то.


          1. DancingOnWater Автор
            00.00.0000 00:00

            Повторюсь, я даже приблизительно не знаю логику вашей задачи, но можно пробовать такие варианты:

            let mut v: Vec<SomeBuf>;
            ......
            for buf in v.iter_mut
            {
              if Err(why) = f.read(buf){
                  .....
              }
            }


            1. KanuTaH
              00.00.0000 00:00

              Здесь не поддерживается целостность инварианта. Хочу обратить ваше внимание на то, что в оригинальном коде на C++ SomeBuf - это всего лишь часть другого объекта, и этот объект либо создаётся целиком в случае успешного чтения в буфер, либо не создаётся вовсе. Здесь это не так: во-первых, внешнего объекта нет вовсе, а во-вторых у SomeBuf есть промежуточное состояние "создан, но не инициализирован". Процесс заполнения вектора неинициализированными экземплярами SomeBuf также опущен.


              1. DancingOnWater Автор
                00.00.0000 00:00

                В третий раз говорю, что я не знаю вашей задачи. Немногим выше вы использовали emplace_back, т.е. добавляли в конец контейнера новый элемент. Потом выяснилось, что нужно использовать область памяти уже выделенную вектором.

                Теперь новые вводные, которые уже вовсю опираются на ту логику, что скрыта от меня. Здесь я не могу помочь.

                Но еще раз хочу написать: раст заставляет задуматься о шаблонах в своей голове.


                1. KanuTaH
                  00.00.0000 00:00

                  Немногим выше вы использовали emplace_back, т.е. добавляли в конец контейнера новый элемент. Потом выяснилось, что нужно использовать область памяти уже выделенную вектором.

                  Вы не знаете, как работает emplace_back, верно? Так прочтите. Он именно что использует область памяти, уже выделенную вектором.

                  Но еще раз хочу написать: раст заставляет задуматься о шаблонах в своей голове.

                  Чтобы сравнивать шаблоны в разных языках, нужно эти шаблоны знать. Иначе сравнивать будет нечего, вы будете не в состоянии оценить ни плюсы, ни минусы, и не сможете сделать осознанный выбор (не основанный просто на евангелист-пропаганде).


                  1. DancingOnWater Автор
                    00.00.0000 00:00

                    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)...

                    Взял отсюда. Т.е. это вот совсем не гарантированное поведение.


                    1. KanuTaH
                      00.00.0000 00:00

                      Т.е. это вот совсем не гарантированное поведение.

                      Если аллокатор предоставляет метод construct(), то будет вызван он (с теми параметрами, что были переданы в emplace_back), и аллокатор сам решит, что ему делать. Если метод construct() у аллокатора отсутствует, то будет использован placement new напрямую. Стандартный аллокатор создает объект сразу на месте, но вы можете использовать нестандартные аллокаторы, при желании. Все в ваших руках.


                      1. DancingOnWater Автор
                        00.00.0000 00:00
                        -1

                        Оки доки. Но вторая схема принципиально не меняется. Обработка ошибок может быть именно такой, без match.


                      1. KanuTaH
                        00.00.0000 00:00

                        Во второй схеме вы сначала должны будете заполнить вектор экземплярами класса, правильно? Покажите, пожалуйста, как вы это делаете без их предварительного создания на стеке а затем копирования в вектор, очень интересно будет взглянуть. На всякий случай напоминаю, что vec![T; n] использует std::vec::from_elem, для работы которого T должен иметь трейт Clone, то есть вы накопируете пустых (точнее, незаполненных/неинициализированных) буферов из стека в вектор (кстати, интересно, откуда вы заранее знаете, сколько их там надо накопировать, ну да ладно), а потом будете их заполнять, вместо заполнения буфера на стеке, а потом опять же копирования его в вектор. Те же яйца, только в профиль, как полагаете?


                      1. DancingOnWater Автор
                        00.00.0000 00:00

                        Моя больная голова наконец сообразила, чего вы хотите.

                        В безопасном, сиречь идиоматическом, Раст это сделать нельзя, т.к. при возникновении ошибки у вас окажется мусор в памяти. Но можно сделать все через небезопасное подмножество.


                      1. KanuTaH
                        00.00.0000 00:00
                        +3

                        Это уже не будет "идиоматический раст", ну и цена ошибки будет высока, потому что небезопасный раст, так сказать, "ещё небезопаснее", чем C или C++, ибо требования, предъявляемые к unsafe коду, выше, и не всегда очевидны, как уже тут обсуждалось - можно даже взять код из растового std, а потом выяснить, что он работает лишь потому, что компилятор подпилен конкретно под это место в std и ни под какое другое :)


                      1. DancingOnWater Автор
                        00.00.0000 00:00
                        -1

                        Все может быть, но и приведенный вами код на плюсах отнюдь не безопасен. Если исключение обрабатывается внутри s, то вы получите, мусор в памяти, а у внешнего кода возникают трудности узнать об ошибке. Если же конструктор прокидывает исключение, то можете получить невалидный контейнер, а можете не получить. В зависимости от реализации std.

                        Кстати, хорошая иллюстрация почему в соглашении Qt прописано, чтобы конструктор не кидал исключений.


                      1. KanuTaH
                        00.00.0000 00:00

                        Если исключение обрабатывается внутри s

                        Тогда поддержание собственного инварианта - его задача. Но в той форме, что я написал в оригинальном комментарии, это не так.

                        Если же конструктор прокидывает исключение, то можете получить невалидный контейнер, а можете не получить. В зависимости от реализации std.

                        Нет. Сам по себе emplace_back гарантирует, что если именно конструируемый элемент выбросит исключение, то последствий не будет. Если std не реализует такое поведение, то это std не соответствует стандарту :) Но как водится в векторах добавление элемента может вызвать реаллокацию, и вот при реаллокации, если конструктор перемещения у T не noexcept и выбросит исключение при перемещении какого-то уже добавленного в вектор элемента из старой области памяти вектора в новый, то вектор действительно не будет знать, что делать. Тут гарантий никаких. Поэтому их и рекомендуют делать noexcept и не особо замороченными - свап-свап-свап.

                        Но даже в этом случае вектор будет стараться поддерживать инвариант: если он увидит, что конструктор перемещения не noexcept, то он будет использовать конструктор копирования, если он есть, и откатит операцию, если что-то пошло не так (просто уничтожит свой новый буфер с уже скопированными элементами). Если же его нет, то да, будет использовать не noexcept перемещение без гарантий.

                        Можете поиграться. Допишите к конструктору перемещения noexcept и наблюдайте результат.

                        Кстати, хорошая иллюстрация почему в соглашении Qt прописано, чтобы конструктор не кидал исключений.

                        Qt - это вообще "C с классами". Им можно. Они никакие exception guarantee не считают нужным реализовывать ни в контейнерах, нигде.


                      1. Kelbon
                        00.00.0000 00:00

                        Всё там безопасно, если в любом методе вектора кидается исключение, то вектор остается таким же каким был до вызова метода. Это базовая гарантия всех классов в стандартной библиотеке С++


                      1. a-tk
                        00.00.0000 00:00

                        Строго говоря наблюдаемое состояние вектора остаётся тем же.

                        На мусор в памяти C++ вообще кладёт большой болт и затягивает его гайкой, но это в его природе. Тут встаёт вопрос только когда надо не допустить утечки в "мусорную" память чувствительных данных.


                      1. KanuTaH
                        00.00.0000 00:00

                        На мусор в памяти болт кладут все. Те же аллокаторы (в том числе и в расте) не имеют привычки забивать освобождённые области памяти нулями или ещё чем (за исключением специальных отладочных режимов), потому что в общем случае это только гробит производительность без какого-либо профита. Когда стек двигается туда-сюда, там тоже нигде ничего специально не затирается.


                      1. a-tk
                        00.00.0000 00:00
                        +1

                        Не все. .NET на эту тему упарывается, и чтобы это запретить надо попрыгать с бубном.


                      1. KanuTaH
                        00.00.0000 00:00
                        +1

                        Такое впечатление, что мы про разные вещи говорим.

                        Ситуация раз: что-то больше не нужно и должно быть освобождено (как в случае с не сумевшим себя создать элементом вектора). Ситуация два: мы получили кусок памяти под переменную и должны будем с ним сейчас работать.

                        То, что по вашей ссылке - это про ситуацию два, чтобы инициализация локальных переменных не выполнялись и в них остался мусор после ситуации раз. На ситуацию раз это никак не влияет.


  1. KanuTaH
    00.00.0000 00:00

    .