Каковы последствия для себя потребления и возвращения его?
Я читал такие вопросы, как Почему функция, принимающая Box
Чтобы сделать более простой пример, представьте, что я хочу создать тип коллекции, который гарантированно не будет пустым. Чтобы достичь этого, операция "удаление" должна потреблять коллекцию и при необходимости возвращать себя.
struct NonEmptyCollection { ... }
impl NonEmptyCollection {
fn pop(mut self) -> Option<Self> {
if self.len() == 1 {
None
} else {
// really remove the element here
Some(self)
}
}
}
(Я полагаю, он должен вернуть значение, которое он также удалил из списка, но это всего лишь пример.) Теперь допустим, что я вызываю эту функцию:
let mut c = NonEmptyCollection::new(...);
if let Some(new_c) = c.pop() {
c = new_c
} else {
// never use c again
}
Что на самом деле происходит с памятью об объекте? Что делать, если у меня есть код вроде:
let mut opt: Option<NonEmptyCollection> = Some(NonEmptyCollection::new(...));
opt = opt.take().pop();
Сигнатура функции не может гарантировать, что возвращаемый объект на самом деле тот же, так что возможна оптимизация? Применяется ли что-то вроде оптимизации возвращаемого значения в C++, позволяя "возвращать" возвращаемый объект в ту же память, в которой он находился раньше? Если у меня есть выбор между интерфейсом, подобным описанному выше, и интерфейсом, где вызывающий должен иметь дело с временем жизни:
enum PopResult {
StillValid,
Dead
};
impl NonEmptyCollection {
fn pop(&mut self) -> PopResult {
// really remove the element
if self.len() == 0 { PopResult::Dead } else { PopResult::StillValid }
}
}
есть ли причина выбирать этот более грязный интерфейс из соображений производительности? В ответе на второй пример, который я привел, trentcl рекомендует хранить Option
s в структуре данных, чтобы позволить вызывающему сделать изменение на месте вместо того, чтобы делать remove
с последующим insert
каждый раз. Будет ли этот грязный интерфейс более быстрой альтернативой?
1 ответ
YMMV
В зависимости от прихоти оптимизатора, вы можете получить:
- близко к неоперативному,
- несколько ходов регистра,
- количество бит-копий.
Это будет зависеть от того:
- звонок встроен или нет,
- вызывающая сторона переназначает исходную переменную или создает новую переменную (и насколько хорошо LLVM справляется с повторным использованием мертвого пространства),
size_of::<Self>()
,
Единственные гарантии, которые вы получаете, - это то, что не произойдет глубокое копирование, так как .clone()
вызов.
Для чего-то еще, вы должны проверить LLVM IR или сборку.