Pull to refresh

Comments 46

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

Считается антипаттерном: 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 — но с ними, понятное дело, часть инструментария начинает скрипеть.

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

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

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

Вопрос 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>>

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



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

А я же ссылку возвращаю. Или он из лайфтайма self выводит?

UFO just landed and posted this here

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

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


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

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


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


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

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

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

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

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

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

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

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

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

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

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

И все-таки, как я понимаю, вы же все равно каким-то образом замеряли выхлоп от оптимизаций? Ведь не было же такого, что вы «добавил ключевое слово к функции» и закрыли таску?
UFO just landed and posted this here
Вот например релевантный пример: Stackoverflow программисты. В сообществе к ним довольно быстро выработолся иммунитет. Хотя казалось бы, что не так? На stackoverflow публикуются хорошие ответы, проверенные разными специалистами, да и вы наверняка иногда копировали ответ со stackoverflow, который сразу начинал работать. Что же с этим подходом не так?

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

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

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

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

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

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

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

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

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


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


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

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


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

Нашёл ещё вот такое: 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.
На С++ уже несколько лет не пишу, но, если мне не изменяет память, обычная рекомендация — как раз не добавлять инлайн куда попало
inline в с++ вообще не про инлайнинг
Sign up to leave a comment.

Articles