Беспредельный copy elision

yndx-antoshkka
yndx-antoshkka

Правила copy elision в секции [class.copy.elision] отлично работают!

Однако они подразумевают, что функция в исходном коде будет являться функцией и в бинарнике. Что не верно, для оптимизирующих компиляторов, умеющих встраивать тела функций и делать link-time оптимизации.

Для таких компиляторов в ассемблерном коде программы зачастую можно встретить цепочки копирования+удаления объекта. Другими словами можно увидеть нечто подобное после встраивания функций:

int caller() {
    A a;
    // ...

    A b = a;
    a.~A();
    // ...

    A c = b;
    b.~A();
    // ...
}

Подобные цепочки всегда генерируются при:

  • возврате субобъекта
  • возврате элемента из structured binding
  • std::move возвращаемого объекта
  • возврате локального объекта используя ссылку
  • возврате активного элемента union
  • передаче параметра по копии в функцию в конце блока
  • возврате объекта расположенного в std::aligned_storage
  • возврате параметра функции, принятого по копии

В https://apolukhin.github.io/papers/ultimate_copy_elision.html предлагаю убрать существующие ограничения на copy elision и всегда позволять его делать если: копируемый локальный не volatile объект или его не volatile субобъект не используются после копирования.

Такая формулировка не влияет на совместный scope объектов: Если код после inline выглядел как

int caller() {
    A a;
    // ... #1

    A b = a;
    a.~A();
    // ... #2

    A c = b;
    b.~A();
    // ... #3

    c.~A();
}

То с убранными ограничениями для copy elision код превратится в

int caller() {
    A a;
    // ... #1
    // ... #2
    // ... #3

    a.~A(); // бывший c.~A();
}

Подобная оптимизация может положительно сказаться на времени компиляции и на время link-time оптимизации: в данный момент компиляторы пытаются встраивать вызов copy конструктора и деструктора и оптимизировать всё их содержимое в контексте текущей функции. Это ведёт к тому, что оптимизатор вынужден оптимизировать несколько мегабайт низкоуровнего кода, вместо того, чтобы просто выкинуть эти два вызова функций и работать с намного меньшими объёмами низкоуровневых команд.

29
рейтинг
в разработке
2 комментария
yndx-antoshkka

Обсудили идею на встрече https://events.yandex.ru/events/cpp-party/15-jun-2018/ . Далее - стенограмма обсуждения:


Ultimate copy elisions
aaaand
Subobjects copy elision

Не вызывать копи или мув конструктор
Ограниченно сильно

В RVO

struct T {
    T() noexcept; T(T&&) noexcept; ~T() noexcept;
    void do_something() noexcept;
};

...

T d = shrink(update(produce()));

Простыня асма.
Фукнции заинлайнены.
Объект создается.

Создает новую переменную, это плохо
Несколько раз делает.

Вместо копирования Y в X можно переиспользовать Y. Используется copy elision.
Код уменьшается. Нет лишних деструкторов и конструкторов.
Ослабить требования.
Позволить смешивать оптимизации.

Поднять опт. с middle до front? Или copy elision в middle?

Какие проблемы решаются?
Код уменьшается, больше оптимизаций. => время компиляции.
Производительность выше, меньше бинари.
Проще учить плюсам людей, не надо думать о std::move на первых порах.

Продвинутое использование:
return path(__lhs) /= __rhs; // copy elision запрещен, not compile

return pair.second; // вызвать destructor от pair.first и продолжить
raturn get<0>(tuple); // same
auto [a,b] = foo(); return a; // same
return local_aggregate_variable.name // same

Работает часто только для inline
Проблемы для не trivially_destructible

return stringstream.str();
return get<int>(variant);
return path.string();

Not only for function returns
All together
Copy elision разрешен для любого non volatile объекта, распол. на стеке и его полей, если source объект не используется перед конструированием.

How far we should go?
Речь о возврате по значению.

Предложение скорее всего ломает пользовательский код, но скорее всего только не переносимый.
Если нарушить условия, код нельзя перенести с одного компилятора на другой и даже на другую версию одного компилятора и нельзя переносить между импл. stl.
Если ваши объекты посли копи или мув не равны, то вы неправильно пишите код.
Алгоритмы не будут работать с такими типами, который нарушают эти условия.
Уже сущетсвующие правила copy elision позволяют делать такие вещи.
Уже сейчас нужно осторожно использовать pmr.
Вывод: пишите regular типы. У вас и так всё сломается, лучше не пишите на С++

Вопрос о странном конструкторе, который передает указатели на несуществующие объекты в свой подклассы.
Ответ: в gcc такое поведение - UB. В любом случае так лучше не делать.
Денис: мы говорим только про return и rvalue?
Ответ: нет

Вопрос: пусть есть несколько локальных объектов, а ты возвр. тупл из этих объектов. Я не буду использовать эти объекты больше. Этот use case сюда входит?
Ответ: входит, но это пункт о передачи аргументов в функцию, она сложная
Вопрос: если мы не можем делать все честно, можно ли просто сделать конвертацию в x-value?
Ответ: Да, по сути можно, нужно подумать еще
Вопрос: Что с типами, внутри которых union?
Ответ: то же самое. Копирования его не будет
Денис: смущает отсутствие адреса. Быстро говорит. Отсутствие адреса смущает
Ответ: Надо подумать. Ответ не особо то есть
Вопрос: неверное направление. Компиляторы же уже могут это делать?
Ответ: нет, не умеют.
Вопрос: может нужно дать больше информации компилятору? На уровне стандарта. Разве сейчас уже нельзя выкинуть new и delete?
Ответ: Да, сейчас что-то можно, но не так, как хотелось бы. То есть конструкторы создаются, но могут не использоваться. Наша цель - убрать конструкторы.
Вопрос: Пусть есть динамик объект. С 1 по 2 пункт не будет нигде гарантии о возвращении под из не под объекта.
Ответ: для таких объектов нельзя такого делать. Json аллоцирует память, работать не будет
Вопрос: а для таких json, которые на стеке и все такое?
Ответ: Не работает в случаях, если конструкторы меняют стейт программы.
Вопрос: про асм. Представляю немного об этом. Как объекты на стеке выделяются. Для функции мы сдвигаем стек поинтер. Вы сделали большой объект. Нам потом нужно убрать большой кусок, но оставить кусок для подобъекта.
Ответ: работает только для за-inline-неных. компилятор может это разрулить
Вопрос: Но ведь нарушается порядок обращения к полям
Ответ: не особо понятно, о чем вы говорите. Если функция не inline, то говорить не о чем. Передвигать поля не нужно.

Ответ: elision - это убирание мув и копи конструктра.
Вопрос: не получится ли так, что при попытке заинлайнить деструктор, сген. компилем, что эффект уменьшения кода будет компенс. необходимостью многое удалять
Ответ: все зависит от разработчиков компилятора. Все зависит от них. Может только с О3, а может и с О0
Денис: что такое dd
Ответ: знать, что написано = default. Или что его нет вообще.
Денис: я отнасл. от вектора.
Ответ: хз

Не только для ретурнов:
Вопрос: пусть где-нибудь исключение и раскрутка стека. Как будет соглас. все удаления.
Ответ: таблицы свертки будут меньше, за счет того, что будет меньше кода. А дальше все компилятор думает.
Вопрос: часто говорите о компилятора
Ответ: ну да, им лучше знать
Вопрос: сейчас take by copy берет значение по копии. То, что вы обозначили на первых строчках решается с использованием universal reference. То есть мы научилсь договаривать.
Ответ: ну да, можно сделать мув. Но если работаем со старым кодом, ничего не получится улучшить. Если работаем с кодом, где пользователь использовал мув, то компилятор и так соптимизирует.
Вопрос: может поговорим о том, что если переменная используется последний раз, то пусть и так будет std::move.
Ответ: а если это цикл? и тд
Вопрос: Я этого так ждал!
Ответ: ура...
Денис: У двух копий должны быть разные адреса. Или все ломается.
Ответ: Сейчас работает в rvo так работает
Денис: если раньше взяли адрес от переменной, которую потом хотим заслать
Ответ: сложно, нужно следить за временем жизни. Эт рили сложно. Если адресная арифметика, компилятор может не делать ничего. Если ему сложно.
Денис: Оч мало случаев, когда адреса можно сравнивать
Ответ: ну да
Вопрос: Не понятно время жизни объекта, который внутрь инлайнится. Допустим T - это гард. Его деструктор в каком блоке вызывается? В функции или в блоке, из которого вызвана?
Ответ: функция заинлайнится, поэтому будет общий скоуп. Опять же, обязана заинлайниться.
Вопрос: как меняется время жизни объекта? в 3 пункте. Про умные указатели говорю. Там всякие каунтеры.
Ответ: надо как-то жестче специфицировать, как деструкторы должны пересекаться. Иначе в этом случае временный объект живет до конца выражения. Может нельзя деструкторы менять местами. Тут должен любой деструктор срабатывать.
Вопрос: про смарт птр. Я понимаю, что все работает со стандартными. А если не стандартные. Какая-то жесть.
Ответ: если у тебя что-то странное с внешними эффектами, не делай так. Покажи этот код ПОКАЖИ ЕГО НАМ
Вопрос: Я придумал юз кейс, когда код может сломаться, который раньше работал. Если тип Т - это некий таймер, который при констр. взводиться и в дестр. записывает в лог. То в первой строчке вызовится не там и 2 раза.
Ответ: обычно таймеры не копируются.
Вопрос: но иногда копируются
Ответ: покажите нам этот код, пришлите нам, мы на него посмотрим.
Вопрос: может просто добавить аттрибут
Ответ: есть такая штука volatile, то для нее ничего не сработает
Вопрос: протухший код можно детектить?
Ответ: можно, будет млн сообщений и придется кучу кода менять. Вообще уже в стандарте кучу кода придется менять, делать trivially_destructible и тд.
Вопрос: думал, как сломать. Договорились, что сайд эффекты - это плохо. Допустим я считаю в компайлтайме. Так уже можно copy elision?
Ответ: да, там уже это можно
Вопрос: не может ли за из-за copy elision в ран и компайл тайме отличаться поведение
Ответ: это возможно уже сейчас

Голосование:

копать во все это?
40 за
15 почти за
5 нейтрально
0 против
0 строго против
=> копай дальше

1 пункт:
17 за
27 почти за
3 нейтрально
0 против
0 строго против

2 пункт:
нет смысла голосовать
нужно ли копать в него?
15 за
15 почти за
6 нейтрально
3 против
2 строго против

Человек: против, потому что нарушает дух языка. С выиграл, потому что язык, который работал везде и позволял запихать биты туда, куда надо. Выше предложение это нарушает. Духа нет. Бездушно.
Ответ: Свойство это уже сейчас есть. С++ такой.
Антон: Не согласен, так как все оптимизации, невидимые пользователю, разрешены.

Человек: я против, потому что не понимаю, как это можно сделать

4 пункт(посылать в функции аргументы):
26 за
24 почти за
7 нейтрально
4 против
0 строго против

yndx-antoshkka
Обновлено 
Mikhail Shostak

Тут попался код из std::thread:

std::invoke(decay_copy(std::forward<Function>(f)), decay_copy(std::forward<Args>(args))...);

where decay_copy is defined as

template <class T>
std::decay_t<T> decay_copy(T&& v) { return std::forward<T>(v); }

https://en.cppreference.com/w/cpp/thread/thread/thread

Я так понимаю с этим предложением подобный код может поломаться и перестать копировать. Но, видимо, предложенный copy elision в случае с потоками не будет срабатывать даже если там всё заинлайнится т.к. decay_copy будет вызывается на стеке нового потока и f и args уже не будут являться "локальными" переменными. Возможно где-то еще есть подобное копирование, но где такое может понадобиться с ходу придумать не получилось.

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