Короткий совет по отладке кода от мэтра
#новичкам
Отладка занимает около 30% всего времени разработки. К тому же сам язык у нас способствует появлению самых неожиданных проблем. Сидишь целый день и не вдупляешь, почему тесты падают.
Невозможно дать конкретный алгоритм по пунктам, что нужно делать. Однако общие советы могут помочь направить внимание в нужную точку. Вот, что об отладке говорит сам Страуструп:
Почему это работает?
✅ Отбрасывая все ненужное, вы сужаете пространство для анализа возможного проблемного места. Меньше кода для анализа - больше вероятность понять место проблемы.
✅ Вместо того, чтобы ныть, вы встаете в авторскую позицию. Это очень актуально для новичков: энтропия задачи ощутимо выше текущих навыков и просто опускаются руки.
Но вашей целью может стать не пофиксить неизвестную причину проблему, на самом минимальном примере попытаться воспроизвести проблему во всей красе.
✅ Если вы так и не поняли, в чем проблема, с маленьким примером вам намного охотнее и эффективнее смогут помочь коллеги.
Как локализовать проблему?
🔍 Мокайте зависимости от других компонентов и модулей.
🔍 Фиксируйте значения переменных.
🔍 Упрощайте входные данные.
Это практика полезна и с точки зрения архитектуры. Если вам много всего нужно менять для составления маленького примера, возможно у вас высокая связность и стоит порефакторить код после успешной отладки.
А какие вы дадите полезные советы по отладке кода?
Fix problems playfully. Stay cool.
#goodpractice
#новичкам
Отладка занимает около 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🔥19❤7👍6❤🔥3🤣3
Какой день будет через месяц?
#новичкам
Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. chrono появилась так-то в С++11. Но и даже с ее появлением жить стало лишь немногим легче.
Например, простая задача: "Прибавить к текущей дате 1 месяц".
В С++11 у нас есть только часы и точки на временной линии. Тут просто дату-то получить сложно. Есть конечно сишная std::localtime, можно мапулировать отдельными полями std::tm(днями, минутами и тд), но придется конвертировать сишные структуры времени в плюсовые, да и можно нарваться на трудноотловимые ошибки, если попытаться увеличить на 1 месяц 30 января.
Как прибавить к дате месяц? +30 дней или +1 месяц не канает. А если февраль? А если високосный год?
В общем стандартного решения нет... Или есть?
В С++20 в библиотеку chrono завезли кучу полезностей. А в частности функционал календаря. Теперь мы можем манипулировать отдельно датами и безопасно их изменять.
Например, чтобы получить сегодняшнюю дату и красиво ее вывести на консоль достаточно сделать следующее:
Появился прекрасный класс std::chrono::year_month_day, который отражает конкретно дату. И его объекты замечательно сериализуются в поток.
Если вам нужно задать определенный формат отображения - не проблема! Есть std::format:
С помощью std::chrono::year_month_day можно удобно манипулировать датами и, главное, делать это безопасно. Что будет если я к 29 января прибавлю месяц?
А будет все хорошо, если год високосный. Но если нет, то библиотека нам явно об этом скажет.
В общем, крутой фиче-сет, сильно облегчает работу со стандартными временными точками.
Take your time. Stay cool.
#cpp11 #cpp20
#новичкам
Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. 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
❤23👍17🔥6😁3
Сколько времени сейчас в Москве?
#новичкам
Как в стандартных плюсах работать с временными зонами? Да никак до С++20. Приходилось использовать разные сторонние решения.
Но стандарт развивается и у нас теперь есть возможность работать с зонами в чистом С++!
Появился класс std::chrono::zoned_time, который представляет собой пару из временной метки и временной зоны. Создать зонированное время можно так:
Функция std::chrono::current_zone() позволяет получить локальную временную зону.
Можно также передать имя зоны:
И это все прекрасно работает с std::format, который позволяет информацию о временной точки настолько подробно, насколько это возможно:
Работы с временными зонами очень не хватало в стандарте и круто, что ее добавили.
Develop yourself. Stay cool.
#cpp20
#новичкам
Как в стандартных плюсах работать с временными зонами? Да никак до С++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🔥37👍12❤8😁7
Внутрянка бэкэнда
Бэкенд интересен тем, что ему ставятся амбициозные задачи из самых разных доменных областей. И с каждым годом планка все повышается. Все больше сфер жизни оцифровываются, rps растет, а ML хотят все да побольше. Это прям огромная кроличья нора и глубины ее не видно. Однако эти челенджи успешно решаются российскими IT-компаниями.
И 4 октября в Москве эксперты нашего бигтеха будут раскрывать про внутрянку своих продуктов на конференции «Я про бэкенд»! В докладах будет,много про бэкенд челледжи, связанных с МЛ, рекомендательными и генеративными технологиями.
Держите список докладов:
-Антон Полднев (Яндекс Реклама): как рекомендательный движок ежегодно экономит 200 тыс. CPU в инфраструктуре Рекламы
-Дмитрий Погорелов (VK): эволюция рекомендательного движка и перезапуск рекомендаций ВКонтакте
-Михаил Чебаков (T-Банк): как прятать сложность LLM-инференса за понятными числами
-Андрей Шукшов (Яндекс R&D): внутри LLM: как выжать максимум из decoder attention на GPU
-Алёна Васильева (Шедеврум): про архитектуру для ML-моделей и длинный инференс
-Никита Сикалов (Яндекс Поиск): про эволюцию технологий реалтайм-индексации
И еще куча интересностей, которые вы может найти в программе мероприятия по ссылочке.
Регистрируйтесь и прокачивайте свои бэкэндерские мускулы
Бэкенд интересен тем, что ему ставятся амбициозные задачи из самых разных доменных областей. И с каждым годом планка все повышается. Все больше сфер жизни оцифровываются, 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 могут быть даже машины ваших коллег!
Наиболее известный представитель систем распределенной компиляции с открытым исходным кодом — это
Вот примерная схема его работы(у кого-то может некорректно отображаться, ничего поделать не можем):
- Координатор анализирует зависимости между файлами и распределяет задачи компиляции
- Компиляционные ноды выполняют фактическую компиляцию, их набор конфигурируется на клиенте
- Распределенный кэш хранит скомпилированные объектные файлы, кэширует результаты компиляции, тем самым ускоряя повторные сборки
Этапы работы примерно такие:
1️⃣ Все цппшники проекта проходят этап препроцессинга на локальной машине и уже в виде единиц трансляции перенаправляются на ноды компиляции.
2️⃣ Ноды компиляции преобразуют единицы трансляции в объектные файлы и пересылают их на клиентскую машину.
3️⃣ Последним этапом идет бутылочное горлышко всей системы - линковка. Для нее необходим доступ ко многим объектым файлам одновременно и эта задача слабо параллелится, поэтому и выполняется на клиентской машине.
Таким образом вы можете уменьшить время сборки проекта в разы и ускорить разработку в целом. Вот ссылочка на доку для заинтересовавшихся.
Speed up processes. Stay cool.
#compiler #tools
#опытным
Как только ваш проект достигает определенного размера, время компиляции начинает становиться проблемой. В моей скромной практике были проекты, которые полностью собирались с 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👍25❤9🔥8
ccache
#опытным
Еще один полезный и простой во внедрении инструмент для ускорения компиляции - ccache.
Это кеш компилятора, который сохраняет артефакты, полученные в ходе предыдущих запусков сборки, чтобы ускорить последующие. Грубо говоря, если вы попытаетесь перекомпилировать исходный файл с тем же содержимым, тем же компилятором и с теми же флагами, готовый результат будет взят из кеша, а не компилироваться заново в течение долгого времени.
ccache работает как обёртка компилятора — его внешний интерфейс очень похож на интерфейс вашего компилятора, и он передаёт ваши команды ему. К сожалению, поскольку ccache должен анализировать и интерпретировать флаги командной строки, его нельзя использовать с произвольными компиляторами. Вроде как он только гцц и шланг поддерживает.
Ну а сам кеш — это обычная директория на вашем диске, где хранятся объектники и всякая метаинформация. То есть он глобальных для всех проектов на одной машине.
Поиск записей в кеше осуществляется с помощью уникального тега, который представляет собой строку, состоящую из двух элементов: хэш-значения и размера препроцессированного исходного файла. Хэш-значение вычисляется путём пропускания через хэш-функцию MD4 всей информации, необходимой для получения выходного файла. Эта информация включает, среди прочего:
👉🏿 идентификатор компилятора
👉🏿 использованные флаги компилятора
👉🏿 содержимое входного исходного файла,
👉🏿 содержимое подключаемых заголовочных файлов (и их транзитивное замыкание).
Кэшу не надо беспокоиться за криптостойкость, поэтому в нем спокойно используется небезопасная, но быстрая функция c хорошим распределением.
После вычисления значения тега ccache проверяет, существует ли запись с таким тегом в кеше. Если да, перекомпиляция не нужна. Что удобно, ccache запоминает не только сам артефакт, но и вывод компилятора в консоль, который был сгенерирован при его создании — поэтому, если вы извлекаете закешированный файл, который ранее вызывал предупреждения компилятора, ccache снова выведет эти предупреждения.
Если распределенный компилятор distcc каждый раз выполняет препроцессинг, то для ccache это не обязательно. В одном из режимов работы ccache вычисляет хэши MD4 для каждого включаемого заголовочного файла отдельно и сохраняет результаты в так называемом манифесте. Поиск в кеше выполняется путём сравнения хэшей исходного файла и всех его включений с содержимым манифеста; если все хэши попарно совпадают, мы имеем попадание. В текущих версиях ccache прямой режим включён по умолчанию.
Для того, чтобы начать пользоваться ccache, достаточно его установить, добавить в PATH и в cmake'е прописать
Но это было введение в ccache для незнающих. Опытный же подписчик спросит: а зачем нужен этот кэш, если cmake и собирает только то, что мы недавно изменили? Об этом ключевом вопросе мы и поговорим в следующем посте.
Compile fast. Stay cool.
#compiler #tools
#опытным
Еще один полезный и простой во внедрении инструмент для ускорения компиляции - 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👍21❤12🔥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
#опытным
И давайте раскроем очевидный вопрос: чем кэширование ccache отличается от кэширования самого cmake'а? Ведь при искрементальной сборке cmake пересобирает только те файлы, которые поменялись.
Основное отличие: cmake - это система сборки, а ccache - это четко кэш. cmake не может себе позволить анализировать контент всех файлов, его основная задача - билдить проект. Поэтому ему нужно очень быстро понять, изменился файл или нет. И принимает он решение на основе времени модификации файла. А ccache не ограничен такими рамками. Он учитывает контент препроцесснутого файла и контекст компиляции.
Проще понять разницу на примерах:
🔍 Вы скомпилировали проект и случайно или специально(бывает нужно, если cmake троит) удалили папку с билдом. Без ccache нужно перекомпилировать все, а с ним - ничего, только линковку сделать.
🔍 Вы плотно работаете с несколькими ощутимо отличающимися бранчами, собираете, коммитите и переключаетесь. Без ccache нужно будет перекомпилировать все измененные при переключении бранчей файлы. С ccache - только те, которые вы сами изменили после последней сборки.
🔍 Если вы правите только комменты в файле, то голый cmake пойдет перекомпилировать его. ccache - нет, потому что работает с препроцесснутым файлом.
🔍 Вы активно переключаетесь между конфигурациями при сборке. Например между релизом и дебагом. cmake будет полностью пересобирать проект при изменении типа билда. А ccahce после одной сборки на каждую конфигурацию все запомнит и вы будете компилировать только последние изменения.
Not much, но каждый из нас частенько сталкивается с одним из этих пунктов. Поэтому ставьте ccache. Это сделать просто, но импакт дает ощутимый в определенных кейсах.
Compile fast. Stay cool.
#compiler #tools
1❤34👍14🔥8
Быстрые линкеры
#опытным
По предыдущим постам стало уже понятно, что линковка - бутылочное горлышко всей сборки. Если меняется хоть одна единица трансляции - перелинковываться бинарник будет полностью вне зависимости от количества TU в ней. Хотелось бы ускорить движение по этому горлышку.
GCC как самый широкоиспользуемый компилятор использует ld в качестве линкера. ld считается очень громоздким, раздутым и от этого медленным. Можно решить проблему гениально и просто использовать быстрый линкер!
С ld можно бесшовно перейти на другой совместимый компоновщик с помощью опции -fuse-ld. То есть буквально:
И ваша программа будет собираться с помощью my_linker. Ну или в cmake:
Какие альтернативные компоновщики существуют?
✅ 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
#опытным
По предыдущим постам стало уже понятно, что линковка - бутылочное горлышко всей сборки. Если меняется хоть одна единица трансляции - перелинковываться бинарник будет полностью вне зависимости от количества 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🔥39❤15👍10❤🔥4⚡2
Откуда такая скорость у 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
#опытным
На графиках с предыдущего поста видно, что 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
GitHub
GitHub - uxlfoundation/oneTBB: oneAPI Threading Building Blocks (oneTBB)
oneAPI Threading Building Blocks (oneTBB). Contribute to uxlfoundation/oneTBB development by creating an account on GitHub.
2👍26🔥13❤11❤🔥2
Несколько советов по написанию быстрого кода от разрабов mold
#новичкам
Чтобы стать самым быстрым опенсорс-линковщиком, нужно постараться. В результате этих стараний вырабатываются некоторые подходы к написанию производительных приложений, которым разрабочики mold хотели бы с вами поделиться.
✅ Не угадывай, а измеряй
Предположения и отрешенные размышления обычно оказываются неверными, поэтому не тратьте время на оптимизацию кода, который не имеет значения. Оптимизируйте самые важные и узкие части. А чтобы их найти, используйте профилировщики, например perf.
✅ Не пытайся писать быстрый код. Вместо этого проектируй структуры данных так, чтобы программа естественным образом становилась быстрее
Данные обычно важнее кода. Оптимальная структура данных может дать большее ускорение, чем оптимизация алгоритма.
✅ Реализуй несколько алгоритмов и выбери самый быстрый
Одних размышлений недостаточно, чтобы понять, какой алгоритм лучше — просто реализуйте несколько прототипов и сравните их на реальных данных.
✅ Напиши одну и ту же программу несколько раз
- Переписывать проект - это нормально. Невозможно и не нужно написать все с первого раза идеально. Это вам скажет почти любой разработчик. Но нужно учиться на первой реализации, затем использовать эти знания для переписывания.
- Существуют оптимизации, которые невозможно реализовать без переделки с нуля.
- Разработка во второй или третий раз происходит быстрее, поэтому время, потраченное на первую итерацию, не будет полностью потеряно.
А какие вы советы дадите по написанию быстрых программ?
Be faster. Stay cool.
#tool #goodpractice
#новичкам
Чтобы стать самым быстрым опенсорс-линковщиком, нужно постараться. В результате этих стараний вырабатываются некоторые подходы к написанию производительных приложений, которым разрабочики mold хотели бы с вами поделиться.
✅ Не угадывай, а измеряй
Предположения и отрешенные размышления обычно оказываются неверными, поэтому не тратьте время на оптимизацию кода, который не имеет значения. Оптимизируйте самые важные и узкие части. А чтобы их найти, используйте профилировщики, например perf.
✅ Не пытайся писать быстрый код. Вместо этого проектируй структуры данных так, чтобы программа естественным образом становилась быстрее
Данные обычно важнее кода. Оптимальная структура данных может дать большее ускорение, чем оптимизация алгоритма.
✅ Реализуй несколько алгоритмов и выбери самый быстрый
Одних размышлений недостаточно, чтобы понять, какой алгоритм лучше — просто реализуйте несколько прототипов и сравните их на реальных данных.
✅ Напиши одну и ту же программу несколько раз
- Переписывать проект - это нормально. Невозможно и не нужно написать все с первого раза идеально. Это вам скажет почти любой разработчик. Но нужно учиться на первой реализации, затем использовать эти знания для переписывания.
- Существуют оптимизации, которые невозможно реализовать без переделки с нуля.
- Разработка во второй или третий раз происходит быстрее, поэтому время, потраченное на первую итерацию, не будет полностью потеряно.
А какие вы советы дадите по написанию быстрых программ?
Be faster. Stay cool.
#tool #goodpractice
👍29❤18😁6🔥5⚡1
include what you use
#опытным
Еще один способ уменьшить время сборки.
Ваша программа может содержать все хэдэры стандартной библиотеки и прекрасно собираться. В чем проблема? Неиспользуемые шаблоны же не компилируются.
Не компилируются, но анализируются. Если включить в вашу единицу трансляции кучу ненужных хэдэров, то вся эта куча ненужного кода все равно будет как минимум анализироваться на этапе компиляции на соответствие синтаксису. И на это тратится время. А инклюдятся не только шаблоны, поэтому как максимум в объектный файл могут попасть совершенно неиспользуемые части.
Чтобы избежать лишнего анализа, есть такая практика в программировании на С/С++ - include what you use. Включайте в код только те заголовочники, где определены сущности, которые вы используете в коде. Тогда не будет тратится время на анализ ненужного кода.
У этого подхода есть еще одно преимущество. Если мы полагаемся на неявное включение одних хэдэров через другие, то могут возникнуть проблемы при рефакторинге. Вы вроде был убрали только ненужный функционал вместе с объявлениями соответствующих сущностей, а билд сломался с непонятной ошибкой. Потому что вы убрали источник тех неявно подключаемых заголовков и компилятору теперь их недостает. А мы знаем, какие он "шедевры" может выдавать, если ему чего-то не хватает(тот же пример с std::ranges::less)
Как использовать этот подход в проекте?
Я знаю пару способов:
1️⃣ Утилита iwyu. Установив ее и прописав зависимости в симейке, на этапе компиляции вам будут выдаваться варнинги, которые нужно будет постепенно фиксить:
Там есть еще нюансы с 3rd-party, которые мы вынесем за рамки обсуждения.
2️⃣ clang-tidy. Если у вас настроены проверки clang-tidy, то вам ничего не стоит подключить include what you use. Достаточно к проверкам добавить пункт misc-include-cleaner. В конфиге также можно настроить различные исключения, что мы также выносим за скобки обсуждения.
В целом, полезная вещь. Помогает меньше связывать модули друг с другом, а также потенциально уменьшает время компиляции.
Include what you use. Stay cool.
#tools #goodpractice
#опытным
Еще один способ уменьшить время сборки.
Ваша программа может содержать все хэдэры стандартной библиотеки и прекрасно собираться. В чем проблема? Неиспользуемые шаблоны же не компилируются.
Не компилируются, но анализируются. Если включить в вашу единицу трансляции кучу ненужных хэдэров, то вся эта куча ненужного кода все равно будет как минимум анализироваться на этапе компиляции на соответствие синтаксису. И на это тратится время. А инклюдятся не только шаблоны, поэтому как максимум в объектный файл могут попасть совершенно неиспользуемые части.
Чтобы избежать лишнего анализа, есть такая практика в программировании на С/С++ - 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
👍35❤11🔥11
shared libraries
#опытным
И еще один способ уменьшить время сборки проекта. А точнее линковки.
Идея такая: вы разбиваете свой проект на отдельные, независимые модули и компилируете их как разделяемые библиотеки. Дальше динамически линкуете эти библиотеки к своему исполняемому файлу.
Почему в этом случае линковка быстрее?
Для того, чтобы это понять, нужно знать, что происходит при статической линковке. На вход принимаются много объектных файлов и статических библиотек и компановщик выполняет примерно следующий набор действий:
1️⃣ Разрешение символов - для каждого неопределенного символа ищется определение.
2️⃣ Создание единого адресного пространства - линковщик определяет окончательные адреса для всех сегментов кода и данных и объединяет однотипные секции из разных объектных файлов.
3️⃣ Применение релокаций - в объектных файлах и статических либах адреса указаны относительно и линковщик пересчитывает все адреса в абсолютные значения.
Так вот при динамической линковке компановщику лишь нужно проверить, что в библиотеке есть символы, которые используются в исполняемом файлы, поставить на месте использования символов заглушки, ну и записать определенную метаинформацию. Никаких вычислений адресов и прочего.
Плюс при изменениях в библиотеке, которые не затрагивают API и ABI, можно вообще не перелинковывать исполняемый файл - все изменения подтянутся в рантайме.
Линковка-то на самом деле происходит быстрее, но у разделяемых библиотек есть свои недостатки:
👉🏿 оверхэд на инициализацию программы за счет загрузки библиотек
👉🏿 оверхэд на первый вызов каждой функции. Но последующий вызовы уже не имеют заметного оверхэда за счет записей конкретных адресов в таблицу для каждого символа
👉🏿 более сложный деплой. Нужно вместе с бинарником распространять все разделяемые библиотеки. Если используется какой-нибудь докер, то головная боль относительно минимальна А если нет, то есть риски получить конфликты разных версий библиотеки для разных исполняемых файлов(так как все программы, слинкованные с одной шареной либой, обращаются в одному файлу) и увеличение coupling'а между разными программами, использующими одну либу.
А вы используете компиляцию модулей своих проектов, как разделяемых библиотек?
Share resources. Stay cool.
#tools
#опытным
И еще один способ уменьшить время сборки проекта. А точнее линковки.
Идея такая: вы разбиваете свой проект на отдельные, независимые модули и компилируете их как разделяемые библиотеки. Дальше динамически линкуете эти библиотеки к своему исполняемому файлу.
Почему в этом случае линковка быстрее?
Для того, чтобы это понять, нужно знать, что происходит при статической линковке. На вход принимаются много объектных файлов и статических библиотек и компановщик выполняет примерно следующий набор действий:
1️⃣ Разрешение символов - для каждого неопределенного символа ищется определение.
2️⃣ Создание единого адресного пространства - линковщик определяет окончательные адреса для всех сегментов кода и данных и объединяет однотипные секции из разных объектных файлов.
3️⃣ Применение релокаций - в объектных файлах и статических либах адреса указаны относительно и линковщик пересчитывает все адреса в абсолютные значения.
Так вот при динамической линковке компановщику лишь нужно проверить, что в библиотеке есть символы, которые используются в исполняемом файлы, поставить на месте использования символов заглушки, ну и записать определенную метаинформацию. Никаких вычислений адресов и прочего.
Плюс при изменениях в библиотеке, которые не затрагивают API и ABI, можно вообще не перелинковывать исполняемый файл - все изменения подтянутся в рантайме.
Линковка-то на самом деле происходит быстрее, но у разделяемых библиотек есть свои недостатки:
👉🏿 оверхэд на инициализацию программы за счет загрузки библиотек
👉🏿 оверхэд на первый вызов каждой функции. Но последующий вызовы уже не имеют заметного оверхэда за счет записей конкретных адресов в таблицу для каждого символа
👉🏿 более сложный деплой. Нужно вместе с бинарником распространять все разделяемые библиотеки. Если используется какой-нибудь докер, то головная боль относительно минимальна А если нет, то есть риски получить конфликты разных версий библиотеки для разных исполняемых файлов(так как все программы, слинкованные с одной шареной либой, обращаются в одному файлу) и увеличение coupling'а между разными программами, использующими одну либу.
А вы используете компиляцию модулей своих проектов, как разделяемых библиотек?
Share resources. Stay cool.
#tools
👍18❤8😁6🔥2🆒1
Как анализировать процесс компиляции?
#опытным
Если вы уже дошли до ручки и у вас ежедневный передоз кофеином от безделья во время сборки проекта, пора что-то менять. Но с чего начать? Как понять, что конкретно занимает так много времени при компиляции?
И действительно, семь раз отмерь, один раз отрежь. Сколько бы вы не теоретизировали о проблемных местах в сборке, это не системный подход. Вам нужны цифры, чтобы хоть на что-то объективное опереться. Сегодня поговорим об инструментах анализа сборки.
Здесь будет только gcc и clang, с виндой у нас опыта особо нет. Знающие могут подсказать в комментах. Поехали.
GCC
Есть определенный набор опций компиляции, которые говорят компилятору выводить подробную информацию о внутренних процессах, происходящих при обработке цппшников и хэдэров. Для гцц это:
при компиляции вам выдастся что-то такое:
Вы получите подробную статистику о времени, потраченном на анализ конкретных хэдэров и на каждый отдельный этап обработки единицы трансляции. Если у вас много шаблонов - вам об этом скажут. Если сложное разрешение перегрузок - тоже. И тд.
И такая портянка генерируется для каждого цппшника. Будьте осторожнее при сборке, используйте make в один поток, иначе не поймете, что куда относится.
Clang
Чтобы получить подобный репорт для шланга нужна опция:
Плюс к этому у шланга с инструментами как всегда по-веселее, чем у гцц. Есть утилитка ClangBuildAnalyzer, который позволяет агрегировать и предоставлять в более читаемом виде информацию о общих таймингах компиляции и самых трудозатратных местах сборки. Можно собрать из исходников по ссылочке и использовать его так:
Вывод будет примерно такой:
Попробуйте начать с этих инструментов и детально проанализировать, где вы тратите больше всего времени. Если у вас глаза вытекают, глядя на статистику - скормите это добро нейронке. Если слишком много данных для анализа(большой проект), можно скрипт аггрегирующий написать.
В любом случае, измерение - залог качественного результата.
Look before you leap. Stay cool.
#tools
#опытным
Если вы уже дошли до ручки и у вас ежедневный передоз кофеином от безделья во время сборки проекта, пора что-то менять. Но с чего начать? Как понять, что конкретно занимает так много времени при компиляции?
И действительно, семь раз отмерь, один раз отрежь. Сколько бы вы не теоретизировали о проблемных местах в сборке, это не системный подход. Вам нужны цифры, чтобы хоть на что-то объективное опереться. Сегодня поговорим об инструментах анализа сборки.
Здесь будет только 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
👍32❤12🔥9⚡3
Идиома IILE
#опытным
Неплохой практикой написания кода является определение переменных, которые не изменяются, как const. Это позволяет коду был более экспрессивным, явным, а также компилятор в этом случае может чуть лучше рассуждать о коде и оптимизировать. И это не требует ничего сложного:
Но что делать, если переменная по сути своей константа, но у нее громоздская инициализация на несколько строк?
По-хорошему это уносится в какую-нибудь отдельную функцию. Но тогда теряется контекст и нужно будет прыгать по коду.
Хочется и const сделать, и в отдельную функцию не выносить. Кажется, что на двух стульях не усидишь, но благодаря лямбдам мы можем это сделать!
Есть такая идиома IILE(Immediately Invoked Lambda Expression). Вы определяете лямбду и тут же ее вызываете. И контекст сохраняется, и единовременность инициализации присутствует:
Пара лишних символов, зато проблема решена.
Тут есть одно "но". Вроде все хорошо, но немного напрягает, что все это можно спутать с простым определением лямбды, если не увидеть скобки вызова в конце.
Тоже не беда! Используем std::invoke:
Теперь мы четко и ясно видим, что лямбда вызывается. В таком виде прям кайф.
Эту же технику можно использовать например в списке инициализации конструктора, например, если нужно константное поле определить(его нельзя определять в теле конструктора).
Be expressive. Stay cool.
#cpp11 #cpp17 #goodpractice
#опытным
Неплохой практикой написания кода является определение переменных, которые не изменяются, как 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👍22❤15👎4🤷♂3❤🔥2
Квиз
#новичкам
Сегодня короткий, но от того не менее интересный #quiz, . Можно было бы разобрать вопрос: "а что случится, если я мувну константную ссылку?", но так не очень интересно. Поэтому давайте проверим ваше знание мув-семантики.
У меня к вам всего один вопрос: Какой результат попытки компиляции и запуска следующего кода:
Challenge yourself. Stay cool.
#новичкам
Сегодня короткий, но от того не менее интересный #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.
🤔20❤8🔥6👍1
Какой результат попытки компиляции и запуска кода?
Anonymous Poll
25%
Ошибка компиляции
31%
copy ctor
14%
move ctor
9%
move ctor move ctor
11%
copy ctor move ctor
3%
move ctor copy ctor
7%
copy ctor copy ctor
👍12❤8🔥4👎1
Ответ
#новичкам
Многие из вас подумали, что будет ошибка компиляции. В целом, логичная цепочка мыслей: ну как же можно мувнуть данные из константной ссылки? Она же неизменяема.
Но она все же неверная. Правильный ответ: на консоль выведется "copy ctor".
Копия? Мы же муваем!
Сейчас разберемся. Но для начало вспомним сам пример:
На самом деле проблема в нейминге. Все вопросы к комитету. Это они имена всему плюсовому раздают.
std::move ничего не мувает. Она делает всего лишь static_cast. Но не просто каст к правой ссылке, это не совсем корректно. Посмотрим на реализацию std::move:
Обратите внимание, что от типа отрезается любая ссылочность и только затем добавляется правоссылочность. Но константность-то никуда не уходит. По сути результирующий тип выражения std::move({константная левая ссылка}) это константная правая ссылка.
Чтобы это проверить, перейдем на cppinsights:
Так как мы просто кастуем к валидному типу, мув успешно отрабатывает, но строчка
Теперь про копирование. Вспомним правила приведения типов. const T&& может приводится только к const T&. То есть единственный конструктор, который может вызваться - это копирующий конструктор.
Интересная ситуация, конечно, что "перемещение" может приводить к копированию в плюсах. Но имеем, что имеем. Терпим и продолжаем грызть гранит С++.
Give a proper name. Stay cool.
#cppcore #template
#новичкам
Многие из вас подумали, что будет ошибка компиляции. В целом, логичная цепочка мыслей: ну как же можно мувнуть данные из константной ссылки? Она же неизменяема.
Но она все же неверная. Правильный ответ: на консоль выведется "copy ctor".
Копия? Мы же муваем!
Сейчас разберемся. Но для начало вспомним сам пример:
#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 emigma = std::move(ref);
}
На самом деле проблема в нейминге. Все вопросы к комитету. Это они имена всему плюсовому раздают.
std::move ничего не мувает. Она делает всего лишь static_cast. Но не просто каст к правой ссылке, это не совсем корректно. Посмотрим на реализацию std::move:
template <class T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Обратите внимание, что от типа отрезается любая ссылочность и только затем добавляется правоссылочность. Но константность-то никуда не уходит. По сути результирующий тип выражения std::move({константная левая ссылка}) это константная правая ссылка.
Чтобы это проверить, перейдем на cppinsights:
Test test;
const Test &ref = test;
using ExprType = decltype(std::move(ref));
// под капотом ExprType вот чему равен
using ExprType = const Test &&;
Так как мы просто кастуем к валидному типу, мув успешно отрабатывает, но строчка
(void)std::move(ref); не дает в консоли никакого вывода, потому что никаких новых объектов мы не создаем.Теперь про копирование. Вспомним правила приведения типов. const T&& может приводится только к const T&. То есть единственный конструктор, который может вызваться - это копирующий конструктор.
Интересная ситуация, конечно, что "перемещение" может приводить к копированию в плюсах. Но имеем, что имеем. Терпим и продолжаем грызть гранит С++.
Give a proper name. Stay cool.
#cppcore #template
❤34👍14🔥7😁4🥱2💯1
Гарантии безопасности исключений
#новичкам
Программа на С++ - очень интересное явление. Вроде мощный, подкаченный, умный и скоростной парень. Но вот проблема. Ходить не умеет нормально. То в ноги себе стреляет, то падает периодически. В общем, беда у него с ходьбой. У этого могут быть разные причины. Все из них даже трудно в голове удержать. Но сегодня обсудим, какие есть гипсы, лангеты и костыли, которые помогут этому парню нормально ходить при работе с исключениями.
Даже не зная, что вы работаете с исключениями - вы уже работаете с ними. Даже обычный, казалось бы, безобидный new может кинуть std::bad_alloc. И это core языка. Стандартная библиотека пронизана исключениями.
Обрабатывать исключения можно по-разному и это будет давать разные результаты. От того, как обрабатываются исключения в модуле, зависит, какие гарантии он может дать в случае возникновения исключительной ситуации.
А это очень важная штука, потому что взаимодействующий с модулем код полагается на его адекватное поведение, которое с легкостью может быть нарушено, если нет гарантий безопасности.
Итак, существует 3 гарантии безопасности исключений:
Базовая гарантия. Формулировка разнится, но более общая звучит так: "после возникновения исключения и его обработки программа должна остаться в согласованном состоянии". Теперь на рабоче-крестьянском: не должно быть утечек ресурсов и должны сохраняться все инварианты классов. С утечками, думаю, все понятно. Инвариант - некое логически согласованное состояние системы. Если класс владеет массивом и содержит поле для его размера, то инвариантом этого класса будет совпадение реального размера массива со значением поля-размера. Для класса "легковая машина" инвариантом будет количество колес - 4. Обычная машина без одного или нескольких колес просто не едет. И для того, чтобы машина корректно работала, количество колес должно быть одинаково - 4. Или например, нельзя, чтобы дата создания чего-то была больше текущего дня, ну никак. Вот такие штуки должны сохраняться.
Строгая гарантия. Если при выполнении операции возникает исключение, операция не должна оказать на систему никакого влияния. То есть пан или пропал. Либо вся операция выполняется успешно и ее результат применяется, либо система откатывается в состояние до выполнения операции. Это свойство программы называется транзакционностью.
Гарантия отсутствия исключений. Ни при каких обстоятельствах не будет брошено исключение. Легко сказать, но тяжело сделать. С++ и его стандартная библиотека разрабатывались с учетом использования исключений. И это накладывает свои ограничения на отсутствие исключений. Мы все-таки не на Go пишем.
Ну и есть еще одна гарантия - отсутствие каких-либо гарантий. Мама - анархия, во всей красе.
Тема важная, будем потихоньку ее разбирать с примерами по каждой гарантии.
Be a guarantor. Stay cool.
#cppcore
#новичкам
Программа на С++ - очень интересное явление. Вроде мощный, подкаченный, умный и скоростной парень. Но вот проблема. Ходить не умеет нормально. То в ноги себе стреляет, то падает периодически. В общем, беда у него с ходьбой. У этого могут быть разные причины. Все из них даже трудно в голове удержать. Но сегодня обсудим, какие есть гипсы, лангеты и костыли, которые помогут этому парню нормально ходить при работе с исключениями.
Даже не зная, что вы работаете с исключениями - вы уже работаете с ними. Даже обычный, казалось бы, безобидный new может кинуть std::bad_alloc. И это core языка. Стандартная библиотека пронизана исключениями.
Обрабатывать исключения можно по-разному и это будет давать разные результаты. От того, как обрабатываются исключения в модуле, зависит, какие гарантии он может дать в случае возникновения исключительной ситуации.
А это очень важная штука, потому что взаимодействующий с модулем код полагается на его адекватное поведение, которое с легкостью может быть нарушено, если нет гарантий безопасности.
Итак, существует 3 гарантии безопасности исключений:
Базовая гарантия. Формулировка разнится, но более общая звучит так: "после возникновения исключения и его обработки программа должна остаться в согласованном состоянии". Теперь на рабоче-крестьянском: не должно быть утечек ресурсов и должны сохраняться все инварианты классов. С утечками, думаю, все понятно. Инвариант - некое логически согласованное состояние системы. Если класс владеет массивом и содержит поле для его размера, то инвариантом этого класса будет совпадение реального размера массива со значением поля-размера. Для класса "легковая машина" инвариантом будет количество колес - 4. Обычная машина без одного или нескольких колес просто не едет. И для того, чтобы машина корректно работала, количество колес должно быть одинаково - 4. Или например, нельзя, чтобы дата создания чего-то была больше текущего дня, ну никак. Вот такие штуки должны сохраняться.
Строгая гарантия. Если при выполнении операции возникает исключение, операция не должна оказать на систему никакого влияния. То есть пан или пропал. Либо вся операция выполняется успешно и ее результат применяется, либо система откатывается в состояние до выполнения операции. Это свойство программы называется транзакционностью.
Гарантия отсутствия исключений. Ни при каких обстоятельствах не будет брошено исключение. Легко сказать, но тяжело сделать. С++ и его стандартная библиотека разрабатывались с учетом использования исключений. И это накладывает свои ограничения на отсутствие исключений. Мы все-таки не на Go пишем.
Ну и есть еще одна гарантия - отсутствие каких-либо гарантий. Мама - анархия, во всей красе.
Тема важная, будем потихоньку ее разбирать с примерами по каждой гарантии.
Be a guarantor. Stay cool.
#cppcore
❤23👍14🔥7👎2
Базовая гарантия исключений
#новичкам
Вспомним свойства базовой гарантии: после возникновения исключения в программе не должно быть утечек ресурсов и должны сохраняться все инварианты классов.
И не всегда базовой гарантии легко удовлетворить. Поскольку исключения добавляют в программу дополнительные пути выполнения кода, крайне важно учитывать последствия работы кода по таким путям и избегать любых нежелательных эффектов, которые в противном случае могут возникнуть. Давайте посмотрим на примере:
Внутренний инвариант класса IntArray - член
Код, который избегает подобных нежелательных эффектов, называется exception safe. То есть предоставление базовой гарантии уже говорит о том, что ваш код "exception safe".
Допустим, что ваш класс предоставляет базовую гарантию исключений. Какие выводы мы можем из этого сделать? Ну сохранены инварианты, а значения-то какие будут у полей?
В том-то и дело, что конкретные значения неизвестны. И это сильно ограничивает практическое использование таких объектов. По сути единственное, что с ним можно гарантировано безопасно сделать - это разрушить.
И это главное. Вся магия с раскруткой стека и вызовом деструктором локальных объектов работает только если деструкторы вызываются безопасно. А для этого объект должен быть в валидном, но необязательно определенном, состоянии. То есть вы в принципе не можете восстановить работоспособность приложения, если ваши инструменты не предоставляют хотя бы базовую гарантию.
Здесь кстати можно провести параллель с мувнутыми объектами: ими тоже особо не попользуешься и по хорошему их надо просто удалить.
Примером предоставления только базовой гарантии может быть использование какой-нибудь базы данных. Если при выполнении запроса фреймворк выкинул исключение, например потому что соединение отвалилось, то объект для работы с базой остался в валидном состоянии, но нет никакой информации о том, выполнился запрос или нет:
Поэтому мы должны иметь ввиду это неопределенное состояние коннекшена и базы при выполнении ретраев.
Provide guarantees. Stay cool.
#cppcore
#новичкам
Вспомним свойства базовой гарантии: после возникновения исключения в программе не должно быть утечек ресурсов и должны сохраняться все инварианты классов.
И не всегда базовой гарантии легко удовлетворить. Поскольку исключения добавляют в программу дополнительные пути выполнения кода, крайне важно учитывать последствия работы кода по таким путям и избегать любых нежелательных эффектов, которые в противном случае могут возникнуть. Давайте посмотрим на примере:
class IntArray {
int *array;
std::size_t nElems;
public:
// ...
~IntArray() { delete[] array; }
IntArray(const IntArray &that); // nontrivial copy constructor
IntArray &operator=(const IntArray &rhs) {
if (this != &rhs) {
delete[] array;
array = nullptr;
nElems = rhs.nElems;
if (nElems) {
array = new int[nElems];
std::memcpy(array, rhs.array, nElems * sizeof(*array));
}
}
return *this;
}
// ...
};Внутренний инвариант класса IntArray - член
array является валидным (возможно, нулевым) указателем, а член nElems хранит количество элементов в массиве. В операторе присваивания освобождается память текущего array'я и присваивается значение счётчику элементов nElems до выделения нового блока памяти для копии. В результате, если из new вылетит исключение, то array будет нулевым, а размер массива нет. Это нарушение инварианта и таким объектом просто небезопасно пользоваться. Метод size потенциально вернет ненулевой размер, а закономерное использование следом оператора[] приведет к неопределенному поведению.Код, который избегает подобных нежелательных эффектов, называется exception safe. То есть предоставление базовой гарантии уже говорит о том, что ваш код "exception safe".
Допустим, что ваш класс предоставляет базовую гарантию исключений. Какие выводы мы можем из этого сделать? Ну сохранены инварианты, а значения-то какие будут у полей?
В том-то и дело, что конкретные значения неизвестны. И это сильно ограничивает практическое использование таких объектов. По сути единственное, что с ним можно гарантировано безопасно сделать - это разрушить.
И это главное. Вся магия с раскруткой стека и вызовом деструктором локальных объектов работает только если деструкторы вызываются безопасно. А для этого объект должен быть в валидном, но необязательно определенном, состоянии. То есть вы в принципе не можете восстановить работоспособность приложения, если ваши инструменты не предоставляют хотя бы базовую гарантию.
Здесь кстати можно провести параллель с мувнутыми объектами: ими тоже особо не попользуешься и по хорошему их надо просто удалить.
Примером предоставления только базовой гарантии может быть использование какой-нибудь базы данных. Если при выполнении запроса фреймворк выкинул исключение, например потому что соединение отвалилось, то объект для работы с базой остался в валидном состоянии, но нет никакой информации о том, выполнился запрос или нет:
auto db = std::make_shared<DBConnection>(credentials);
try {
auto result = db->Execute("UPDATE ...");
process(result);
} catch (std::exception& ex) {
std::cout << "We can cannot rely on table state and must retry with that in mind" << std::endl;
// retry
}
Поэтому мы должны иметь ввиду это неопределенное состояние коннекшена и базы при выполнении ретраев.
Provide guarantees. Stay cool.
#cppcore
👍11❤9🔥4😁3👎2
Строгая гарантия исключений
#новичкам
Базовая гарантия - это конечно хорошо, наше приложение будет корректно работать, даже если что-то пойдет не так. Но иногда этого недостаточно. Иногда нам нужно, чтобы ошибка операции вообще никак не повлияла на текущее состояние системы. Либо операция выполнилась и все хорошо, либо она бросила исключение, но после его отлова система находится в том же состоянии, что и до выполнения операции.
Такое свойство операций называется транзакционность. Транзакция может либо выполниться полностью, либо все результаты промежуточных операций в ней откатываются до состояния до начала исполнения транзакции.
Это важно, когда ваша операция требует выполнения нескольких промежуточных операций, постепенно меняющих систему. Если остановиться посередине, то уже невозможно или очень сложно будет восстановить консистентность данных.
Давайте перепишем оператор присваивания класс IntArray из предыдущего поста так, чтобы он предоставлял строгую гарантию:
В этот раз мы ничего не изменяем в самом объекте до тех пор, пока не выделим новый буфер и не скопируем туда элементы
Хрестоматийный пример из стандартной библиотеки - вектор с его методом push_back. Если у типа есть небросающий перемещающий конструктор, то метод предоставляет строгую гарантию. Вот примерно как это работает:
в хэлпере reallocate используется std::move_if_noexcept, который условно кастит в rvalue ссылке, если мув конструктор noexcept. И только в этом случае можно предоставить строгую гарантию: если вы уже повредили один из исходных объектов, его уже никак не восстановить. А безопасное перемещение элементов гарантирует готовый к использованию новый расширенный буфер.
Be strong. Stay cool.
#cppcore
#новичкам
Базовая гарантия - это конечно хорошо, наше приложение будет корректно работать, даже если что-то пойдет не так. Но иногда этого недостаточно. Иногда нам нужно, чтобы ошибка операции вообще никак не повлияла на текущее состояние системы. Либо операция выполнилась и все хорошо, либо она бросила исключение, но после его отлова система находится в том же состоянии, что и до выполнения операции.
Такое свойство операций называется транзакционность. Транзакция может либо выполниться полностью, либо все результаты промежуточных операций в ней откатываются до состояния до начала исполнения транзакции.
Это важно, когда ваша операция требует выполнения нескольких промежуточных операций, постепенно меняющих систему. Если остановиться посередине, то уже невозможно или очень сложно будет восстановить консистентность данных.
Давайте перепишем оператор присваивания класс IntArray из предыдущего поста так, чтобы он предоставлял строгую гарантию:
class IntArray {
int *array;
std::size_t nElems;
public:
// ...
~IntArray() { delete[] array; }
IntArray(const IntArray &that); // nontrivial copy constructor
IntArray &operator=(const IntArray &rhs) {
int *tmp = nullptr;
if (rhs.nElems) {
tmp = new int[rhs.nElems];
std::memcpy(tmp, rhs.array, rhs.nElems * sizeof(*array));
}
delete[] array;
array = tmp;
nElems = rhs.nElems;
return *this;
}
// ...
};В этот раз мы ничего не изменяем в самом объекте до тех пор, пока не выделим новый буфер и не скопируем туда элементы
rhs. И только после этого выполняем обновление самого объекта с помощью небросающих инструкций.Хрестоматийный пример из стандартной библиотеки - вектор с его методом push_back. Если у типа есть небросающий перемещающий конструктор, то метод предоставляет строгую гарантию. Вот примерно как это работает:
template <typename T>
class vector {
private:
T *data = nullptr;
size_t size = 0;
size_t capacity = 0;
void reallocate(size_t new_capacity) {
// allocate memory
T *new_data =
static_cast<T *>(::operator new(new_capacity * sizeof(T)));
size_t new_size = 0;
try {
// Move or copy elements
for (size_t i = 0; i < size; ++i) {
new (new_data + new_size) T(std::move_if_noexcept(data[i]));
++new_size;
}
} catch (...) {
// Rollback in case of exception
for (size_t i = 0; i < new_size; ++i) {
new_data[i].~T();
}
::operator delete(new_data);
throw;
}
// cleanup
// ...
}
public:
void push_back(const T &value) {
if (size >= capacity) {
size_t new_capacity = capacity == 0 ? 1 : capacity * 2;
// save for rollback
T *old_data = data;
size_t old_size = size;
size_t old_capacity = capacity;
try {
reallocate(new_capacity);
} catch (...) {
// restore
data = old_data;
size = old_size;
capacity = old_capacity;
throw;
}
}
// actually insert element
// ...
}
};
в хэлпере reallocate используется std::move_if_noexcept, который условно кастит в rvalue ссылке, если мув конструктор noexcept. И только в этом случае можно предоставить строгую гарантию: если вы уже повредили один из исходных объектов, его уже никак не восстановить. А безопасное перемещение элементов гарантирует готовый к использованию новый расширенный буфер.
Be strong. Stay cool.
#cppcore
1❤14🔥8👍7👎3
Гарантия отсутствия исключений
#новичкам
Переходим к самой сильной гарантии - отсутствие исключений.
В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.
Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.
Но если такой термин есть, значит такие гарантии можно предоставлять для отдельных сущностей. Давайте как раз об этих сущностях и поговорим.
Но для начала проясним термины.
Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.
nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.
Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).
nofail же подразумевает полное отсутствие ошибок. nofail гарантия ожидается от std::swap, мув-конструкторов классов и других функций с помощью которых достигается строгая гарантия исключений.
Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.
nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.
В языке в целом эти гарантии обеспечиваются ключевым словом noexcept. При появлении этой нотации компилятор понимает, что для этой функций не нужно генерировать дополнительный код, необходимый для обработки исключений. Но у этого есть своя цена: если из noexcept функции вылетит исключение, то сразу же без разговоров вызовется std::terminate.
Provide guarantees. Stay cool.
#cppcore #cpp11
#новичкам
Переходим к самой сильной гарантии - отсутствие исключений.
В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.
Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.
Но если такой термин есть, значит такие гарантии можно предоставлять для отдельных сущностей. Давайте как раз об этих сущностях и поговорим.
Но для начала проясним термины.
Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.
nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.
Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).
nofail же подразумевает полное отсутствие ошибок. nofail гарантия ожидается от std::swap, мув-конструкторов классов и других функций с помощью которых достигается строгая гарантия исключений.
Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.
nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.
В языке в целом эти гарантии обеспечиваются ключевым словом noexcept. При появлении этой нотации компилятор понимает, что для этой функций не нужно генерировать дополнительный код, необходимый для обработки исключений. Но у этого есть своя цена: если из noexcept функции вылетит исключение, то сразу же без разговоров вызовется std::terminate.
Provide guarantees. Stay cool.
#cppcore #cpp11
❤15👍8🔥5❤🔥3😁3👎1