Грокаем C++
9.36K subscribers
45 photos
1 video
3 files
561 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Spaceship оператор. Детали 1
#новичкам

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

Ну для начала: наличие определенного spaceship оператора гарантирует вам наличие всех 6 операций сравнения:

struct Time {
int hours;
int minutes;
int seconds;

// Spaceship operator (генерирует все 6 операторов сравнения)
auto operator<=>(const Time& other) const = default;
};

Time t1{10, 30, 15}; // 10:30:15
Time t2{9, 45, 30}; // 09:45:30
Time t3{10, 30, 15}; // 10:30:15

assert(t1 > t2); // 10:30:15 > 09:45:30
assert(!(t1 < t2)); // 10:30:15 не < 09:45:30
assert(t1 == t3); // 10:30:15 == 10:30:15
assert(t1 != t2); // 10:30:15 != 09:45:30
assert(t1 <= t3); // 10:30:15 <= 10:30:15
assert(t1 >= t2); // 10:30:15 >= 09:45:30


Это уже прекрасно, но это еще не все!

Обратите внимание на сигнатуру spaceship operator. Зачем там нужен auto?

Вот теперь объясненяем, почему это называется оператор трехстороннего сравнения.

Он возвращает объект, который содержит информацию о результате сравнения:

Time t1{10, 30, 15};  // 10:30:15
Time t2{9, 45, 30}; // 09:45:30

// Можно использовать и сам spaceship operator напрямую
auto cmp = t1 <=> t2;
if (cmp > 0) {
std::cout << "t1 is later than t2\n";
} else if (cmp < 0) {
std::cout << "t1 is earlier than t2\n";
} else {
std::cout << "t1 is the same as t2\n";
}
// OUTPUT:
// t1 is later than t2


Если результат сравнения >0, то первый операнд больше второго. И так далее по аналогии.

Тип возвращаемого значения у оператора один из этих трех:
- std::strong_ordering
- std::weak_ordering
- std::partial_ordering

Что они значат - тема отдельного разговора, но каждый из них может находится в одном из 3-х состояний: less, greater, equal. Это можно использовать, например, для проверки возвращаемых значений системных вызовов:

constexpr int strong_ordering_to_int(const std::strong_ordering& o)
{
if (o == std::strong_ordering::less) return -1;
if (o == std::strong_ordering::greater) return 1;
return 0;
}

char buffer[256];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));

// Сравниваем результат read() с нулём через <=>
switch (strong_ordering_to_int(bytes_read <=> 0)) {
case 1:
std::cout << "Read " << bytes_read << " bytes: "
<< std::string(buffer, bytes_read) << "\n";
break;
case 0:
std::cout << "End of file reached (0 bytes read)\n";
break;
case -1:
perror("read failed");
return 1;
}


Кейсы применения непосредственно spaceship'а в коде не так обширны, потому что не очень привычно, есть вопросы к перфу(об этом в следующем посте) да и поди разберись с этими ордерингами еще. Но его точно стоит использовать для автоматической генерации 6 базовых операторов.

Be universal. Stay cool.

#cppcore #cpp20
30👍15🔥101
Spaceship оператор. Детали 2
#опытным

Продолжаем углубляться в космический оператор.

В прошлом посте мы рассказали о том, что определение одного единственного spaceship оператора возможность использовать все 6 операторов сравнения.

Это правда, но с оговорочками. На самом деле компилятор подменяет привычные операторы на использование spaceship оператора вот так:

SomeType a;
SomeType b;
a == b; // компилятор распознает как (a <=> b) == 0
a != b; // компилятор распознает как (a <=> b) != 0
a < b; // компилятор распознает как (a <=> b) < 0
// и тд, суть вы поняли


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

Но не всегда трехсторонний оператор предоставляет все 6 операторов.

Вгляните на пример:

struct String {
std::string str;
auto operator<=>(const String &other) const {
std::cout << "Using <=>\n";
return str <=> other.str;
}
};

String a{"foobar"};
String b{"foo"};
a == b; // Ошибка компиляции
a != b; // Ошибка компиляции
a < b; // OK
a > b; // OK
a <= b; // OK
a >= b; // OK


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

Однако казус: операторы проверки на равенство не определены, тогда как операторы определения порядка элементов нормально работают.

Почему так?

Дело в производительности.

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

template<typename T>
strong_ordering operator<=>(vector<T> const& lhs,
vector<T> const& rhs)
{
size_t min_size = min(lhs.size(), rhs.size());
for (size_t i = 0; i != min_size; ++i) {
if (auto const cmp = lhs[i] <=> rhs[i]; cmp != 0) {
return cmp;
}
}
return lhs.size() <=> rhs.size();
}


Этот код работает и проходит все тесты. Однако для определения равенства этот код очень плохо перфомит. Вектора могут иметь очень длинный общий префикс, но по итогу иметь разные размеры. Логично сразу же проверить размеры векторов и, если они не равны, сразу принять решение. В рамках данного spaceship оператора это сделать невозможно(только по размерам нельзя определить отношение порядка), но стандарт нам не мешает явно определять нужные операторы:

template<typename T>
bool operator==(vector<T> const& lhs, vector<T> const& rhs)
{
// short-circuit on size early
const size_t size = lhs.size();
if (size != rhs.size()) {
return false;
}

for (size_t i = 0; i != size; ++i) {
// use ==, not <=>, in all nested comparisons
if (!(lhs[i] == rhs[i])) {
return false;
}
}

return true;
}


И это будет работать для ==. Но не будет работать для !=. Вы же помните, во что он раскрывается? В (a <=> b) != 0. Поэтому надо явно предоставлять и этот оператор. Но компилятор за нас здесь поработает и сам сгенерирует operator!=, как отрицание ==.

Получается, что если мы сами определили оператор <=>, то нас не удовлетворило базовое лексикографическое сравнение. Поэтому скорее всего нас не удовлетворит и использование такого spaceship'а для проверки на равенство. Поэтому нужно писать его отдельно. И это решает проблему с производительностью.

Хорошие новости в том, что при оборачивании такого класса в другой класс, сравнения обертки будут использовать оптимизированное сравнение, а не spaceship реализацию:
👍108🔥3
struct String {
std::string str;
bool operator==(const String &other) const {
std::cout << "Using optimized ==\n";
if (str.size() != other.str.size())
return false;
return str == other.str;
}
auto operator<=>(const String &other) const {
std::cout << "Using <=>\n";
return str <=> other.str; // Сравнивает символы до первого различия
}

};

struct SString {
String str;
auto operator<=>(const SString &other) const = default;
};

a == b; // Вызовет operator==
a != b; // Вызовет operator!=, который определен через ==
a < b; // Вызовет spaceship
// OUTPUT:
// Using optimized ==
// Using optimized ==
// Using <=>


В общем, оказалось, что здесь такая собака зарыта... Если используйте дефолтовый космический оператор для простых структур, то скорее всего все будет ок. Но если хотите определять свой spaceship, то придется глубоко погружаться в кроличью нору.

Don't go to a rabbit hole. Stay cool.

#cppcore
22👍7🔥5
std::less vs std::ranges::less
#опытным

В догонку к предыдущим постам. Вот у вас есть структурка TIme из С++17:

struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time &other) const {
return std::tie(hours, minutes, seconds) <
std::tie(other.hours, other.minutes, other.seconds);
}
};


Вы как-то заполняете ее и сортируете:

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::sort(times.begin(), times.end());


И все работает прекрасно.

И тут решили вы перейти на С++20 и заменить все алгоритмы классической stl на алгоритмы ренджей. Синтаксис у них понятнее, да и почему бы и нет. Изменения тут будут тривиальные:

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times);


Запускаете сборку, а она падает с какими-то нечитаемыми ошибки, типа таких:

/usr/include/c++/15/concepts:362:13:   required for the satisfaction of 'invocable<_Fn, _Args ...>' [with _Fn = std::ranges::less&; _Args = {Time&, Time&}]
/usr/include/c++/15/concepts:366:13: required for the satisfaction of 'regular_invocable<_Fn, _Args ...>' [with _Fn = std::ranges::less&; _Args = {Time&, Time&}]
/usr/include/c++/15/concepts:370:13: required for the satisfaction of 'predicate<_Rel, _Tp, _Tp>' [with _Rel = std::ranges::less&; _Tp = Time&]
/usr/include/c++/15/concepts:375:13: required for the satisfaction of 'relation<_Rel, _Tp, _Up>' [with _Rel = std::ranges::less&; _Tp = Time&; _Up = Time&]
/usr/include/c++/15/concepts:385:13: required for the satisfaction of 'strict_weak_order<_Fn&, typename std::__detail::__indirect_value<_Iter>::type, typename std::__detail::__indirect_value<_I2>::type>' [with _Fn = std::ranges::less; _I1 = __gnu_cxx::__normal_iterator<Time*, std::vector<Time, std::allocator<Time> > >; _I2 = __gnu_cxx::__normal_iterator<Time*, std::vector<Time, std::allocator<Time> > >]


Нам говорили, что с концептами ошибки станут понятнее, но это только в теории. А на практике вот это.

В чем в итоге проблема?


По умолчанию в сортировке рэнджей используется компаратор std::ranges::less, а в обычной сортировке - std::less. И в их разнице и зарыта собака: std::ranges::less требует определения всех шести операторов сравнения или одного spaceship'а и все заработает:

struct Time {
int hours;
int minutes;
int seconds;

// Spaceship operator (генерирует все 6 операторов сравнения)
auto operator<=>(const Time& other) const = default;
};

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times);


Ну или можете пометить оператор< дефотным и передавать в сортировку std::less:
struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) const = default; // Here
};

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times, std::less{}); // And here


Пару раз натыкался на эту проблему, поэтому решил поделиться болью.

Give clear feedback. Stay cool.
44👍25🔥18🤯1
Unity build
#опытным

Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.

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

Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали std::vector<int> - компилируем эту инстанциацию. В другом написали std::vector<int> - заново скомпилировали эту инстанциацию. То есть большая проблема в компиляции одного и того же кучу раз.

Но помимо компиляции вообще-то есть линковка. И чем больше единиц трансляции, библиотек и все прочего, тем больше времени нужно линковщику на соединение все этого добра в одно целое.

Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)

Условно. Есть у вас 2 цппшника и один хэдэр:

// header.hpp
#pragma once
void foo();

// source1.cpp
#include "header.hpp"
void foo() {
std::cout << "You are the best!" << std::endl;
}

// source2.cpp
#include "header.hpp"
int main() {
foo();
}


Вы все цппшники подключаете в один файл unity_build.cpp:

#include "source1.cpp"
#include "source2.cpp"


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

Или нет?

У этой техники есть ряд недостатков:

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

Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.

Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.

У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.

Solve the problem. Stay cool.

#cppcore #compiler #tools
122🔥7👍6😁2🗿1
​​Короткий совет по отладке кода от мэтра
#новичкам

Отладка занимает около 30% всего времени разработки. К тому же сам язык у нас способствует появлению самых неожиданных проблем. Сидишь целый день и не вдупляешь, почему тесты падают.

Невозможно дать конкретный алгоритм по пунктам, что нужно делать. Однако общие советы могут помочь направить внимание в нужную точку. Вот, что об отладке говорит сам Страуструп:

"I get maybe two dozen requests for help with some sort of programming or design problem every day. Most have more sense than to send me hundreds of lines of code. If they do, I ask them to find the smallest example that exhibits the problem and send me that. Mostly, they then find the error themselves. 'Finding the smallest program that demonstrates the error' is a powerful debugging tool."

Каждый день я получаю около пары дюжин запросов с помощью решить какую-то программную или архитектурную проблему. Большинство из них достаточно благоразумны, чтобы не присылать мне сотни строк кода. Если они всё же присылают, я прошу их найти самый маленький пример, демонстрирующий проблему, и прислать его мне. В большинстве случаев они сами находят ошибку. «Найти самую маленькую программу, демонстрирующую ошибку» — мощный инструмент отладки.

Почему это работает?

Отбрасывая все ненужное, вы сужаете пространство для анализа возможного проблемного места. Меньше кода для анализа - больше вероятность понять место проблемы.

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

Если вы так и не поняли, в чем проблема, с маленьким примером вам намного охотнее и эффективнее смогут помочь коллеги.

Как локализовать проблему?

🔍 Мокайте зависимости от других компонентов и модулей.

🔍 Фиксируйте значения переменных.

🔍 Упрощайте входные данные.


Это практика полезна и с точки зрения архитектуры. Если вам много всего нужно менять для составления маленького примера, возможно у вас высокая связность и стоит порефакторить код после успешной отладки.

А какие вы дадите полезные советы по отладке кода?

Fix problems playfully. Stay cool.

#goodpractice
1🔥197👍6❤‍🔥3🤣3
​​Какой день будет через месяц?
#новичкам

Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. chrono появилась так-то в С++11. Но и даже с ее появлением жить стало лишь немногим легче.

Например, простая задача: "Прибавить к текущей дате 1 месяц".

В С++11 у нас есть только часы и точки на временной линии. Тут просто дату-то получить сложно. Есть конечно сишная std::localtime, можно мапулировать отдельными полями std::tm(днями, минутами и тд), но придется конвертировать сишные структуры времени в плюсовые, да и можно нарваться на трудноотловимые ошибки, если попытаться увеличить на 1 месяц 30 января.

Как прибавить к дате месяц? +30 дней или +1 месяц не канает. А если февраль? А если високосный год?

В общем стандартного решения нет... Или есть?

В С++20 в библиотеку chrono завезли кучу полезностей. А в частности функционал календаря. Теперь мы можем манипулировать отдельно датами и безопасно их изменять.

Например, чтобы получить сегодняшнюю дату и красиво ее вывести на консоль достаточно сделать следующее:

std::chrono::year_month_day current_date =
std::chrono::floor<std::chrono::days>(
std::chrono::system_clock::now());
std::cout << "Today is: " << current_date << '\n';
// Today is: 2025-09-10


Появился прекрасный класс std::chrono::year_month_day, который отражает конкретно дату. И его объекты замечательно сериализуются в поток.

Если вам нужно задать определенный формат отображения - не проблема! Есть std::format:

std::cout << "Custom: "
<< std::format("{:%d.%m.%Y}", current_date)
<< '\n';
// Custom: 10.09.2025


С помощью std::chrono::year_month_day можно удобно манипулировать датами и, главное, делать это безопасно. Что будет если я к 29 января прибавлю месяц?

auto date = std::chrono::year_month_day{
std::chrono::year(2004), std::chrono::month(1), std::chrono::day(29)};
std::cout << "Date: " << date << "\n";

std::chrono::year_month_day next_month = date + std::chrono::months{1};
std::chrono::year_month_day next_year_plus_month =
date + std::chrono::years{1} + std::chrono::months{1};

std::cout << "Next month: " << next_month << "\n";
std::cout << "Next year plus month: " << next_year_plus_month << "\n";

// OUTPUT:
// Date: 2004-01-29
// Next month: 2004-02-29
// Next year plus month: 2005-02-29 is not a valid date


А будет все хорошо, если год високосный. Но если нет, то библиотека нам явно об этом скажет.

В общем, крутой фиче-сет, сильно облегчает работу со стандартными временными точками.

Take your time. Stay cool.

#cpp11 #cpp20
22👍17🔥6😁3
​​Сколько времени сейчас в Москве?
#новичкам

Как в стандартных плюсах работать с временными зонами? Да никак до С++20. Приходилось использовать разные сторонние решения.

Но стандарт развивается и у нас теперь есть возможность работать с зонами в чистом С++!

Появился класс std::chrono::zoned_time, который представляет собой пару из временной метки и временной зоны. Создать зонированное время можно так:

auto now = std::chrono::zoned_time{std::chrono::current_zone(), std::chrono::system_clock::now()};


Функция std::chrono::current_zone() позволяет получить локальную временную зону.

Можно также передать имя зоны:

auto msw_time = std::chrono::zoned_time{"Europe/Moscow", std::chrono::system_clock::now()};


И это все прекрасно работает с std::format, который позволяет информацию о временной точки настолько подробно, насколько это возможно:

std::string get_time_string(const std::chrono::zoned_time<std::chrono::system_clock::duration>& zt) {
return std::format("{:%Y-%m-%d %H:%M:%S %Z}", zt);
}

std::string get_detailed_time_string(const std::chrono::zoned_time<std::chrono::system_clock::duration>& zt) {
return std::format("{:%A, %d %B %Y, %H:%M:%S %Z (UTC%z)}", zt);
}

std::cout << "Current time: " << get_time_string(now) << std::endl;
std::cout << "Detailed: " << get_detailed_time_string(now) << std::endl;

std::cout << "Time in Moscow: " << get_time_string(msw_time) << std::endl;
std::cout << "Detailed: " << get_detailed_time_string(msw_time) << std::endl;

// OUTPUT:
// Current time: 2025-09-11 17:50:48.035852842 UTC
// Detailed: Thursday, 11 September 2025, 17:50:48.035852842 UTC (UTC+0000)
// Time in Moscow: 2025-09-11 20:50:48.041000112 MSK
// Detailed: Thursday, 11 September 2025, 20:50:48.041000112 MSK (UTC+0300)


Работы с временными зонами очень не хватало в стандарте и круто, что ее добавили.

Develop yourself. Stay cool.

#cpp20
2🔥36👍128😁7
Внутрянка бэкэнда

Бэкенд интересен тем, что ему ставятся амбициозные задачи из самых разных доменных областей. И с каждым годом планка все повышается. Все больше сфер жизни оцифровываются, rps растет, а ML хотят все да побольше. Это прям огромная кроличья нора и глубины ее не видно. Однако эти челенджи успешно решаются российскими IT-компаниями.

И 4 октября в Москве эксперты нашего бигтеха будут раскрывать про внутрянку своих продуктов на конференции «Я про бэкенд»! В докладах будет,много про бэкенд челледжи, связанных с МЛ, рекомендательными и генеративными технологиями.

Держите список докладов:

-Антон Полднев (Яндекс Реклама): как рекомендательный движок ежегодно экономит 200 тыс. CPU в инфраструктуре Рекламы

-Дмитрий Погорелов (VK): эволюция рекомендательного движка и перезапуск рекомендаций ВКонтакте

-Михаил Чебаков (T-Банк): как прятать сложность LLM-инференса за понятными числами

-Андрей Шукшов (Яндекс R&D): внутри LLM: как выжать максимум из decoder attention на GPU

-Алёна Васильева (Шедеврум): про архитектуру для ML-моделей и длинный инференс

-Никита Сикалов (Яндекс Поиск): про эволюцию технологий реалтайм-индексации

И еще куча интересностей, которые вы может найти в программе мероприятия по ссылочке.

Регистрируйтесь и прокачивайте свои бэкэндерские мускулы
5👍5🔥3🤨1🗿1
​​Распределенные компиляторы
#опытным

Как только ваш проект достигает определенного размера, время компиляции начинает становиться проблемой. В моей скромной практике были проекты, которые полностью собирались с 1-2 часа. Но это далеко не предел. Пишите кстати в комментах ваши рекордные тайминги сборки проектов.

С этим жить, конечно, очень затруднительно. Даже инкрементальная компиляция может занимать десятки минут. Как разрабатывать, когда большая часть времени уходит на билд? Кто-то безусловно будет радоваться жизни и попивать кофеек, если не пивко, пока билд билдится. Но компании это не выгодно, поэтому кто-то должен озаботится этой проблемой. То есть вам необходимо найти эффективные способы сократить это время, чтобы свести к минимуму периодические задержки и максимизировать продуктивность.

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

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

Так как обычно девелоперские задачи в среднем потребляют мало ресурсов(не так много нужно, чтобы писать буквы в редакторе), этими удаленными CPU могут быть даже машины ваших коллег!

Наиболее известный представитель систем распределенной компиляции с открытым исходным кодом — это distcc. Он состоит из демона-сервера, принимающего задания на сборку по сети, и обёртки (wrapper) для компилятора, которая распределяет задания по доступным узлам сборки в сети.

Вот примерная схема его работы(у кого-то может некорректно отображаться, ничего поделать не можем):

┌─────────────────┐    ┌─────────────────────────────────┐
│ Клиентская │ │ Ферма компиляции │
│ машина │ │ │
│ │ │ ┌───────┐ ┌───────┐ ┌───────┐│
│ ┌─────────────┐│ │ │Worker │ │Worker │ │Worker ││
│ │ Координатор │◄──────►│ 1 │ │ 2 │ │ N ││
│ └─────────────┘│ │ └───────┘ └───────┘ └───────┘│
│ │ │ │
│ ┌─────────────┐│ │ ┌─────────────────────────────┐│
│ │ Кэш .o ││ │ │ Distributed Cache ││
│ │ файлов │◄──────►│ ││
│ └─────────────┘│ │ └─────────────────────────────┘│
└─────────────────┘ └─────────────────────────────────┘

- Координатор анализирует зависимости между файлами и распределяет задачи компиляции

- Компиляционные ноды выполняют фактическую компиляцию, их набор конфигурируется на клиенте

- Распределенный кэш хранит скомпилированные объектные файлы, кэширует результаты компиляции, тем самым ускоряя повторные сборки

Этапы работы примерно такие:

1️⃣ Все цппшники проекта проходят этап препроцессинга на локальной машине и уже в виде единиц трансляции перенаправляются на ноды компиляции.

2️⃣ Ноды компиляции преобразуют единицы трансляции в объектные файлы и пересылают их на клиентскую машину.

3️⃣ Последним этапом идет бутылочное горлышко всей системы - линковка. Для нее необходим доступ ко многим объектым файлам одновременно и эта задача слабо параллелится, поэтому и выполняется на клиентской машине.

Таким образом вы можете уменьшить время сборки проекта в разы и ускорить разработку в целом. Вот ссылочка на доку для заинтересовавшихся.

Speed up processes. Stay cool.

#compiler #tools
1👍249🔥8
​​ccache
#опытным

Еще один полезный и простой во внедрении инструмент для ускорения компиляции - ccache.

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

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

Ну а сам кеш — это обычная директория на вашем диске, где хранятся объектники и всякая метаинформация. То есть он глобальных для всех проектов на одной машине.

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

👉🏿 идентификатор компилятора
👉🏿 использованные флаги компилятора
👉🏿 содержимое входного исходного файла,
👉🏿 содержимое подключаемых заголовочных файлов (и их транзитивное замыкание).

Кэшу не надо беспокоиться за криптостойкость, поэтому в нем спокойно используется небезопасная, но быстрая функция c хорошим распределением.

После вычисления значения тега ccache проверяет, существует ли запись с таким тегом в кеше. Если да, перекомпиляция не нужна. Что удобно, ccache запоминает не только сам артефакт, но и вывод компилятора в консоль, который был сгенерирован при его создании — поэтому, если вы извлекаете закешированный файл, который ранее вызывал предупреждения компилятора, ccache снова выведет эти предупреждения.

Если распределенный компилятор distcc каждый раз выполняет препроцессинг, то для ccache это не обязательно. В одном из режимов работы ccache вычисляет хэши MD4 для каждого включаемого заголовочного файла отдельно и сохраняет результаты в так называемом манифесте. Поиск в кеше выполняется путём сравнения хэшей исходного файла и всех его включений с содержимым манифеста; если все хэши попарно совпадают, мы имеем попадание. В текущих версиях ccache прямой режим включён по умолчанию.

Для того, чтобы начать пользоваться ccache, достаточно его установить, добавить в PATH и в cmake'е прописать CMAKE_CXX_COMPILER_LAUNCHER=ccache. Это можно сделать и через команду запуска, и через установку переменной окружения. Вот вам ссыль.

Но это было введение в ccache для незнающих. Опытный же подписчик спросит: а зачем нужен этот кэш, если cmake и собирает только то, что мы недавно изменили? Об этом ключевом вопросе мы и поговорим в следующем посте.

Compile fast. Stay cool.

#compiler #tools
1👍2112🔥9🤯1
​​ccache vs cmake
#опытным

И давайте раскроем очевидный вопрос: чем кэширование ccache отличается от кэширования самого cmake'а? Ведь при искрементальной сборке cmake пересобирает только те файлы, которые поменялись.

Основное отличие: cmake - это система сборки, а ccache - это четко кэш. cmake не может себе позволить анализировать контент всех файлов, его основная задача - билдить проект. Поэтому ему нужно очень быстро понять, изменился файл или нет. И принимает он решение на основе времени модификации файла. А ccache не ограничен такими рамками. Он учитывает контент препроцесснутого файла и контекст компиляции.

Проще понять разницу на примерах:

🔍 Вы скомпилировали проект и случайно или специально(бывает нужно, если cmake троит) удалили папку с билдом. Без ccache нужно перекомпилировать все, а с ним - ничего, только линковку сделать.

🔍 Вы плотно работаете с несколькими ощутимо отличающимися бранчами, собираете, коммитите и переключаетесь. Без ccache нужно будет перекомпилировать все измененные при переключении бранчей файлы. С ccache - только те, которые вы сами изменили после последней сборки.

🔍 Если вы правите только комменты в файле, то голый cmake пойдет перекомпилировать его. ccache - нет, потому что работает с препроцесснутым файлом.

🔍 Вы активно переключаетесь между конфигурациями при сборке. Например между релизом и дебагом. cmake будет полностью пересобирать проект при изменении типа билда. А ccahce после одной сборки на каждую конфигурацию все запомнит и вы будете компилировать только последние изменения.

Not much, но каждый из нас частенько сталкивается с одним из этих пунктов. Поэтому ставьте ccache. Это сделать просто, но импакт дает ощутимый в определенных кейсах.

Compile fast. Stay cool.

#compiler #tools
133👍14🔥8
​​Быстрые линкеры
#опытным

По предыдущим постам стало уже понятно, что линковка - бутылочное горлышко всей сборки. Если меняется хоть одна единица трансляции - перелинковываться бинарник будет полностью вне зависимости от количества TU в ней. Хотелось бы ускорить движение по этому горлышку.

GCC как самый широкоиспользуемый компилятор использует ld в качестве линкера. ld считается очень громоздким, раздутым и от этого медленным. Можно решить проблему гениально и просто использовать быстрый линкер!

С ld можно бесшовно перейти на другой совместимый компоновщик с помощью опции -fuse-ld. То есть буквально:

g++ -fuse-ld=<my_linker> main.cpp -o program


И ваша программа будет собираться с помощью my_linker. Ну или в cmake:

# Установка линкера для всего проекта
set(CMAKE_LINKER my_linker)

# Или для конкретной цели
target_link_options(my_target PRIVATE "LINKER:my_linker")


Какие альтернативные компоновщики существуют?


GNU Gold. Еще один официальный линковщик из пакета GNU. Создавался как более быстрая альтернатива ld для линковки ELF файлов. Он действительно быстрее ld, но теперь его уже никто не поддерживает и недавно в binutils задепрекейтили его.

lld (LLVM Linker). Линковщик от проекта llvm. Активно развивается и имеет интерфейсную совместимость с дефолтовым ld, как и clang имеет в gcc. Быстрее Gold.

mold. Или modern linker. В несколько раз быстрее lld и является самым быстрым drop-in опенсорсным линковщиком. Он использует более оптимизированные структуры данных и каким-то образом линкует в параллель! Благодаря этому достигается фантастическая скорость работы.

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

В любом случае стоит попробовать и, возможно, вы в несколько раз сможете сократить время линковки.

Be faster. Stay cool.

#tools #compiler
3🔥3715👍10❤‍🔥42
Откуда такая скорость у mold?
#опытным

На графиках с предыдущего поста видно, что mold работает чуть ли не на порядок быстрее, чем ld или gold. За счет чего они так сильно ускорили линковщик?

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

⚡️Самая мякотка - работа в параллель. C единицами трансляции мы интуитивно понимаем как параллелить: каждому вычислительному юниту даем обрабатывать свою TU. С линковкой конечно сложнее, но тоже решаемо. Линкерам на вход подается большое число однотипных данных, которые нужно обработать, и между которыми не так уж и много связей. Поэтому эту гору данных можно разбить на поток задачек, которые независимо можно выполнять на большом количестве потоков.

Однако рано или поздно наступит этап reduce, когда нужно собирать данные воедино. Для этого они используют потокобезопасную мапу, которая хранит отображение названия символа на сам объект символа. В качестве такой мапы mold использует Intel TBB's tbb::concurrent_hash_map. Крутая либа на самом деле, одно из лучших решений для высокопроизводительных потокобезопасных вычислений.

⚡️В качестве аллокатора используют mimaloc. Cтандартный malloc из glibc плохо масштабируется на большом количестве ядер, поэтому они решили попробовать сторонние решения. Среди jemalloc, tbbmalloc, tcmalloc и mimalloc - mimalloc от Microsoft
показал наилучшую производительность.

⚡️Маппинг файлов в адресное пространство процесса. Операции ввода-вывода всегда долгие. Но в mold'е сделали ход конем: Они просто отображают содержимое файла в память программы и могут его читать быстрее.

⚡️Если им и нужно записывать данные в файл, то они используют уже существующие файлы для перезаписи данных в них, нежели чем создают новые файлы. Данные намного быстрее записываются в файл, который уже находится в кэше буфера файловой системы.

Молодцы, ребята. Комплексно подошли к проблеме, работали по всем фронтам и применили интересные технические решения.

Be faster. Stay cool.

#tools
2👍25🔥1311❤‍🔥2
​​Несколько советов по написанию быстрого кода от разрабов mold
#новичкам

Чтобы стать самым быстрым опенсорс-линковщиком, нужно постараться. В результате этих стараний вырабатываются некоторые подходы к написанию производительных приложений, которым разрабочики mold хотели бы с вами поделиться.

 Не угадывай, а измеряй

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

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

Данные обычно важнее кода. Оптимальная структура данных может дать большее ускорение, чем оптимизация алгоритма.

 Реализуй несколько алгоритмов и выбери самый быстрый

Одних размышлений недостаточно, чтобы понять, какой алгоритм лучше — просто реализуйте несколько прототипов и сравните их на реальных данных.


Напиши одну и ту же программу несколько раз

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

- Существуют оптимизации, которые невозможно реализовать без переделки с нуля.

- Разработка во второй или третий раз происходит быстрее, поэтому время, потраченное на первую итерацию, не будет полностью потеряно.

А какие вы советы дадите по написанию быстрых программ?

Be faster. Stay cool.

#tool #goodpractice
👍2918😁6🔥51
include what you use
#опытным

Еще один способ уменьшить время сборки.

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

Не компилируются, но анализируются. Если включить в вашу единицу трансляции кучу ненужных хэдэров, то вся эта куча ненужного кода все равно будет как минимум анализироваться на этапе компиляции на соответствие синтаксису. И на это тратится время. А инклюдятся не только шаблоны, поэтому как максимум в объектный файл могут попасть совершенно неиспользуемые части.

Чтобы избежать лишнего анализа, есть такая практика в программировании на С/С++ - include what you use. Включайте в код только те заголовочники, где определены сущности, которые вы используете в коде. Тогда не будет тратится время на анализ ненужного кода.

У этого подхода есть еще одно преимущество. Если мы полагаемся на неявное включение одних хэдэров через другие, то могут возникнуть проблемы при рефакторинге. Вы вроде был убрали только ненужный функционал вместе с объявлениями соответствующих сущностей, а билд сломался с непонятной ошибкой. Потому что вы убрали источник тех неявно подключаемых заголовков и компилятору теперь их недостает. А мы знаем, какие он "шедевры" может выдавать, если ему чего-то не хватает(тот же пример с std::ranges::less)

Как использовать этот подход в проекте?

Я знаю пару способов:

1️⃣ Утилита iwyu. Установив ее и прописав зависимости в симейке, на этапе компиляции вам будут выдаваться варнинги, которые нужно будет постепенно фиксить:

# установка

sudo apt-get install iwyu

# интеграция с cmake

option(ENABLE_IWYU "Enable Include What You Use" OFF)

if(ENABLE_IWYU)
find_program(IWYU_PATH NAMES include-what-you-use iwyu)
if(IWYU_PATH)
message(STATUS "Found IWYU: ${IWYU_PATH}")
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "${IWYU_PATH}")
else()
message(WARNING "IWYU not found, disabling")
endif()
endif()

# запуск

mkdir -p build && cd build
cmake -DENABLE_IWYU=ON ..
make 2> iwyu_initial.out

# Анализ результатов
wc -l iwyu_initial.out # Общее количество предупреждений
grep -c "should add" iwyu_initial.out # Пропущенные includes
grep -c "should remove" iwyu_initial.out # Лишние includes


Там есть еще нюансы с 3rd-party, которые мы вынесем за рамки обсуждения.

2️⃣ clang-tidy. Если у вас настроены проверки clang-tidy, то вам ничего не стоит подключить include what you use. Достаточно к проверкам добавить пункт misc-include-cleaner. В конфиге также можно настроить различные исключения, что мы также выносим за скобки обсуждения.

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

Include what you use. Stay cool.

#tools #goodpractice
👍3511🔥11
​​shared libraries
#опытным

И еще один способ уменьшить время сборки проекта. А точнее линковки.

Идея такая: вы разбиваете свой проект на отдельные, независимые модули и компилируете их как разделяемые библиотеки. Дальше динамически линкуете эти библиотеки к своему исполняемому файлу.

Почему в этом случае линковка быстрее?

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

1️⃣ Разрешение символов - для каждого неопределенного символа ищется определение.

2️⃣ Создание единого адресного пространства - линковщик определяет окончательные адреса для всех сегментов кода и данных и объединяет однотипные секции из разных объектных файлов.

3️⃣ Применение релокаций - в объектных файлах и статических либах адреса указаны относительно и линковщик пересчитывает все адреса в абсолютные значения.

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

Плюс при изменениях в библиотеке, которые не затрагивают API и ABI, можно вообще не перелинковывать исполняемый файл - все изменения подтянутся в рантайме.

Линковка-то на самом деле происходит быстрее, но у разделяемых библиотек есть свои недостатки:

👉🏿 оверхэд на инициализацию программы за счет загрузки библиотек

👉🏿 оверхэд на первый вызов каждой функции. Но последующий вызовы уже не имеют заметного оверхэда за счет записей конкретных адресов в таблицу для каждого символа

👉🏿 более сложный деплой. Нужно вместе с бинарником распространять все разделяемые библиотеки. Если используется какой-нибудь докер, то головная боль относительно минимальна А если нет, то есть риски получить конфликты разных версий библиотеки для разных исполняемых файлов(так как все программы, слинкованные с одной шареной либой, обращаются в одному файлу) и увеличение coupling'а между разными программами, использующими одну либу.

А вы используете компиляцию модулей своих проектов, как разделяемых библиотек?

Share resources. Stay cool.

#tools
👍187😁6🔥2🆒1
​​Как анализировать процесс компиляции?
#опытным

Если вы уже дошли до ручки и у вас ежедневный передоз кофеином от безделья во время сборки проекта, пора что-то менять. Но с чего начать? Как понять, что конкретно занимает так много времени при компиляции?

И действительно, семь раз отмерь, один раз отрежь. Сколько бы вы не теоретизировали о проблемных местах в сборке, это не системный подход. Вам нужны цифры, чтобы хоть на что-то объективное опереться. Сегодня поговорим об инструментах анализа сборки.

Здесь будет только gcc и clang, с виндой у нас опыта особо нет. Знающие могут подсказать в комментах. Поехали.

GCC

Есть определенный набор опций компиляции, которые говорят компилятору выводить подробную информацию о внутренних процессах, происходящих при обработке цппшников и хэдэров. Для гцц это:

g++ -fstats  -fstack-usage  -ftime-report  -ftime-report-details -c large_file.cpp -o large_file.o

// или в CMakeLists.txt прописать

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstats -fstack-usage -ftime-report -ftime-report-details")


при компиляции вам выдастся что-то такое:

******
time in header files (total): 0.259532 (22%)
time in main file (total): 0.884263 (76%)
ratio = 0.293501 : 1

******
time in <path_to_header_1>: 0.000444 (0%)
time in <path_to_header_2>: 0.008682 (1%)
time in <path_to_header_3>: 0.885595 (76%)
/.../

Time variable wall GGC
phase setup : 0.05 ( 4%) 1813k ( 3%)
phase parsing : 1.11 ( 94%) 55M ( 97%)
phase lang. deferred : 0.01 ( 1%) 128k ( 0%)
// other metrics
TOTAL : 1.18 57M


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

И такая портянка генерируется для каждого цппшника. Будьте осторожнее при сборке, используйте make в один поток, иначе не поймете, что куда относится.

Clang

Чтобы получить подобный репорт для шланга нужна опция:

clang++ -ftime-trace -c large_file.cpp -o large_file.o

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ftime-trace")


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

// обязательно собрать проект с опцией -ftime-trace

ClangBuildAnalyzer --all build/ build_analysis.json
ClangBuildAnalyzer --analyze build_analysis.json


Вывод будет примерно такой:

**** Files that took longest to codegen (compiler backend):
// files list

**** Templates that took longest to instantiate:
// templates list

**** Functions that took longest to compile:
// functions list

etc...



Попробуйте начать с этих инструментов и детально проанализировать, где вы тратите больше всего времени. Если у вас глаза вытекают, глядя на статистику - скормите это добро нейронке. Если слишком много данных для анализа(большой проект), можно скрипт аггрегирующий написать.

В любом случае, измерение - залог качественного результата.

Look before you leap. Stay cool.

#tools
👍3212🔥93
Идиома IILE
#опытным

Неплохой практикой написания кода является определение переменных, которые не изменяются, как const. Это позволяет коду был более экспрессивным, явным, а также компилятор в этом случае может чуть лучше рассуждать о коде и оптимизировать. И это не требует ничего сложного:

const int myParam = inputParam * 10 + 5;
// or
const int myParam = bCondition ? inputParam * 2 : inputParam + 10;


Но что делать, если переменная по сути своей константа, но у нее громоздская инициализация на несколько строк?

int myVariable = 0; // this should be const...

if (bFirstCondition)
myVariable = bSecondCindition ? computeFunc(inputParam) : 0;
else
myVariable = inputParam * 2;

// more code of the current function...
// and we assume 'myVariable` is const now


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

Хочется и const сделать, и в отдельную функцию не выносить. Кажется, что на двух стульях не усидишь, но благодаря лямбдам мы можем это сделать!

Есть такая идиома IILE(Immediately Invoked Lambda Expression). Вы определяете лямбду и тут же ее вызываете. И контекст сохраняется, и единовременность инициализации присутствует:

const int myVariable = [&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
}(); // call!

Пара лишних символов, зато проблема решена.

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

Тоже не беда! Используем std::invoke:

const int myVariable = std::invoke([&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
});


Теперь мы четко и ясно видим, что лямбда вызывается. В таком виде прям кайф.

Эту же технику можно использовать например в списке инициализации конструктора, например, если нужно константное поле определить(его нельзя определять в теле конструктора).

Be expressive. Stay cool.

#cpp11 #cpp17 #goodpractice
🔥48👍2215👎4🤷‍♂3❤‍🔥2
Квиз
#новичкам

Сегодня короткий, но от того не менее интересный #quiz, . Можно было бы разобрать вопрос: "а что случится, если я мувну константную ссылку?", но так не очень интересно. Поэтому давайте проверим ваше знание мув-семантики.

У меня к вам всего один вопрос: Какой результат попытки компиляции и запуска следующего кода:

#include <iostream>

struct Test {
Test() = default;
Test(const Test &other) {
std::cout << "copy ctor " << std::endl;
}
Test(Test &&other) {
std::cout << "move ctor " << std::endl;
}
Test &operator=(const Test &other) = default;
Test &operator=(Test &&other) = default;
~Test() = default;
};

int main() {
Test test;
const Test &ref = test;
(void)std::move(ref);
auto enigma = std::move(ref);
}


Challenge yourself. Stay cool.
🤔208🔥6👍1