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