Copy elision для субобъектов

yndx-antoshkka
yndx-antoshkka

В C++17 если мы возвращаем субобъект из функции, для него невозможно применить copy elision. Получается, что в данном коде:

#include <utility>
#include <string>

std::pair<std::string, std::string> produce();

static std::string first_non_empty() {
    auto v = produce();
    if (!v.first.empty()) {
        return v.first;
    }
    return v.second;
}

int example_1() {
    return first_non_empty().size();
}

Будет копирование строки из v.second.

Ситуацию можно немного улучшить, если явно добавить std::move:

#include <utility>
#include <string>

std::pair<std::string, std::string> produce();

static std::string first_non_empty_move() {
    auto v = produce();
    if (!v.first.empty()) {
        return std::move(v.first);
    }
    return std::move(v.second);
}

int example_1_move() {
    return first_non_empty_move().size();
}

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

#include <utility>
#include <string>

std::pair<std::string, std::string> produce();

int example_1_optimized() {
    auto v = produce();
    if (!v.first.empty()) {
        return v.first.size();
    }
    return v.second.size();
}

Предлагаю добавить больше разрешающих правил для copy elision в секцию [class.copy.elision] или разрешить coy elision для всех случаев, когда копируемый объект или субобъект не используются после копирования.

Подробности (вместе с ассембленым кодом, доказательствами что copy elision оптимальнее чем std::move) доступны по ссыле: https://apolukhin.github.io/papers/subobjects_copy_elision.html

12
рейтинг
в разработке
10 комментариев
Andrey Davydov
Я, наверное, не понимаю каких-то фундаментальных вещей. Я всегда думал, что NRVO работает так -- перед вызовом функции выделяется место куда положить результат и локальная переменная, возвращаемая из тела функции, выделяется сразу в этом месте. Но если локальная переменная не совпадает по типу с возвращаемым значением функции, как в Вашем примере, то что может соптимизировать компилятор?
Andrey Davydov
yndx-antoshkka
Вы всё верно описали для NRVO. Но идея предложения не связана с NRVO напрямую, а предлагает позволить убрать лишние копирования для случаев, не попадающих под NRVO.

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

Так в example_1_optimized() функция first_non_empty() была заинлайнена, v.first/v.second переиспользованы без копирования. (Если совсем по честному, то в данном примере перед переиспользованием должен быть вызов деструктора для неиспользуемого субобъекта; но я предположил, что компилятор достаточно умный чтобы понять что порядок удаления объектов не имеет значения, и не стал загромождать пример).
yndx-antoshkka
Andrey Davydov
yndx-antoshkka, Прочитал Ваш proposal еще раз и, кажется, понял как предполагается subobject copy ellision должен работать. Но возникают следующие вопросы:
1. Если в Вашем примере функция first_non_empty не будет проинлайнена, copy ellision не сработает и строка будет копироваться (не перемещаться!), а в случае first_non_empty_move зато точно не сработает copy ellision. Так использовать std::move или нет тому кто реализует функцию first_non_empty?
2. В случае (#1)
auto v = produce();
return v.first;
предлагаемый Вами copy ellision сработает, а в случае (#2)
auto v = produce();
decltype(auto) v_first = std::get<0>(v);
decltype(auto) v_second = std::get<1>(v);
return v_first;
или, что эквивалентно, (#3)
auto [v_first, v_second] = produce();
return v_first;
не сработает. Получается #1 будет эффективнее чем #3, что, по меньшей мере, будет крайне неожиданно.
Andrey Davydov
yndx-antoshkka
Для пункта 1 я напишу отдельное предложение, по обязательному implicit move при возвращении субобъектов. Но там правила получаются намного более жёсткие, чем в случае с copy elision.

В случае с пунктом 2, проблема в данный момент присутствует и для NRVO https://godbolt.org/g/XKopwS . Чинить такое поведение думаю разом, так же в отдельном proposal.
yndx-antoshkka
yndx-antoshkka
Andrey Davydov, а вообще "VII. Ultimate solution" в бумаге решает проблемы пункта 1 и 2 ( и ещё десяток случаев). Но я очень сомневаюсь что его согласятся принять в ближайшее время.
yndx-antoshkka
Andrey Davydov
yndx-antoshkka,
> Для пункта 1 я напишу отдельное предложение, по обязательному implicit move при возвращении субобъектов.
Ок, но с точки зрения прикладного программиста бесполезно текущее предложение без обязательного implicit move, ведь ему придется явно использовать std::move, что убьет возможность copy elision.
> В случае с пунктом 2, проблема в данный момент присутствует и для NRVO.
Да, но сейчас нет такого, что при механической замене на structured binding NRVO перестает работать (при переходе от моего примера #1 к #3). И нет разницы в смысле эффективности между pair и tuple, а если принять Ваш proposal, то станет эффективнее писать
std::pair v = ...;
return v.first;
чем
std::tuple v = ...;
return std::get<0>(v);
Andrey Davydov
yndx-antoshkka
Верно. Прорабатываю более общее решение|, которое учитывает эти случаи.
yndx-antoshkka
Antervis
можно даже несколько переформулировать: неявный move/nrvo для объектов/субобъектов, не используемых после return. Чтобы Class a {args...}; auto b = move(a); оптимизировался в Class b {args...};
Antervis
Antervis
поправка: *не используемых после move/return
Antervis
yndx-antoshkka
Последние 3 дня как раз прорабатываю более общее решение, оптимизирующее ещё большее количество случаев.

Как приведу в должный вид - опубликую на сайте, отдельным предложением.
yndx-antoshkka
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения