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 — но с ними, понятное дело, часть инструментария начинает скрипеть.
В голосовании не хватает пункта "копировать код". Всякий скопированный код на самом деле уменьшает связность кодовой базы и сохраняет код более простым. Если очень надо сохранить одинаковый код, то он спокойно выносится во внешние модули.
Вопрос 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 далеко не в каждой ситуации подходит таки, часто хотелось бы просто статически делегировать поля-методы и все, не разводя большую инфраструктуру.
В данном случае лайфтаймы проставлять не нужно.
Вопрос не по теме статьи: расставлять #[inline]
в таких случаях принято? Это правда даёт профит?
Для мелких публичных функций. Например, метод для получения длины вектора — это ведь по сути просто обращение к полю структуры, и вызывать call для неё слишком накладно. Или инкремент счётчика — если заинлайнить — это одна-две инструкции, полный вызов функции будет стоить дороже, чем само действие.
Внутри одного и того же крейта компилятор сам прекрасно инлайнит, так что непубличным функциям он особо не нужен. А вот чтобы инлайнить функцию из другого крейта, компилятор должен иметь о ней больше информации, чем об обычной функции. Именно благодаря inline эта информация сохраняется, а не выкидывается при компиляции того крейта.
Спасибо. Зачем вообще нужен инлайн я знаю, но почему-то думал, что компилятор и сам достаточно умный. Честно говоря, проверять лень, но может где-то есть хороший разбор этой темы с рекомендациями как и когда поступать?
Беглый поиск выдал два противоречивых совета. Вот тут тоже говорят, что мелкие функции стоит помечать как #[inline]
так как без LTO компилятор не справляется. С другой стороны, нашёл упоминание, что инлайн замедляет компиляцию и лучше как раз использовать LTO, если нужно выжать максимум. Правда оба обсуждения довольно старые.
В общем, мне интересно как всё-таки стоит поступать авторам библиотек и достаточно ли полагаться на LTO или есть какие-то нюансы.
Мне тоже так кажется, но периодически попадаются натыканные #[inline]
, вот и возникают сомнения.
#[inline]
я лично ничего плохого не вижу. Это не указание register
в языках C/C++, там действительно компилятору лучше знать какие переменные класть на стек, а какие в регистр, а вот инлайнить или нет функцию… IMHO от такого указания во всяком случае хуже уж точно не будет.Я ведь надеюсь под «знает» понимается «написал кучу бенчмарков и замерил профит с разных путей выполнения»?
#[inline]
fn get_hp(&self) -> f32 {
self.hp
}
«куча бенчмарков» вовсе не нужна, достаточно собственной головы. А «куча бенчмарков» в подобном это сравнимо с «обжегшись горячим молоком теперь на воду всегда дуть».> увидеть есть там bottle neck или нет и лишь в случае сомнений уже сделать бенчмарк
Почему тогда не наоборот, увидеть bottle neck и соптимизировать? Зачем эта предварительная оптимизация?
#[inline]
fn get_hp(&self) -> f32 {
self.hp
}
совсем не матрицы перемножаются, и не выборка из массива делается по, к примеру, индексам, кратным исключительно трём, и не полином CRC высчитывается. Надо быть полным идиотом чтобы подобное не заинлайнить и, извините, надо быть полным идиотом чтобы для подобного кода делать бенчмарки или, о боже, оборачивать подобный код тестами.Да, безусловно, какие-то утверждения будут правильными, например что инлайнинг полезен всегда. Это нормально. Если бы все утверждения какого-то конкретного оппонента были неправильными, то это было бы слишком просто: можно было бы делать все в точности до наоборот того, что говорит оппонент. И возможно, что в случае с inline вы правы, но это не отменяет общего правила: нужно мерять.
Даже по данному примеру, один из комментаторов спросил, почему тогда уж не использовать inline(always) вместо inline? Раз уж такая уверенность? Ведь даже в офф документации сказано, что inline не обязан инлайнить (да и inline(always) вроде бы тоже). Вот тут мы и видим пример веры в «божьше слово», что вставка какого-то магического слова внезапно ускорит нашу программу. От этого-то я и хочу предостеречь
> В некоторых случаях (особенно если мы обсуждаем подобные микрооптимизации) измерить эффект практически невозможно адекватным образом.
Да, я верю. У меня не было опыта в таких микрооптимизациях, поэтому тут я не компетентен. Но я подозреваю, что у большинства других программистов тоже не было. Незачем навязывать им ложные убеждения.
И все-таки, как я понимаю, вы же все равно каким-то образом замеряли выхлоп от оптимизаций? Ведь не было же такого, что вы «добавил ключевое слово к функции» и закрыли таску?
Вот и я призываю не быть 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)]
— это уже гарантированно перебивающий все эвристики приказ взять и заинлайнить вызов.
По мне лучше перебдеть, чем недобдеть.
Не хочется совершать "шаманские действия на всякий случай". Почему тогда не идти дальше и не использовать #[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 в с++ вообще не про инлайнинг
Как не копировать код в Rust