Empty base optimization
Разрешает классу-предку иметь нулевой размер в байтовом представлении объекта.
struct empty {};
struct derived : empty {
int data;
};
static_assert(sizeof(empty) > 0);
static_assert(sizeof(derived) == sizeof(int));
В примере выше объект derived
будет иметь одинаковый адрес со своим предком.
Можно подумать, что для полей, состоящих пустого класса (как empty
) такая оптимизация тоже справедлива, но как бы не так.
struct derived {
empty e;
int data;
};
static_assert(sizeof(derived) == sizeof(int)); // compile error
Однако если класс действительно пустой, то его можно лишить своего адреса легально с помощью атрибута [[no_unique_address]]
. В случае, если класс пустым не был, этот атрибут будет проигнорирован.
struct derived {
[[no_unique_address]] empty e;
int data;
};
static_assert(sizeof(derived) == sizeof(int)); // OK!
Return value optimization
Нельзя опираться на то, что у возвращаемого по значению объекта будут вызваны конструкторы копирования, перемещения и/или деструктор, если он конструируется при вызове return
. Более того, они могут даже не понадобиться при компиляции.
mytype f() {
return mytype(1, 2, 3);
}
// компилится во что-то, похожее на:
void f(void* result) {
mytype_ctor(result, 1, 2, 3);
}
Если из функции возвращается prvalue
, то копия не создаётся и объект конструируется уже на месте - там, куда возвращается значение. Данная оптимизация записана в стандарте С++17 как Copy elision
и обязательна для компилятора.
Named return value optimization
Можно пойти дальше и ввести подобную оптимизацию для lvalue
. На данный момент она необязательна для компиляторов. Пример:
std::string f() {
std::string tmp;
for (;;) {
tmp += ...;
}
return tmp;
}
// можно разместить tmp уже на result-е, псевдокод:
void f(void* result) {
string_ctor(result);
for (;;) {
*result += ...;
}
}
Иногда NRVO
не может быть применено, когда мы не знаем, что должно возвращаться:
std::string g() {
std::string a("abc");
std::string b("def");
if (flag) {
return a;
} else {
return b;
}
}
Гарантированно нельзя использовать NRVO
в constexpr
-функциях и при инициализации глобальных, статических и thread-local переменных. Также NRVO
неприменимо с volatile
-объектами.
struct E {
constexpr E() = default;
E(const E&) = delete;
//E(E&&) = default; <-- uncomment to fix
};
constexpr E f() {
E e;
return e; // compile-error: E(const E&) deleted
}
E e = f();
Profile-guided optimization
В отличии от методов оптимизации, основанных исключительно на анализе исходного кода, позволяет использовать трассировку работы программы, собранной со специальным флагом, на основании которой компилятором принимается решение об оптимизации тех частей кода, которые вызывались чаще всего.
Крайне важно запускать программу с PGO на тех данных, с которыми ваша программа будет работать в реальном мире, иначе итоговая производительность может даже уменьшиться.
PGO может использовать следующие оптимизации:
- Inlining
- Virtual Call Suspection - условно-прямой (по условию) вызов определенной виртуальной функции в обход таблицы виртуальных функций
- Register Allocation - оптимизация распределения данных в регистрах
- Basic Block Optimization - помещать совместно вызываемые блоки кода в общую страницу памяти
И так далее, лучше глянуть официальную документацию.
После запуска программы со сбором статистики необходимо скомпилировать программу еще раз уже с учетом этой статистики.
Link-time optimization
LTO значит, что оптимизация будет происходить на этапе линковки.
Некоторые компиляторы (например, Clang) не переводят исходный код напрямую в ассемблер, а используют так называемый бэкенд, например, LLVM. Тогда при компиляции код сначала будет переводиться в байткод LLVM IR.
Компилятор LLVM будет оптимизировать полученный байткод во время встраивания кода функций в итоговый бинарник, и за счет снижения уровня абстракций у него это может получиться гораздо лучше, чем у компилятора на своем "верхнем" уровне (например, GCC).
Однако LTO в деле требует мощного компьютера при сборке проекта (в основном требуется больше оперативной памяти).
Из плюшек разделения процесса компиляции на фронтенд и бэкенд стоит отметить более лучшую переносимость кода. А в случае в LLVM, который используется уже очень много где, появляется еще одна фишка - LTO может работать при компиляции кода в единый бинарник, написанного на разных языках.
Заимствования:
RVO и NRVO в C++17 / Хабр (habr.com)
cpp-notes/14_move_rvalue.md at master · lejabque/cpp-notes (github.com)