Как стать автором
Обновить

Комментарии 31

Было бы интересно услышать комментарии по поводу распространенного сочетания:
C f() {
  if(/*condition*/)
    return C();

  C local_variable;
  // Действия с local_variable
  return local_variable;  
}

Как мне кажется тут как раз серая зона. RVO/NRVO может сработать, а может и нет. Зависит от компилятора, платформы и даже еще от одного факта. От того, всегда или нет выполнено условие /*condition*/. Если выполнено всегда, компилятор может понять это, и оптимизировать код, оставив одну ветку if().

Думаю, что если мы перепишем пример так:

C f() {
  C local_variable;
  if(/*condition*/)
    return local_variable;

  // Действия с local_variable
  return local_variable;  
}

шансов на NRVO прибавится.

да, интересен случай, когда condition заранее неизвестен.

кажется, что RVO для компилятора понятнее и удобнее, чем NRVO.
поэтому обычно считается, что после проверки if(/*condition*/), быстрый возврат return {}; — это хорошо.

и еще есть принцип объявления переменной как можно ближе к месту использования.
в совокупности эти два совета приводят нас к указанному мной варианту кода.

вроде бы, он хорош со всех сторон. но получается, что шансы на получения оптимизации для нас уменьшаются?

кажется, что RVO для компилятора понятнее и удобнее, чем NRVO.

Похоже что так. RVO от части перекочевало из оптимизации в правила обязательной отмены копирования. Похоже, тенденция от стандарта к стандарту в том, чтоб уменьшать серую зону, в которой компилятор сам решает применять ли RVO/NRVO или нет.

поэтому обычно считается, что после проверки if(/condition/), быстрый возврат return {}; — это хорошо.

Кроме того, отмечу, что при таком подходе (return early pattern), код становится менее громоздким и часто лучше читается, на мой взгляд.

вроде бы, он хорош со всех сторон. но получается, что шансы на получения оптимизации для нас уменьшаются?

Кажется, что да. Я проверил на clang (MacOS X). На нашем втором примере (где в двух местах return local_variable; ) NRVO был применен. В нашем первом примере, если вызвать вот так:

NRVOCheck f(int k) {
  if (k == 3)
    return NRVOCheck();
  NRVOCheck local_variable;
  // Действия с local_variable
  return local_variable;
}
f(2);

NRVO не сработал.

А бывает такое, чтобы в первом return был применён RVO, а во втором - NRVO? Или оптимизация одна на всю функцию, либо так либо так?

Думаю, в одной функции может быть, чтоб иногда было RVO, иногда NRVO, а иногда было вызвано копирование. Т.е. если говорить языком стандарта, то и RVO, и NRVO это частные случаи отмены копирования (copy elision). Крайняя точка принятия решения о том, случится ли отмена копирования - это точка вызова ф-ции в Run Time. Технически даже можно представить, что в ряде ситуаций, компилятор может оставить место под результат работы функции на стеке и начать ее исполнять. И уже позже, создать в оставленном месте результат работы функции. А если так не будет получатся, то по возможности выполнить перемещение в оставленное место. А если и это не возможно, то копирование.

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


Ведь уже появился синтаксис атрибутов, что мешает наконец завести атрибуты для включения гарантий нужного поведения?

Ведь уже появился синтаксис атрибутов, что мешает наконец завести атрибуты для включения гарантий нужного поведения?

Тут традиционный компромис:

Гарантированное поведение на одной чаше весов.

На другой, эффективность на разных платформах.

С++ решает обычно решает такие штуки в строну эффективности. С другой стороны, синтаксис языка и так сложен. Кажется, что нежелание добавлять еще опций по указанию таких штук (например, через атрибуты), тоже можно понять. Думаете пользовался бы популярностью атрибут "Не применять RVO/NRVO"?

Ну и в дополнение, можно вспомнить о том, как принимаются решения о добавлении новых особенностей в C++ :)

Как следствие получаем C++, как весьма сложную конструкцию, с очень высоким порогом входа. На мой взгляд, сейчас язык непомерно сложен, но стал по современнее последние годы.

Думаете пользовался бы популярностью атрибут "Не применять RVO/NRVO"?

Такой - точно нет, с другой стороны, возможно, некоторой популярностью бы пользовался атрибут "выдавать ошибку, если в данном месте не удается применить copy elision". С третьей стороны, это всего лишь оптимизация, и тогда таких атрибутов надо наделать вообще для любых оптимизаций, потому что чем они хуже? Мы утонем в атрибутах с таким подходом.

некоторой популярностью бы пользовался атрибут "выдавать ошибку, если в данном месте не удается применить copy elision".

Такой, кажется, что мог бы использоваться. Для уверенности, что сработает.

Мы утонем в атрибутах с таким подходом.

Да. Есть такое дело :)

Думаете пользовался бы популярностью атрибут "Не применять RVO/NRVO"?

Наоборот, было бы полезно иметь "вот здесь должен быть rvo", "вот здесь только хвостовая рекурсия", если разработчик переписывает код и это уже не получается гарантировать, то бить по рукам, пусть думает дальше.


Вижу мне уже вроде намекают, что такие атрибуты есть. Не знаю, я вообще на C++-сник уже давно.

Наоборот, было бы полезно иметь "вот здесь должен быть rvo", "вот здесь только хвостовая рекурсия", если разработчик переписывает код и это уже не получается гарантировать, то бить по рукам, пусть думает дальше.

Да, это отличный подход! Много таких штук в C++, которые не понятно сработают или нет.

Вижу мне уже вроде намекают, что такие атрибуты есть. Не знаю, я вообще на C++-сник уже давно.

Если не сложно, уточните п-та, о чем речь. Мне про них не известно, а если есть я бы хотел знать. Если информация не очень точная, можно и в личку. Спасибо!

Это я про ответ tbl ниже

Про хвостовую рекурсию в clang уже musttail вроде как есть.

[[fail_if_not(rvo, nrvo)]]

Что-то я такого атрибута ни где найти не могу. Это пример, как могло бы выглядеть? Вообщем ссылочку пришлите п-та, если знаете про такой стандартный или не стандартный атрибут.

Это ирония по поводу того, что разработчики C++ предлагают или ищут новые атрибуты на каждый новый чих стандарта. Например, недавно всплывало при обсуждении лайфтаймов и временных объектов, чтобы починить поломанные "by design" range-based for loop в C++23: [[short_and_concise_lifetime_annotation]] и [[probably_broken_if_this_is_ignored]]

А если будет, то вместо создания local_variable компилятор сразу создаст result конструктором по умолчанию в точке вызова функции f(). А функция f() будет выполнять действия сразу с переменной result.
Попытка максимально упростить описание может привести к неправильному понимаю того, что на самом деле происходит. На практике компилятор просто выделяет память под объект, никакого конструктора по умолчанию не вызывается (его вообще может не быть), а объект создается уже поверх выделенной памяти внутри функции, в которую передается указатель на выделенную память, тем конструктором, который используется в RVO.
Вот пример:
#include <iostream>

class A
{
public:

    A() noexcept  { std::cout << "A()"; }
    A(const A& that) = delete;
    A(A&& that) noexcept = delete;
    A(int x, int y) noexcept : x(x), y(y) { std::cout << "A(int, int)"; };

    A& operator=(const A& that) = delete;
    A& operator=(A&& that) noexcept = delete;

    int x;
    int y;
};

A func(int x, int y) {
    return A(x, y);
}

int main()
{
    A a = func(5, 5);

    return a.x + a.y;
}

Спасибо за уточнение.

Мне кажется, для начала было бы неплохо копнуть чуть-чуть глубже. А именно, рассмотреть вопрос, а как физически из функций возвращаются объекты, которые не влезают в машинное слово (регистр процессора). Т.е. результат int f() возвращается в регистре-аккумуляторе, но как быть с std::string f()? Строка может быть любой длины и потому размещается в куче; но кто тогда должен отвечать за освобождение этой памяти, когда результат больше не требуется? Сама функция f очевидно этим заниматься не может, значит, это должна делать вызывающая функция. Но тогда и выделять память должен тот, кто её освобождает (у них с f, в принципе, аллокаторы могут и не совпадать).

И тут мы приходим к тому, что объявление C f() для крупных типов есть всего лишь синтаксический сахар для void f(C * result) . Это именно указатель на память, не ссылка на объект; обязанность вызвать at-конструктор в этой памяти лежит на функции f. Но если знать про наличие этого указателя, вся "оптимизация" rvo/nrvo сводится к тому, чтобы компилятор не тупанул и не завёл ещё одну переменную для временного хранения результата.

Кстати, в языке Си вариант C f() разрешён синтаксисом и в С++, собственно, появился оттудова. Но в Си подобный неявный сахар считается табу, атата и харам, причём настолько, что такой синтаксис ставит многих знакомых мне сишников в ступор. Хочешь вернуть структуру — передавай указатель. Точка.

Нет, это не сахар, это именно работа над объектами (известной на момент компиляции длины) на стеке, а не в куче. И std::string - это объект известной длины, содержащий в себе длину строки и указатель на область памяти в куче, содержащей само содержимое строки. *RVO для std::string - это ожидание, что сохрантсяя оригинальная строка в куче, а не занятие байтоперекладыванием. Т.е. при *RVO компилятор выделяет место на стеке именно в том месте, где будет указатель вершины стека при возврате из функции. А если это невозможно, то используются move-конструкторы (поэтому отсутствие запрета существования move-конструктора нужно в этих случаях для *RVO)

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

Это хорошая тема! Антон Жилин писал paper (предложение в Стандарт) о гарантированном NRVO: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2025r2.html

Сейчас ни один компилятор не поддерживает все описанные в paper кейсы. Я отправлял патч в Clang, чтобы покрыть на 20% больше кейсов, чем поддерживается сейчас, но до конца не добил тему, потому что патчи медленно принимаются.

Быть может, я упускаю из вида, какие-то особые платформы, но кажется, что уже есть целый ряд случаев, когда любой современный компилятор сделает NRVO. Надеюсь на предложение Антона обратят внимание...

Сейчас ни один компилятор не поддерживает все описанные в paper кейсы. Я отправлял патч в Clang, чтобы покрыть на 20% больше кейсов, чем поддерживается сейчас, но до конца не добил тему, потому что патчи медленно принимаются.

А что за возражения? По делу или просто не обращают внимание на патч?

Есть много кейсов, где может применяться NRVO. В paper описано 20 кейсов, сейчас Clang делает это только в 13 кейсах (13/20). Мой патч добавил бы еще 4 кейса (было бы 17/20).

https://reviews.llvm.org/D119792 - тут я описал, что делаю.

Возражения там по делу, но просто стало влом тащить до конца, потому что КПД контрибьютинга в подобные опенсорсные проекты очень низок.

Спасибо за ответ и за комментарий! Я посмотрю патч и внимательнее прочитаю предложение Антона. Интересно.

Как интересно обязательный copy elision "дружит" с variadic functions и __stdcall?

Как будто c variadic functions должно работать. Я к тому, что еще до вызова variadic functions можно прикинуть сколько места нужно для возвращаемого значения.

А в чем сомнения по поводу __stdcall?

Насколько я помню, в обсуждениях предложения в стандарт упоминались какие-то проблемы на этот счёт. Могу ошибаться.

Опечатка:

Здесь NRVO может быть применено, поскольку N конструируется из p.

В коде нет переменной p. Должна быть n

Спасибо большое! Поправил.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий