Как не копировать код в Rust

  • Tutorial

Первое правило хорошего тона в программировании (или одно из первых) гласит: "Не копируй код". Используй функции и наследование.

Если с функциями все понятно, то с наследованием посложнее. Вы, наверное, знаете, что в Rust нет прямого наследования, но есть способы добиться чего-то на него похожего. О них я и расскажу.

impl dyn Trait

Если вы посмотрите на реализацию итератора, то увидите, что у него есть один required метод (то есть тот, который нужно реализовать нам) и provided методы (те, которые реализованы за нас). Что нам это даёт? А то, что если мы напишем свою реализацию какой-либо коллекции, то чтобы реализовать для нее итератор нам нужно всего-навсего имплементировать next(). Все остальные фп-шные штуки вроде map(), filter(), fold() и т.д. будут реализованы автоматически.

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

enum Damage{
    Physical(f32),
    Magic(f32)
}

trait Character{
  fn get_magic_resistance(&self) -> f32;
  fn get_physical_resistance(&self) -> f32;
  fn set_hp(&mut self, new_value: f32);
  fn get_hp(&self) -> f32;
  fn get_type(&self) -> CharacterType;
  fn get_dmg(&self) -> Damage;
}

impl dyn Character {
  fn make_hurt(&mut self, dmg: Damage) {
    match dmg{
      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),
      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())
    }
  }
}

#[derive(Debug, Clone, Copy)]
enum CharacterType {
    Mage,
    Warrior,
    Rogue
}

impl Default for CharacterType {
    fn default() -> Self{
        CharacterType::Warrior
    }
}

#[derive(Default)]
struct Player{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Player {
    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {
        Self{ ty, hp, dmg, .. Default::default() }
    }
}

impl Character for Player{
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    #[inline]
    fn get_type(&self) -> CharacterType {
        self.ty
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }
}

struct EnemyWarrior{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Default for EnemyWarrior {
    fn default() -> Self {
        Self{
            ty: CharacterType::Warrior,
            phys_resist: 0.,
            mag_resist: 0.,
            hp: 10.,
            dmg: 1.
        }
    }
}

impl Character for EnemyWarrior{
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    fn get_type(&self) -> CharacterType {
        CharacterType::Warrior
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }
}

fn main(){
    let mut player = Player::new(CharacterType::Warrior, 10., 1.);
    let mut enemy = EnemyWarrior::default();
    
    <dyn Character>::make_hurt(&mut enemy, player.get_dmg());
    println!("{}", enemy.get_hp());
}

TL;DR сделали 2 структуры и реализовали для них трейт Character, в котором 6 required методов и 1 provided метод.

Но внимательный читатель совершенно справедливо спросит: "Так мы ведь всё равно скопировали код?" Верно, из-за того, что в трейтах нельзя объявлять поля, нам пришлось городить required методы для получения нужных данных. Сейчас-то, конечно, их всего 6, но потом у нас могут добавиться передвижения, анимации, левел-апы и проч. Конечно, целый один метод у нас единый на все имплементации, но код-то мы все равно копируем?

Deref<Target=_>

Тут нам на помощь приходит трейт Deref. Честно говоря, не знаю, насколько это хорошая практика использовать его с такой целью, но, например, в image есть такое: ImageBuffer реализует Deref для [P::Subpixel] и, соответственно, имеет все методы массива, которые есть в стандартной библиотеке. Давайте перепишем наш пример под Deref и посмотрим, сколько места нам удалось сэкономить.

use std::ops::Deref;
use std::ops::DerefMut;

enum Damage{
    Physical(f32),
    Magic(f32)
}

#[derive(Default)]
struct Character{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Character {
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    #[inline]
    fn get_type(&self) -> CharacterType {
        self.ty
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }

  fn make_hurt(&mut self, dmg: Damage) {
    match dmg{
      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),
      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())
    }
  }
}


#[derive(Debug, Clone, Copy)]
enum CharacterType {
    Mage,
    Warrior,
    Rogue
}

impl Default for CharacterType {
    fn default() -> Self{
        CharacterType::Warrior
    }
}

#[derive(Default)]
struct Player(Character);

impl Player {
    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {
        Self(Character{ ty, hp, dmg, .. Default::default() })
    }
}

impl Deref for Player {
    type Target = Character;
    
    #[inline]
    fn deref(&self) -> &Self::Target{
        &self.0
    }
}

impl DerefMut for Player {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target{
        &mut self.0
    }
}


struct EnemyWarrior(Character);

impl Default for EnemyWarrior {
    fn default() -> Self {
        Self(Character {
            ty: CharacterType::Warrior,
            phys_resist: 0.,
            mag_resist: 0.,
            hp: 10.,
            dmg: 1.
        })
    }
}

impl Deref for EnemyWarrior {
    type Target = Character;
    
    #[inline]
    fn deref(&self) -> &Self::Target{
        &self.0
    }
}

impl DerefMut for EnemyWarrior {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target{
        &mut self.0
    }
}

fn main(){
    let mut player = Player::new(CharacterType::Warrior, 10., 1.);
    let mut enemy = EnemyWarrior::default();
    
    enemy.make_hurt(player.get_dmg());
    println!("{}", enemy.get_hp());
}

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

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

struct Child (Parent, ChildInner);

impl Child {
  pub fn get_some_field_from_inner(&self) -> &ChildInner::Field {
    &self.1.field
  }
}

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

Заключение

В заключение хочу сказать, что в расте не помешало бы явное наследование структур. Всё-таки, как ни крути, в других языках оно позволяет писать с одной стороны хорошо читаемый, с другой стороны лаконичный код (если, конечно, это не множественное наследование). Подходы в Rust, конечно, позволяют экономить какое-то количество места, но хотелось бы чего-то более явного. Да и интерфейсы Deref, DerefMut вообще не предназначены для того, для чего мы их использовали в данной статье. Они, как следует из названия, нужны чтобы разыменовывать умные указатели. А если вы объединяете несколько структур данных, то вам придется использовать синтаксис self.1, который далеко не очевидный. В общем, как и всегда, есть и плюсы и минусы.

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

Only registered users can participate in poll. Log in, please.

Какой вариант лучше?

  • 80.0%impl dyn Trait28
  • 11.4%Deref<Traget=_>4
  • 8.6%Копировать код3

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 46

    –3
    Здравствуйте. В статье поднят своевременный и актуальный вопрос. К сожалению, в статье не рассмотрен смежный вопрос — как не использовать полуторамегабайтные картинки?
      +8
      Честно говоря, не знаю, насколько это хорошая практика использовать его с такой целью

      Считается антипаттерном: https://github.com/rust-unofficial/patterns/blob/master/anti_patterns/deref.md


      Официальная дока по Deref тоже гласит, что "… Because of this, Deref should only be implemented for smart pointers to avoid confusion."


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

      О да. Легендарная https://github.com/rust-lang/rfcs/issues/349 "Efficient code reuse", об которую за годы порядочно RFC и pre-RFC зубы обломало. Авось когда-нибудь кто-то запаровозит таки.


      Из доступных в данный момент альтернатив еще можно посмотреть на всякие библиотеки, дающие процмакросы для автоматизации делегации, типа https://crates.io/crates/ambassador — но с ними, понятное дело, часть инструментария начинает скрипеть.

        0

        Из ссылки очень внезапной новостью стало обнаружение в способах реализации наследования сишной вложенности с #[repr©], за который меня в свое время только ленивый не пнул.

        +6

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

          0
          Запоздало, но добавил. Тут дело случая, если в одном файле две сущности делают одно и тоже одним и тем же кодом (или минимально изменённым, например класс1 двигается вправо каждый кадр, а класс2 влево), то тут точно копирование — плохо.
          +9

          Вопрос 1


          А как такая же проблема решается в Haskell, откуда подобная система во многом позаимствована? В некотором приближении, там существует те же "структуры" и тайпклассы (трейты).


          Вопрос 2


          А действительно нужно наследование структур? Вообще, наследование различных игровых сущностей — во многом хрестоматийный пример к "composition over inhertiance", а в геймдев архитектуре получил широкое распространение паттерн Entity Component System.


          Почему бы не сделать с композицией, примерно так:


          struct CharacterProps{
              phys_resist: f32,
              mag_resist: f32,
              hp: f32,
              dmg: f32
          }
          
          trait Character {
              fn props<'a>(&self) -> &'a CharacterProps;
              fn props<'a>(&mut self) ->&'a mut CharacterProps;
              fn make_hurt(&mut self, dmg: Damage) { /* ... */ }
          }
          
          struct Player {
              props: CharacterProps, 
              // other player-related fields
          }
          impl Character for Player {
              fn props<'a>(&self) -> &'a CharacterProps { &self.props }
              fn props<'a>(&mut self) ->&'a mut CharacterProps {&mut self.props }
          }
          
          struct EnemyWarrior {
              props: CharacterProps, 
              // other enemy-related fields
          }
          impl Character for EnemyWarrior {
              fn props<'a>(&self) -> &'a CharacterProps { &self.props }
              fn props<'a>(&mut self) ->&'a mut CharacterProps {&mut self.props }
          }

          Да, осталась копипаста для получения props, но ее куда меньше и без хаков.


          Примечания:


          • я мог ошибиться с лайфтаймами, да и с синтаксисом
          • я не уверен, как лучше, имплементировать Character для Player и EnemyWarrior или вообще можно будет просто имплементировать расчет урона только для CharacterProps и тогда вообще не нужен этот геттер. Но использовать такую структуру будет менее удобно. Наверное.

          Если же говорить об имплементации ECS, то там, скорее, будут не жестко заданные поля соответствующих типов, а вектор различных компонентов (положение, рендер, персонаж итп). Не уверен, как это имплементировать в Rust. Возможно, Vec<Box<dyn Component>>

            +3

            Если что, готовых ECS под раст целая куча есть и постоянно новые появляются



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

              +1

              В данном случае лайфтаймы проставлять не нужно.

              0
              А как такая же проблема решается в Haskell, откуда подобная система во многом позаимствована? В некотором приближении, там существует те же "структуры" и тайпклассы (трейты).

              У тайпклассов есть minimal complete definition. Например:


              λ> :info Ord
              type Ord :: * -> Constraint
              class Eq a => Ord a where
                compare :: a -> a -> Ordering
                (<) :: a -> a -> Bool
                (<=) :: a -> a -> Bool
                (>) :: a -> a -> Bool
                (>=) :: a -> a -> Bool
                max :: a -> a -> a
                min :: a -> a -> a
                {-# MINIMAL compare | (<=) #-}

              что определяется как


              class  (Eq a) => Ord a  where
                  compare              :: a -> a -> Ordering
                  (<), (<=), (>), (>=) :: a -> a -> Bool
                  max, min             :: a -> a -> a
              
                  compare x y = if x == y then EQ
                                else if x <= y then LT
                                else GT
              
                  x <  y = case compare x y of { LT -> True;  _ -> False }
                  x <= y = case compare x y of { GT -> False; _ -> True }
                  x >  y = case compare x y of { GT -> True;  _ -> False }
                  x >= y = case compare x y of { LT -> False; _ -> True }
              
                  max x y = if x <= y then y else x
                  min x y = if x <= y then x else y
                  {-# MINIMAL compare | (<=) #-}

              На чуть более высоком уровне есть такая тема, как Generics, и возможность описать правила вывода тайпклассов через Generic-представление типов данных. Например, библиотека aeson для работы с жсоном умеет эти инстансы выводить, и достаточно написать:


              import qualified Data.Aeson as A
              import GHC.Generics
              
              data Person = Person
                { name :: String
                , age :: Int
                , friends :: [Person]
                } deriving (Generic, A.FromJSON, A.ToJSON)

              чтобы определить тип Person вместе с конвертаций в/из json-объекты с соответствующими полями (внимательный читатель обратит внимание на забавную рекурсию в friends).


              Или, например, я не так давно наваял набор вспомогательных типов для упрощения описания форматом бинарной сериализации.

              0

              Вопрос не по теме статьи: расставлять #[inline] в таких случаях принято? Это правда даёт профит?

                +3

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


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

                  +1

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


                  Беглый поиск выдал два противоречивых совета. Вот тут тоже говорят, что мелкие функции стоит помечать как #[inline] так как без LTO компилятор не справляется. С другой стороны, нашёл упоминание, что инлайн замедляет компиляцию и лучше как раз использовать LTO, если нужно выжать максимум. Правда оба обсуждения довольно старые.


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

                    +1
                    в подавляющем большинстве случаев компилятор достаточно умный (и сильно умнее среднего программиста) касательно того, инлайнить код или нет. Проставлять inline вручную может иметь смысл в случае динамической линковки или выключенного LTO. Насколько я понимаю, и то и другое для раста не типично.
                      0

                      Мне тоже так кажется, но периодически попадаются натыканные #[inline], вот и возникают сомнения.

                        –1
                        По мне лучше перебдеть, чем недобдеть. Так что в явном указании
                        #[inline]
                        я лично ничего плохого не вижу. Это не указание
                        register
                        в языках C/C++, там действительно компилятору лучше знать какие переменные класть на стек, а какие в регистр, а вот инлайнить или нет функцию… IMHO от такого указания во всяком случае хуже уж точно не будет.
                          +1
                          вы уж определитесь, на компилятор надеяться или разобраться наверняка как он работает. Потому что гарантировать что «хуже точно не будет» можно только исходя из предположения, что #[inline] не делает вообще ничего.
                            0
                            Функции из одной-двух строк вполне можно (и даже нужно) заинлайнить. А функции на полэкрана инлайнить не надо. Вот так и надо не на компилятор надеяться, а самому не плошать.
                              +1
                              Во-первых, вы сильно упрощаете эвристики инлайнинга. Во-вторых, забываете, что при чрезмерном инлайнинге код разрастается, и накладные расходы на префетчинг могут начать перевешивать расходы на call'ы, сэкономленные инлайнингом. Иными словами, от инлайнинга конкретной функции в конкретном месте может стать хуже.
                                0
                                Так думать надо что инлайнить, а что нет, и где инлайнить, а где нет. Безусловно «средний программист» которого вы упомянули выше не особо этим будет заморачиваться, да и не сможет при всём желании, но речь идёт про программистов выше среднего. Которые реально лучше компилятора знают где инлайнить и что инлайнить.
                                  +3
                                  > Которые реально лучше компилятора знают

                                  Я ведь надеюсь под «знает» понимается «написал кучу бенчмарков и замерил профит с разных путей выполнения»?
                                    –2
                                    В тривиальных случаях наподобие как раз
                                    #[inline]
                                    fn get_hp(&self) -> f32 {
                                        self.hp
                                    }
                                    «куча бенчмарков» вовсе не нужна, достаточно собственной головы. А «куча бенчмарков» в подобном это сравнимо с «обжегшись горячим молоком теперь на воду всегда дуть».
                                      +3
                                      Вот удивляюсь всегда таким людям как вы. Замеры времени — это одна из немногих метрик в программировании, которая поддается боле-менее безболезненному измерению. Но нет, все равно люди предпочитают пользоваться какими-то советами, возникшими хрен знает откуда, хотя казалось бы, чего проще, взять да померять
                                        –1
                                        Вот удивляюсь всегда таким людям которые потратят час времени на написание бенчмарков вместо того чтобы за полминуты глянуть на код, который сгенерировал компилятор, и собственными глазами увидеть есть там bottle neck или нет и лишь в случае сомнений уже сделать бенчмарк. У таких людей видать просто очень дофига свободного времени, которое они запросто могут потратить на подобные вещи.
                                          +1
                                          Тоесть вы глядите на вывод компилятора вместо бенчмарков?

                                          > увидеть есть там bottle neck или нет и лишь в случае сомнений уже сделать бенчмарк

                                          Почему тогда не наоборот, увидеть bottle neck и соптимизировать? Зачем эта предварительная оптимизация?
                                            –1
                                            Это не предварительная оптимизация ни в коей мере. В коде типа
                                            #[inline]
                                            fn get_hp(&self) -> f32 {
                                                self.hp
                                            }
                                            совсем не матрицы перемножаются, и не выборка из массива делается по, к примеру, индексам, кратным исключительно трём, и не полином CRC высчитывается. Надо быть полным идиотом чтобы подобное не заинлайнить и, извините, надо быть полным идиотом чтобы для подобного кода делать бенчмарки или, о боже, оборачивать подобный код тестами.
                                              +2
                                              Понимаете, мне не за себя. Мне за державу обидно. Обидно видеть, как программисты вокруг считают, что достаточно добавить какое-то слово, заменить одну функцию на другую и все станет волшебным образом быстрее. Хотя вроде бы это так легко — просто взять и проверить. Измерение производительности — это одна из немногих (а может и вообще единственная) область программирования, где правильного результата можно добиться обычным измерением. Да, не всегда это просто, и тут тоже есть подводные камни разной степени сложности, но почему бы не прививать культуру «хочешь ускорить — замерь»? Оптимизация — такой же важный этап работы программиста, как и написание кода, зачем его всеми силами избегать?
                                                +1
                                                Обидно видеть, как программисты вокруг считают, что достаточно добавить какое-то слово, заменить одну функцию на другую и все станет волшебным образом быстрее.

                                                Для функции, тело которой сводится к return self.field или return this->field, инлайнинг в подавляющем большинстве случаев полезен (и я затрудняюсь придумать даже очень искусственный пример, когда это было бы не так). Для таких функций inline можно ставить вообще, всегда, по умолчанию, не думая.


                                                Измерение производительности — это одна из немногих (а может и вообще единственная) область программирования, где правильного результата можно добиться обычным измерением.

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


                                                Пример, который мне запомнился — несколько разных способов сделать circular buffer поверх массива (можно взять массив длиной степени два и брать битовую маску счётчика, можно ещё несколько вариантов). Ожидаемый эффект — порядка нескольких циклов на операцию. Как измерить результат оптимизаций? Прогнать все данные не вариант — в проде будут другие паттерны получения данных, другое состояние кэшей, вот это всё. Да и даже если вариант — как измерить скорость конкретно этого места? rtdsc имеет сходную задержку и её вариабельность, да и версия без барьеров измеряет непонятно что, версия с барьерами слишком пессимизирует код.


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


                                                дисклеймер

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

                                                  –1
                                                  Слава богу хоть кто-то думает так же, как и я. А я уж боялся что программистов больше нет, остались только «измерители времени выполнения».
                                                    +2
                                                    > инлайнинг в подавляющем большинстве случаев полезен (и я затрудняюсь придумать даже очень искусственный пример, когда это было бы не так). Для таких функций inline можно ставить вообще, всегда, по умолчанию, не думая.

                                                    Да, безусловно, какие-то утверждения будут правильными, например что инлайнинг полезен всегда. Это нормально. Если бы все утверждения какого-то конкретного оппонента были неправильными, то это было бы слишком просто: можно было бы делать все в точности до наоборот того, что говорит оппонент. И возможно, что в случае с inline вы правы, но это не отменяет общего правила: нужно мерять.

                                                    Даже по данному примеру, один из комментаторов спросил, почему тогда уж не использовать inline(always) вместо inline? Раз уж такая уверенность? Ведь даже в офф документации сказано, что inline не обязан инлайнить (да и inline(always) вроде бы тоже). Вот тут мы и видим пример веры в «божьше слово», что вставка какого-то магического слова внезапно ускорит нашу программу. От этого-то я и хочу предостеречь

                                                    > В некоторых случаях (особенно если мы обсуждаем подобные микрооптимизации) измерить эффект практически невозможно адекватным образом.

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

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

                                                      Да, с этим я даже и спорить не буду — в такой формулировке это и правда не имеет смысла. Но, тем не менее, rules of thumb в программировании вполне бывают.


                                                      И все-таки, как я понимаю, вы же все равно каким-то образом замеряли выхлоп от оптимизаций? Ведь не было же такого, что вы «добавил ключевое слово к функции» и закрыли таску?

                                                      Решили в итоге, что прогнать записанные данные с биржи всем скопом — на самом деле достаточно неплохое приближение.

                                                      +1
                                                      Вот например релевантный пример: Stackoverflow программисты. В сообществе к ним довольно быстро выработолся иммунитет. Хотя казалось бы, что не так? На stackoverflow публикуются хорошие ответы, проверенные разными специалистами, да и вы наверняка иногда копировали ответ со stackoverflow, который сразу начинал работать. Что же с этим подходом не так?

                                                      Вот и я призываю не быть stackoverflow-оптимизатором, а задействовать специально придуманные под это дело инструменты
                                                        +1
                                                        Для функции, тело которой сводится к return self.field или return this->field, инлайнинг в подавляющем большинстве случаев полезен (и я затрудняюсь придумать даже очень искусственный пример, когда это было бы не так)
                                                        помнится писал SIMD-числодробилку и пара лишних forceinline раздули код на мегабайт и сбрили процентов 20 перфа. Хотя там тоже казалось бы был не шибко большой кусок, по прикидкам сравнимый с накладными расходами от call
                                                      +3
                                                      Это не предварительная оптимизация ни в коей мере. В коде типа… совсем не матрицы перемножаются, и не выборка из массива делается по, к примеру, индексам, кратным исключительно трём, и не полином CRC высчитывается
                                                      в таком коде компилятор справится в подавляющем большинстве случаев, а даже если когда-то вдруг и не справится, прирост скорее всего не окупит время, потраченное вами на набор #[inline]. Будьте прагматичнее
                                                      –1
                                                      Тоесть вы глядите на вывод компилятора вместо бенчмарков?

                                                      Да, если компилятор сделал вызов функции для подобного вместо того чтобы заинлайнить, то я ему явно укажу что это надо инлайнить или вообще оформлю это в виде ассемблерной вставки. Для продакшена само собой, а для девелоперской версии пусть «умный» компилятор генерит чего ему в его тупую голову взбредёт, моя голова всё равно умнее его во сто крат.
                                                        0
                                                        Ну если вы действительно смотрите ассамблер и понимаете, как инструкции ассамблера матчатца на производительность, то это вполне себе альтернатива бенчмаркинга. Но все равно без должных измерений это никуда не годится
                                                          –1
                                                          Согласен, «без должных измерений это никуда не годится». Но в тривиальных случаях подобных данному бенчмаркинг это пустая трата времени. Я так считаю.
                                                          Ну если вы действительно смотрите ассамблер и понимаете, как инструкции ассамблера матчатца на производительность

                                                          Я начал программировать в 1986, сначала для i8080, после для 6502, после для к1801вм1, после для z80 и в 1989 перешёл на x86. И до 1993 я программировал исключительно на ассемблере потому как считал абсолютно все HLL слишком сложными и запутанными по сравнению с абсолютно понятным и без всяких сложностей языком ассемблера. После как-то постепенно перешёл на C и на Pascal. Но привычка видеть глазами готовый ассемблерный код в том же дебагере осталась. Поэтому естественно я «действительно смотрите ассамблер», раньше в 100% случаев, теперь только особо интересные места — «интересно, а во что компилятор мне перевёл вот это и вот это?» И если вижу уж откровенные ляпы, то либо делаю в этом месте ассемблерную вставку, или вставляю пресловутый
                                                          #[inline]
                                                          или какой надо по месту
                                                          #pragma что-то
                                                          .
                                                          Естественно что соревноваться с компилятором как он распараллелил обращения к памяти чтобы по тактам меньше выходило я не буду, но откровенные ляпы типа «call функция», а внутри функции лишь «inc eax» и «ret» я исправлю, хоть вручную, но заинлайню.
                                                            +2
                                                            Довольно часто такое «упрямство» с нежеланием измерить профит идет сильно во вред написанию кода. Конкретно в этом случае оно может и правильно, но общая тенденция пугает.

                                                            Помню например, когда только начинал программирование, нужно было разработать многопоточное приложение. К тому времени я знал, что мьютекс — это очень медленно, и старался его не использовать. Да, я сознательно допускал в некоторых местах кода состояние гонки только ради того, чтобы не использовать мьютекс. При этом частота вызова этих функций не превышала 1000 запросов в секунду. Сейчас же я пишу программы по 100 000 rps, использующие мьютексы, и мьютекс там даже близко не является бутылочным горлышком.

                                                            И я довольно часто вижу у текущих программистов поведение, которое практиковал сам раньше. «Ну, это дорого, не будем это использовать»
                                            +2

                                            На всякий еще отмечу, что (если я ничего не путаю) просто #[inline] в Rust'е — это лишь намек компилятору, который компилятор вполне может проигнорировать. Вот #[inline(always)] — это уже гарантированно перебивающий все эвристики приказ взять и заинлайнить вызов.

                                              +2
                                              компилятор rust может игнорировать любой из inline аттрибутов: «Note: #[inline] in every form is a hint, with no requirements on the language to place a copy of the attributed function in the caller.»
                                                0

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

                                                  +3
                                                  в компиляторах есть даже эвристики вида «после N forceinline не по делу считаем что программист дурак и игнорируем их»
                                        +2
                                        По мне лучше перебдеть, чем недобдеть.

                                        Не хочется совершать "шаманские действия на всякий случай". Почему тогда не идти дальше и не использовать #[inline(always)] раз уж мы уверены, что хуже не будет?.. Если LTO всегда (или в 99% случаев) хватает, то лишние атрибуты — это просто замусоривание кода.


                                        Если что, я не придираюсь, просто любопытствую.


                                        в языках C/C++, там действительно компилятору лучше знать какие переменные класть на стек, а какие в регистр, а вот инлайнить или нет функцию…

                                        На С++ уже несколько лет не пишу, но, если мне не изменяет память, обычная рекомендация — как раз не добавлять инлайн куда попало. А для тех, кто "точно знает, что делает" есть always_inline и noinline.


                                        Кстати, ключевое слово register начиная с С++17 ничего не делает.

                                          0

                                          Нашёл ещё вот такое: https://nnethercote.github.io/perf-book/inlining.html
                                          Но там вообще не дают советов как поступать, а рассказывают как оно работает. Ну и заодно предостерегают:


                                          You should measure again after adding inline attributes, because the effects can be unpredictable. Sometimes it has no effect because a nearby function that was previously inlined no longer is. Sometimes it slows the code down. Inlining can also affect compile times, especially cross-crate inlining which involves duplicating internal representations of the functions.
                                            +3
                                            На С++ уже несколько лет не пишу, но, если мне не изменяет память, обычная рекомендация — как раз не добавлять инлайн куда попало
                                            inline в с++ вообще не про инлайнинг

                              Only users with full accounts can post comments. Log in, please.