THISNOTES Telegram 198
#cpp

RVO/NRVO 1/2.

Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь донести все детали, чтобы картина сложилась.

Для начала про терминологию в стандарте. Формально есть общий термин copy elision, который в целом о том, чтобы не делать лишних копий объекта и создавать его сразу там, где нужно. Например ниже примеры copy elision:

T t = T(); // not T() + operator= but only T();
T t = T(T(T(T(T(T(T(1))))))); // not T(1) + many T(const T&) + operator= but only T(1)


Вот что говорит стандарт ([class.copy.elision]):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

Т.е. компилятор в праве применять эту оптимизацию в обход copy-/move-кторов, даже если в них или деструкторе есть какие-то сайдэффекты.

Так же тут есть 4 пункта. В первом говорится, что тип инициализирующего выражения и тип переменной с итоговым результатом должны совпадать. Это главное условие для RVO/NRVO.

RVO/NRVO это частные случаи copy elision, которые происходят в результате возврата чего-либо из функции. Далее мы будем пользовать примерно таким классом, чтобы понять, что происходит с объектами:

#define TELL(str) { std::cout << str << std::endl; }
struct S {
S() TELL("def")
S(const S&) TELL("cp")
S(S&&) TELL("mv")
};


Так же есть такой термин как temporary materialization – принцип, согласно которому prvalue-выражение не создаётся физически в памяти до тех пор, пока оно не будет присвоено в не prvalue-объект.

Return Value Optimization по сути и есть пример temporary materialization. Посмотрим на базовый пример (godbolt):

S f() { return S(); }
S s = f();


Тут у нас prvalue-выражение S(), благодаря чему мы можем не создавать переменную в момент его появления в return и присвоить создать лишь в s. И из всех интересных нам конструкторов будет вызван лишь конструктор по умолчанию. cv-квалификаторы в сигнатуре функции в данном случае нам ничего не портят (gb).

А если мы можем (не)явно сконструировать возвращаемый тип из выражения в return? Добавим в S такой конструктор:

S(std::string&&) TELL("str&&")

и собственно пример (gb):

S f() { return std::string(10, '1'); }
S s = f();


Всё хорошо, даже не смотря на то, что тип в return и в сигнатуре не совпадают. По факту компилятор представляет выражение в return как S(std::string(10, ‘1’)), что тоже является prvalue. Даже в таком случае всё в порядке (gb):

S f() {
std::string s(10, '1');
return s;
}


Всё корректно. Т.к. компилятор справляется тут даже сделать неявный move переменной s. Причём это всё ещё RVO.

Стоит понимать, что с C++17 RVO — гарантированная штука и даже не оптимизация.
Давайте поймём, как оно работает. У компилятора есть адрес места в памяти, где должен оказаться результат выполнения функции (называется return slot). Когда выполняются условия для RVO, компилятор может сразу создать объект из вашего return в нужном месте в памяти. Т.е. это скорее про свойства инициализации объектов.

Named Return Value Optimization уже оптимизация (причём не гарантированная, тут компиляторы делают кто что может). Базовый пример (gb):

S f() { S s; return s; }

Или пример посложнее, где в S появился int x (gb):

S f() { S s; s.x = 1; return s; }

void gg(S& s) { s.x = 1; }
S g() {
S s; gg(s); return s;
}


Компилятор всё ещё может создавать локальную переменную s в функциях сразу в return slot итогового объекта.
Для NRVO правда volatile для локальной переменной всё портит (gb).

Почему важно знать об этих штуках? Потому что NRVO можно сломать (gb):

S f() {
S s;
return std::move(s); // return S&&
}


Теперь компилятора обязан создать объект и мувнуть его, потому что это то, о чём вы его попросили явно. Так же можно думать, что у выражения в return и в сигнатуре теперь разные типы (S&& и S).
7👍4



tgoop.com/thisnotes/198
Create:
Last Update:

#cpp

RVO/NRVO 1/2.

Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь донести все детали, чтобы картина сложилась.

Для начала про терминологию в стандарте. Формально есть общий термин copy elision, который в целом о том, чтобы не делать лишних копий объекта и создавать его сразу там, где нужно. Например ниже примеры copy elision:

T t = T(); // not T() + operator= but only T();
T t = T(T(T(T(T(T(T(1))))))); // not T(1) + many T(const T&) + operator= but only T(1)


Вот что говорит стандарт ([class.copy.elision]):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

Т.е. компилятор в праве применять эту оптимизацию в обход copy-/move-кторов, даже если в них или деструкторе есть какие-то сайдэффекты.

Так же тут есть 4 пункта. В первом говорится, что тип инициализирующего выражения и тип переменной с итоговым результатом должны совпадать. Это главное условие для RVO/NRVO.

RVO/NRVO это частные случаи copy elision, которые происходят в результате возврата чего-либо из функции. Далее мы будем пользовать примерно таким классом, чтобы понять, что происходит с объектами:

#define TELL(str) { std::cout << str << std::endl; }
struct S {
S() TELL("def")
S(const S&) TELL("cp")
S(S&&) TELL("mv")
};


Так же есть такой термин как temporary materialization – принцип, согласно которому prvalue-выражение не создаётся физически в памяти до тех пор, пока оно не будет присвоено в не prvalue-объект.

Return Value Optimization по сути и есть пример temporary materialization. Посмотрим на базовый пример (godbolt):

S f() { return S(); }
S s = f();


Тут у нас prvalue-выражение S(), благодаря чему мы можем не создавать переменную в момент его появления в return и присвоить создать лишь в s. И из всех интересных нам конструкторов будет вызван лишь конструктор по умолчанию. cv-квалификаторы в сигнатуре функции в данном случае нам ничего не портят (gb).

А если мы можем (не)явно сконструировать возвращаемый тип из выражения в return? Добавим в S такой конструктор:

S(std::string&&) TELL("str&&")

и собственно пример (gb):

S f() { return std::string(10, '1'); }
S s = f();


Всё хорошо, даже не смотря на то, что тип в return и в сигнатуре не совпадают. По факту компилятор представляет выражение в return как S(std::string(10, ‘1’)), что тоже является prvalue. Даже в таком случае всё в порядке (gb):

S f() {
std::string s(10, '1');
return s;
}


Всё корректно. Т.к. компилятор справляется тут даже сделать неявный move переменной s. Причём это всё ещё RVO.

Стоит понимать, что с C++17 RVO — гарантированная штука и даже не оптимизация.
Давайте поймём, как оно работает. У компилятора есть адрес места в памяти, где должен оказаться результат выполнения функции (называется return slot). Когда выполняются условия для RVO, компилятор может сразу создать объект из вашего return в нужном месте в памяти. Т.е. это скорее про свойства инициализации объектов.

Named Return Value Optimization уже оптимизация (причём не гарантированная, тут компиляторы делают кто что может). Базовый пример (gb):

S f() { S s; return s; }

Или пример посложнее, где в S появился int x (gb):

S f() { S s; s.x = 1; return s; }

void gg(S& s) { s.x = 1; }
S g() {
S s; gg(s); return s;
}


Компилятор всё ещё может создавать локальную переменную s в функциях сразу в return slot итогового объекта.
Для NRVO правда volatile для локальной переменной всё портит (gb).

Почему важно знать об этих штуках? Потому что NRVO можно сломать (gb):

S f() {
S s;
return std::move(s); // return S&&
}


Теперь компилятора обязан создать объект и мувнуть его, потому что это то, о чём вы его попросили явно. Так же можно думать, что у выражения в return и в сигнатуре теперь разные типы (S&& и S).

BY this->notes.


Share with your friend now:
tgoop.com/thisnotes/198

View MORE
Open in Telegram


Telegram News

Date: |

The main design elements of your Telegram channel include a name, bio (brief description), and avatar. Your bio should be: The creator of the channel becomes its administrator by default. If you need help managing your channel, you can add more administrators from your subscriber base. You can provide each admin with limited or full rights to manage the channel. For example, you can allow an administrator to publish and edit content while withholding the right to add new subscribers. Hui said the messages, which included urging the disruption of airport operations, were attempts to incite followers to make use of poisonous, corrosive or flammable substances to vandalize police vehicles, and also called on others to make weapons to harm police. As five out of seven counts were serious, Hui sentenced Ng to six years and six months in jail. How to Create a Private or Public Channel on Telegram?
from us


Telegram this->notes.
FROM American