Привет, Хабр! Меня зовут Алексей Салтыков, я инженер-программист в команде КОМПАС-3D. Решил поделиться соображениями насчет оптимизаций в С++ глазами обычного разработчика. Хочется сразу предупредить, что статья никого ни к чему не призывает, цель – наглядно показать, как незначительные трансформации кода могут помочь компилятору лучше оптимизировать код и насколько это вообще эффективно.
Дисклеймер:
-
Примеры кода нацелены на сохранение семантики. Подразумевается, что для внешнего пользователя способ работы с кодом не поменяется.
-
Некоторые примеры могут не содержать оптимизации, о которых говорилось ранее. Это сделано намеренно, чтобы подчеркнуть именно ту оптимизацию, о которой говорится здесь и сейчас.
-
Приведенные оптимизации не исправят проблемы производительности, связанные с неэффективной архитектурой или высокой асимптотической сложностью.
-
Эффект от приведенных практик может быть незначительным в рамках небольшого участка кода. Заметных улучшений можно добиться только большом в масштабе.
-
Будет много ассемблера (куда же без него?).
-
Для получения результатов использовался clang 18.1 с флагами -std=c++20 -O3
Нет информации — нет оптимизаций
Рассмотрим простой пример. Мы написали функцию, которая на вход принимает две точки. Находим среднюю точку и, если обе ее координаты не равны нулю, возвращаем ее, иначе – точку с координатами (-1, -1). Код будет выглядеть примерно так:
#include "Point.hpp" #include "GeometryAlgorithm.hpp" Point GetSomePoint(Point p1, Point p2) { Point p = Middle(p1, p2); return p.GetX() != 0 and p.GetY() != 0 ? p : Point{-1, -1}; }
Если вставить содержимое #include, то получим следующее:
class Point { private: double m_x; double m_y; public: Point(double x, double y); double GetX() const; double GetY() const; }; Point Middle(Point p1, Point p2); Point GetSomePoint(Point p1, Point p2) { Point p = Middle(p1, p2); return p.GetX() != 0 and p.GetY() != 0 ? p : Point{-1, -1}; }
Именно так компилятор и будет видеть наш файл, препроцессор при виде директивы #include фактически просто копирует содержимое указанного файла. После компиляции (и дизассемблирования) в объектном файле будет следующий код:
.LCPI0_0: .quad 0xbff0000000000000 .LCPI0_1: .quad 0x0000000000000000 GetSomePoint(Point, Point): sub rsp, 40 call Middle(Point, Point)@PLT movsd qword ptr [rsp], xmm0 movsd qword ptr [rsp + 8], xmm1 mov rdi, rsp call Point::GetX() const@PLT xorpd xmm1, xmm1 ucomisd xmm0, xmm1 jne .LBB0_1 jnp .LBB0_3 .LBB0_1: mov rdi, rsp call Point::GetY() const@PLT ucomisd xmm0, qword ptr [rip + .LCPI0_1] jne .LBB0_2 jnp .LBB0_3 .LBB0_2: movups xmm0, xmmword ptr [rsp] movaps xmmword ptr [rsp + 16], xmm0 movsd xmm0, qword ptr [rsp + 16] movsd xmm1, qword ptr [rsp + 24] add rsp, 40 ret .LBB0_3: lea rdi, [rsp + 16] movsd xmm0, qword ptr [rip + .LCPI0_0] movaps xmm1, xmm0 call Point::Point(double, double)@PLT movsd xmm0, qword ptr [rsp + 16] movsd xmm1, qword ptr [rsp + 24] add rsp, 40 ret
В ассемблерном коде можно явно найти выделение памяти на стеке для промежуточного результата (переменная p), вызовы функций Point::GetX(), Point::GetY(), Middle и конструктора.
Уже стоит обратить внимание на то, что передача аргументов нашей функции в функцию Middle произошла бесплатно. И правда, перекладывать ничего не нужно, они уже находятся там, где должны. Давайте ради интереса усложним задачу и поменяем их местами:
Point GetSomePoint(Point p1, Point p2) { Point p = Middle(p2, p1);
Тогда получим следующее:
.LCPI0_0: .quad 0xbff0000000000000 .LCPI0_1: .quad 0x0000000000000000 GetSomePoint(Point, Point): sub rsp, 40 movaps xmm4, xmm1 movaps xmm5, xmm0 movapd xmm0, xmm2 movapd xmm1, xmm3 movaps xmm2, xmm5 movaps xmm3, xmm4 call Middle(Point, Point)@PLT movsd qword ptr [rsp], xmm0 movsd qword ptr [rsp + 8], xmm1 mov rdi, rsp call Point::GetX() const@PLT xorpd xmm1, xmm1 ucomisd xmm0, xmm1 jne .LBB0_1 jnp .LBB0_3 .LBB0_1: mov rdi, rsp call Point::GetY() const@PLT ucomisd xmm0, qword ptr [rip + .LCPI0_1] jne .LBB0_2 jnp .LBB0_3 .LBB0_2: movups xmm0, xmmword ptr [rsp] movaps xmmword ptr [rsp + 16], xmm0 movsd xmm0, qword ptr [rsp + 16] movsd xmm1, qword ptr [rsp + 24] add rsp, 40 ret .LBB0_3: lea rdi, [rsp + 16] movsd xmm0, qword ptr [rip + .LCPI0_0] movaps xmm1, xmm0 call Point::Point(double, double)@PLT movsd xmm0, qword ptr [rsp + 16] movsd xmm1, qword ptr [rsp + 24] add rsp, 40 ret
Стало только хуже :D. Теперь можно и приступить к оптимизациям.
В реальном коде ситуация, когда аргументы функции сразу же передаются в другую функцию в неизменном виде, не так часто встречается. Поэтому в таком виде пример выглядит более правдоподобно.
Рассмотрим происходящее с точки зрения предметной области (пусть и скудной). Что делают функции GetX() и GetY() нам очевидно. То же касается и порядка аргументов функции Middle() – он не важен, так как результат останется такой же. Это очевидно нам, но не компилятору.
Довольно распространенная практика на языках С и С++ — разделять файлы на заголовочные и реализацию. В данном случае так и происходит. Реализация методов класса Point и функции Middle находятся в других файлах. В данной единице трансляции нет никакой информации о том, что эти функции делают. Это значительно ограничивает компилятор в оптимизациях.
Такой подход оправдан (я его не критикую), это как минимум полезно для скорости компиляции. Тем не менее, давайте попробуем дать компилятору информацию о тривиальных функциях. Для этого перенесем реализацию в заголовочный файл и добавим ключевое слово inline для соблюдения ODR — one definition rule. То же самое сделаем и для конструктора.
class Point { private: double m_x; double m_y; public: inline Point(double x, double y) : m_x(x) , m_y(y) {} inline double GetX() const { return m_x; } inline double GetY() const { return m_y; } };
Результат впечатляет:
.LCPI0_0: .quad 0xbff0000000000000 GetSomePoint(Point, Point): push rax movaps xmm4, xmm1 movapd xmm5, xmm0 movapd xmm0, xmm2 movapd xmm1, xmm3 movapd xmm2, xmm5 movaps xmm3, xmm4 call Middle(Point, Point)@PLT xorpd xmm2, xmm2 movapd xmm3, xmm1 cmpneqpd xmm3, xmm2 cmpneqpd xmm2, xmm0 andpd xmm2, xmm3 movd eax, xmm2 test al, 1 jne .LBB0_2 movsd xmm0, qword ptr [rip + .LCPI0_0] movapd xmm1, xmm0 .LBB0_2: pop rax ret
Не будем останавливаться и сделаем то же для функции `Middle`:
inline Point Middle(Point p1, Point p2) { double x = p1.GetX() / 2 + p2.GetX() / 2; double y = p1.GetY() / 2 + p2.GetY() / 2; return Point{x, y}; }
Итого получаем следующее:
.LCPI0_0: .quad 0x3fe0000000000000 .quad 0x3fe0000000000000 .LCPI0_1: .quad 0xbff0000000000000 .quad 0xbff0000000000000 GetSomePoint(Point, Point): unpcklpd xmm2, xmm3 movapd xmm3, xmmword ptr [rip + .LCPI0_0] mulpd xmm2, xmm3 unpcklpd xmm0, xmm1 mulpd xmm0, xmm3 addpd xmm0, xmm2 xorpd xmm1, xmm1 movapd xmm2, xmm0 cmpneqpd xmm2, xmm1 movapd xmm3, xmm0 unpckhpd xmm3, xmm0 cmpneqpd xmm3, xmm1 andpd xmm3, xmm2 pshufd xmm2, xmm3, 68 andpd xmm0, xmm2 andnpd xmm2, xmmword ptr [rip + .LCPI0_1] orpd xmm2, xmm0 pshufd xmm1, xmm2, 238 movapd xmm0, xmm2 ret
Результат стал не таким тривиальным, как был до этого. Но даже при беглом осмотре становится понятно, что работать это будет значительно быстрее. Главное, на что стоит обратить внимание, это прямое обращение к полям класса без вызова getter-ов.
Мы получили то, что хотели. Код теперь будет работать быстрее, при этом семантика полностью сохранена. Всю сложную работу сделал компилятор, а исходный вид нашей функции остался неизменным.
Point GetSomePoint(Point p1, Point p2) { Point p = Middle(p2, p1); return p.GetX() != 0 and p.GetY() != 0 ? p : Point{-1, -1}; }
Стоит указать на некоторые важные вещи:
-
В данном примере функции тривиальные, что позволяет получить улучшение производительности практически бесплатно. В реальности (к сожалению) это не всегда уместно, поскольку скорость компиляции пострадает многократно, а оптимизации будут незаметны.
-
Link time optimization не рассматриваем специально, речь в статье идет именно о преобразовании исходного кода.
-
Существует мнение, что inline расставлять не стоит, компилятор лучше знает, где его надо использовать. Это правда, но лишь отчасти. Компилятор и правда лучше знает, но фактически ключевое слово inline используется только для ODR. Большинство современных компиляторов его игнорируют (уже достаточно давно) и строят оптимизации по своим правилам.
virtual
Избитый всеми (и мной тоже) пример с фигурами. У нас есть фигура, и в очередной раз надо вычислять ее площадь. Будем смотреть только на квадрат.
class Figure { public: virtual ~Figure() = default; virtual double Area() const = 0; }; class Square : public Figure { private: double m_size; public: virtual double Area() const override { return m_size * m_size; } };
Напишем элементарную функцию:
double CalculateSomeArea(const Square* s) { return s != nullptr ? s->Area() * 2 : 0; }
Получаем следующее:
CalculateSomeArea(Square const*): test rdi, rdi je .LBB0_1 push rax mov rax, qword ptr [rdi] call qword ptr [rax + 16] addsd xmm0, xmm0 add rsp, 8 ret .LBB0_1: xorps xmm0, xmm0 ret
Стоит обратить внимание на:
call qword ptr [rax + 16]
Это вызов виртуальной функции. В данном случае нам не поможет inline, даже несмотря на то, что тип объекта мы знаем (аргумент функции — Square). Причина в том, что компилятор делает предположение, что может быть какой-то наследник класса Square, который эту функцию переопределяет.
Его нет и быть не может. Скажем об этом прямо:
class Square final : public Figure // ~~~~~
Если же есть, например, квадрат с именем, который не переопределяет метод `Area()`, то можно сделать замену `override -> final`:
virtual double Area() const final { return m_size * m_size; }
Оба способа дают одинаковый результат:
CalculateSomeArea(Square const*): test rdi, rdi je .LBB0_1 movsd xmm0, qword ptr [rdi + 8] mulsd xmm0, xmm0 addsd xmm0, xmm0 ret .LBB0_1: xorps xmm0, xmm0 ret
За счет того, что компилятор знает содержимое функции, а сама функция не переопределена, ее содержимое раскрывается по месту вызова. Благодаря этому мы избегаем ненужных операций.
Отсюда получается довольно интересный эффект:
class Figure { public: virtual ~Figure() = default; virtual double Area() const = 0; }; class Square : public Figure { private: double m_size; public: Square(double size) : m_size(size) { } virtual double Area() const override { return m_size * m_size; } }; void SetSomeNumber(double); inline double FigureArea(const Figure* f) { return f != nullptr ? f->Area() : 0; } void SomeCode(double value) { Square s(value); double num = FigureArea(&s); SetSomeNumber(num); }
Весь код сверху превращается в это:
SomeCode(double): mulsd xmm0, xmm0 jmp SetSomeNumber(double)@PLT
Красиво, не правда ли? Мы сохранили абстракции, но при этом получили максимально производительный код.
Может произойти что угодно
class Bar { private: int m_int; double m_double; public: Bar(); ~Bar(); int foo() const; }; int SomeCode() { Bar bar; return bar.foo(); }
Обычный код. Есть какой-то класс, у него есть какой-то метод. Мы его вызываем. Сразу скажу, inline тут не причем. Давайте посмотрим, что может пойти не так:
SomeCode(): push rbx sub rsp, 16 mov rbx, rsp mov rdi, rbx call Bar::Bar()@PLT mov rdi, rbx call Bar::foo() const@PLT mov ebx, eax mov rdi, rsp call Bar::~Bar()@PLT mov eax, ebx add rsp, 16 pop rbx ret mov rbx, rax mov rdi, rsp call Bar::~Bar()@PLT mov rdi, rbx call _Unwind_Resume@PLT DW.ref.__gxx_personality_v0: .quad __gxx_personality_v0
Все было бы хорошо, если бы не одна деталь — код после ret. В нашем коде мы исключениями не пользуемся. Глазами разработчика, если ничего не намекает на исключения, их скорее всего нет. Со стороны компилятора — наоборот. Не написали явно, значит, исключения могут быть. По понятным причинам, компилятор обязан встроить дополнительный код для их обработки. Добавим то, чего не хватает – noexcept.
class Bar { // ... Bar() noexcept; ~Bar(); int foo() const noexcept;
И все встает на свои места:
SomeCode(): push rbp push rbx sub rsp, 24 lea rbx, [rsp + 8] mov rdi, rbx call Bar::Bar()@PLT mov rdi, rbx call Bar::foo() const@PLT mov ebp, eax mov rdi, rbx call Bar::~Bar()@PLT mov eax, ebp add rsp, 24 pop rbx pop rbp ret
За счет этого мы не только избавились от ненужного кода, но и лучше оптимизировали тот, который нам нужен.
Говоря простыми словами, компилятор разделяет код на части. Если какая-то функция может бросить исключение, то код до нее и после разделяется на разные фрагменты, а оптимизации применяются к каждому фрагменту отдельно. В действительности все устроено значительно сложнее, но этого достаточно, чтобы передать общую идею — даже неиспользуемый код может сделать гадость.
Эксперименты
Все мы любим эксперименты. И чтобы не быть голословным, напишем код и проверим. Будем писать игру, а точнее ее малую часть. Есть игровое поле, представленное в виде ячеек. Ячеек бывает два вида — море и суша. Напишем:
// Position.hpp #pragma once #include <cstddef> #include <functional> class Position { public: Position(int x, int y); int GetX() const; int GetY() const; bool operator==(const Position&) const = default; bool operator!=(const Position&) const = default; private: int m_x; int m_y; }; namespace std { template <> struct hash<Position> { size_t operator()(const Position& pos) const { return (static_cast<size_t>(pos.GetX()) << 32) + static_cast<size_t>(pos.GetY()); } }; } // namespace std
// Cell.hpp #pragma once enum class CellType { Ground, Sea }; class Cell { public: Cell(CellType type); CellType GetType() const; private: CellType m_type; };
// Map.hpp #pragma once #include <memory> #include <vector> class Cell; class Position; class Map { public: Map(); std::shared_ptr<Cell> GetCellAt(const Position& pos) const; int GetWidth() const; int GetHeight() const; private: std::vector<std::shared_ptr<Cell>> m_cells; int m_width; int m_height; };
Что-то похожее на правду получилось. Ячейки специально разместил в виде указателей. Предполагаем, что это какой-то сложный объект, и по значению мы его хранить не можем. Реализацию методов выкладывать не вижу смысла – они тривиальные. Теперь напишем алгоритм. У нас на карте есть суша и море. Мы хотим написать следующую функцию:
Для определенной позиции вычисляем размер острова (количество ячеек земли), на котором расположена указанная позиция. Если позиция указывает на море, возвращаем 0.
В качестве основы для алгоритма используем что-то похожее на поиск в глубину. Чтобы избежать переполнения стека, рекурсию использовать не будем. Получается следующий код:
#include "cell.hpp" #include "map.hpp" #include "position.hpp" int GetGroundAreaAt(const Map& map, const Position& initialPos) { int result = 0; std::unordered_set<Position> visited; std::stack<Position> toVisit; toVisit.emplace(initialPos); while (not toVisit.empty()) { Position pos = toVisit.top(); toVisit.pop(); if (pos.GetX() < 0 or pos.GetY() < 0 or pos.GetX() >= map.GetWidth() or pos.GetY() >= map.GetHeight()) { continue; } auto insertRes = visited.insert(pos); if (not insertRes.second) { continue; } auto c = map.GetCellAt(pos); if (c->GetType() != CellType::Ground) { continue; } ++result; toVisit.emplace(pos.GetX() + 1, pos.GetY()); toVisit.emplace(pos.GetX() - 1, pos.GetY()); toVisit.emplace(pos.GetX(), pos.GetY() + 1); toVisit.emplace(pos.GetX(), pos.GetY() - 1); } return result; }
Проверять будем на карте размером 1000х1000. Заполняем случайно, с шансом 70% будет земля, так мы получим огромные острова (и долгое время выполнения 😀). При проверке будем вызывать функцию для 100 случайных позиций. Сид для генерации устанавливаем фиксированный – 0. В результате время выполнения составляет 10.3 секунды. Кстати, максимальная площадь острова составила 700183 клетки (материк с лужами, а не острова в море), что объясняет такое время выполнения.
Теперь переделаем все методы классов Cell, Position и Map, они будут inline. Работа очевидная и в примерах кода не нуждается. Результат порадовал, время выполнения стало 9.2 секунды.
Не так впечатляет, как снижение сложности алгоритма или выполнение в несколько потоков, но тоже приятно – 10% без особых усилий. Как и говорилось в начале, подход не дает большого преимущества, но изменения в коде тривиальные, мы не рискуем что-то сломать. Разумеется, представленный алгоритм далек от идеала, но наша цель состояла в том, чтобы ускорить его без изменений. Если изменить алгоритм, время выполнения, естественно, будет ниже, но преимущество от описанных оптимизаций никуда не денется.
Бонус — range vs for
В качестве бонуса хочется поговорить про новинку С++20 (новинке уже 4 года…) – ranges. Давайте посмотрим, как их видит компилятор. Для начала сравним обычные алгоритмы и их аналоги в namespace std::ranges
bool HasZero(const std::vector<int>& vi) { return std::any_of(vi.begin(), vi.end(), [](int x) { return x == 0; }); }
Тривиально, а в результате получаем это:
Скрытый текст
HasZero(std::vector<int, std::allocator<int>> const&): mov rdx, qword ptr [rdi] mov rax, qword ptr [rdi + 8] mov rdi, rax sub rdi, rdx mov rsi, rdi sar rsi, 4 test rsi, rsi jle .LBB0_8 and rdi, -16 mov rcx, rdi add rcx, rdx inc rsi add rdx, 8 .LBB0_2: cmp dword ptr [rdx - 8], 0 je .LBB0_17 cmp dword ptr [rdx - 4], 0 je .LBB0_18 cmp dword ptr [rdx], 0 je .LBB0_16 cmp dword ptr [rdx + 4], 0 je .LBB0_19 dec rsi add rdx, 16 cmp rsi, 1 jg .LBB0_2 mov rdi, rax sub rdi, rcx sar rdi, 2 cmp rdi, 1 jne .LBB0_9 jmp .LBB0_15 .LBB0_8: mov rcx, rdx sar rdi, 2 cmp rdi, 1 je .LBB0_15 .LBB0_9: cmp rdi, 2 je .LBB0_13 mov rdx, rax cmp rdi, 3 jne .LBB0_16 cmp dword ptr [rcx], 0 je .LBB0_21 add rcx, 4 .LBB0_13: cmp dword ptr [rcx], 0 je .LBB0_21 add rcx, 4 .LBB0_15: cmp dword ptr [rcx], 0 cmovne rcx, rax mov rdx, rcx .LBB0_16: cmp rdx, rax setne al ret .LBB0_17: add rdx, -8 cmp rdx, rax setne al ret .LBB0_18: add rdx, -4 cmp rdx, rax setne al ret .LBB0_21: mov rdx, rcx cmp rdx, rax setne al ret .LBB0_19: add rdx, 4 cmp rdx, rax setne al ret
Выглядит не очень, попробуем аналог:
bool HasZero(const std::vector<int>& vi) { return std::ranges::any_of(vi, [](int x) { return x == 0; }); }
HasZero(std::vector<int, std::allocator<int>> const&): mov rdx, qword ptr [rdi] mov rcx, qword ptr [rdi + 8] cmp rdx, rcx je .LBB0_1 add rdx, 4 .LBB0_4: cmp dword ptr [rdx - 4], 0 sete al je .LBB0_2 lea rsi, [rdx + 4] cmp rdx, rcx mov rdx, rsi jne .LBB0_4 .LBB0_2: ret .LBB0_1: xor eax, eax ret
Очевидно, где лучше. Посмотрим кое-что интересней:
int OddSum(const std::vector<int>& vi) { int res = 0; auto&& range = vi | std::views::filter([](int x) { return x % 2 == 0; }); for (int i : range) { res += i; } return res; }
Стоит сразу оговориться — accumulate я специально не использовал, важно показать влияние от применения фильтрации.
Посмотрим, что получилось:
OddSum(std::vector<int, std::allocator<int>> const&): mov rcx, qword ptr [rdi] mov rdx, qword ptr [rdi + 8] cmp rcx, rdx je .LBB0_4 .LBB0_1: test byte ptr [rcx], 1 je .LBB0_4 add rcx, 4 cmp rcx, rdx jne .LBB0_1 xor eax, eax ret .LBB0_4: xor eax, eax .LBB0_5: cmp rcx, rdx je .LBB0_9 add eax, dword ptr [rcx] .LBB0_8: add rcx, 4 cmp rcx, rdx je .LBB0_9 test byte ptr [rcx], 1 jne .LBB0_8 jmp .LBB0_5 .LBB0_9: ret
Прекрасный код. Ничего лишнего, именно то, что я и написал. «Доделали!» – сказал я себе, радуясь, что новинка не только удобная, но и работать будет быстро.
«Ага, ЩАС!». Поверили? В статье про оптимизации в коде нет хитростей? Давайте проверим старый добрый цикл for, его точно доделали:
int OddSum(const std::vector<int>& vi) { int res = 0; for (int i : vi) { if (i % 2 == 0) { res += i; } } return res; }
Скрытый текст
.LCPI0_0: .long 1 .long 1 .long 1 .long 1 OddSum(std::vector<int, std::allocator<int>> const&): mov r8, qword ptr [rdi] mov rcx, qword ptr [rdi + 8] cmp r8, rcx je .LBB0_1 mov rdi, rcx sub rdi, r8 add rdi, -4 xor edx, edx cmp rdi, 28 jae .LBB0_5 xor eax, eax mov rsi, r8 jmp .LBB0_8 .LBB0_1: xor eax, eax ret .LBB0_5: shr rdi, 2 inc rdi mov r9, rdi and r9, -8 lea rsi, [r8 + 4*r9] pxor xmm0, xmm0 xor eax, eax movdqa xmm3, xmmword ptr [rip + .LCPI0_0] pxor xmm2, xmm2 pxor xmm1, xmm1 .LBB0_6: movdqu xmm4, xmmword ptr [r8 + 4*rax] movdqu xmm5, xmmword ptr [r8 + 4*rax + 16] movdqa xmm6, xmm4 pand xmm6, xmm3 movdqa xmm7, xmm5 pand xmm7, xmm3 pcmpeqd xmm6, xmm0 pand xmm6, xmm4 paddd xmm2, xmm6 pcmpeqd xmm7, xmm0 pand xmm7, xmm5 paddd xmm1, xmm7 add rax, 8 cmp r9, rax jne .LBB0_6 paddd xmm1, xmm2 pshufd xmm0, xmm1, 238 paddd xmm0, xmm1 pshufd xmm1, xmm0, 85 paddd xmm1, xmm0 movd eax, xmm1 cmp rdi, r9 je .LBB0_2 .LBB0_8: mov edi, dword ptr [rsi] test dil, 1 cmovne edi, edx add eax, edi add rsi, 4 cmp rsi, rcx jne .LBB0_8 .LBB0_2: ret
Выглядит страшно. Но этот код работает примерно в 12 раз быстрее за счет векторизации. Да, элементарный код будет лучше, мнимая красота того не стоит. Однако в защиту стоит сказать, где применение ranges реально может ускорить быстродействие:
inline auto Document::GetFiguresInRect(const Rect& rect) const { return std::views::all(m_figures) | std::views::filter([&rect](const Figure* fig) { return fig->CollidesRect(rect); }) | std::views::transform([](auto&& i) -> const Figure* { return i; }); }
Документ содержит фигуры. Нам нужен список фигур, которые попадают в прямоугольник. Такой подход как минимум позволит избежать создания нового списка, то есть выделения памяти, что очень удобно.
Примечание: из-за своей специфики подобный подход опасен висячими ссылками, применять с осторожностью. Берегите свои ноги!
Итоги
-
Для простых функций применение inline может быть оправдано, например:
-
Тривиальные get/set
-
Простые функции
-
Не сложные вычисления
-
-
Для оптимизации работы виртуальных функций inline не всегда достаточно. Тем не менее, поставить у класса-наследника final может помочь компилятору лучше оптимизировать.
-
Если необходимо применить функцию STL на весь контейнер, есть смысл выбрать версию из namespace std::ranges.
-
Использование std::views вместо цикла for может быть оправдано только в тех случаях, когда мы избегаем создания нового контейнера. В остальных случаях обычный цикл будет как минимум не хуже.
-
Исключения достаточно хорошо оптимизированы, но не бесплатны. Если их нет и не будет — не лишним будет добавить noexcept.
ссылка на оригинал статьи https://habr.com/ru/articles/850406/
Добавить комментарий