Каковы последствия для себя потребления и возвращения его?

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

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

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 рекомендует хранить Options в структуре данных, чтобы позволить вызывающему сделать изменение на месте вместо того, чтобы делать remove с последующим insert каждый раз. Будет ли этот грязный интерфейс более быстрой альтернативой?

1 ответ

YMMV

В зависимости от прихоти оптимизатора, вы можете получить:

  • близко к неоперативному,
  • несколько ходов регистра,
  • количество бит-копий.

Это будет зависеть от того:

  • звонок встроен или нет,
  • вызывающая сторона переназначает исходную переменную или создает новую переменную (и насколько хорошо LLVM справляется с повторным использованием мертвого пространства),
  • size_of::<Self>(),

Единственные гарантии, которые вы получаете, - это то, что не произойдет глубокое копирование, так как .clone() вызов.

Для чего-то еще, вы должны проверить LLVM IR или сборку.

Другие вопросы по тегам