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

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

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

Рады приветствовать всех на нашем канале!
Вы устали от скучного, монотонного, обезличенного контента по плюсам?

Тогда мы идем к вам!

Здесь не будет бесполезных 30 IQ постов, сгенеренных ChatGPT, накрученных подписчиков и активности.

Канал ведут два сеньора, Денис и Владимир, которые искренне хотят делится своими знаниями по С++ и создать самое уютное коммьюнити позитивных прогеров в телеге!
(ну вы поняли, да? с++, плюс плюс, плюс типа
позитивный?.. ай ладно)

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

Материалы для новичка

ГАЙДЫ:

Мини-гайд по собеседованиям
Гайд по тестовым заданиям
Гайд по категория выражения и мув-семантике
Гайд по inline

Дальше пойдет список хэштегов, которыми вы можете пользоваться для более удобной навигации по каналу и для быстрого поиска группы постов по интересующей теме:
#algorithms
#datastructures
#cppcore
#stl
#goodoldc
#cpp11
#cpp14
#cpp17
#cpp20
#commercial
#net
#database
#hardcore
#memory
#goodpractice
#howitworks
#NONSTANDARD
#interview
#digest
#OS
#tools
#optimization
#performance
#fun
#compiler
#design
#exception
#guide
#задачки
#base
#quiz
#concurrency
#ЧЗХ
#ревью
🔥3719👍15🤔2🐳1
Дублирование - зло. Ч2

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

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

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

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

Если уж все таки было принято решение избавляться от клонов, то следует в первую очередь попробовать использовать возможности среды разработки / задействовать сторонние инструменты. Например, посмотрите на SonarQube и плагин для IDEA, Eclipse, Visual Studio, Visual Studio Code и Atom — SonarLint. Дело даже не в том, что это рутинная работа, которая может быть автоматизирована. Программный поиск даст возможность быстро провести разведку и легко оценить ситуацию в вашем проекте. Это сильно ускорит анализ, сократит рутину и снизит риски найти на поздних этапах какой-то исключительный клон, меняющий правила обобщения кода.

Надеюсь, что мне удалось убедить вас в злостности и неоднозначности проблемы дублирования 😉 Эта статья мне пригодится для следующих постов, так что если остались вопросы - пишите комменты!

#design #goodpractice #tools
👍102🔥1
Терминал

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

Хочу поделиться с вами похожим приколом только из мира computer science. Думаю, что все мы хоть раз в жизни открывали графический терминал на своих Unix системах(реальных или виртуальных), ну или хотя бы подключались удалённо к ним. Все-таки, знание команд для unix - это маст хэв и де факто стандарт для сферы разработки. Если вы хоть раз разрабатывали не локально, то с 99% вероятности вы подключались к Линукс системе и ей надо бы уметь управлять.

Ну дак вот. Помните, какие раньше были компьютеры? Я вот тоже не помню, потому застал время уже полностью персональных компьютеров, где все было соединено вместе. А лет 50 назад нормальной практикой в компании было иметь один здоровый ЭВМ, размером с самомнение веганов, и много-много отдельных «терминалов», через которые сотрудники могли общаться с эвм. Они имели клавиатуру, дисплей, печатающее устройство, динамик и ещё пару простых прибамбасов. Пользователь вводит команду, команда по проводам попадает в эвм, обрабатывается и передаётся в виде текстовой или графической информации на терминал.

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

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

Stay surprised. Stay cool.

#fun #tools #OS
👍6🤔32🔥2
​​Конфигурация и переменные окружения
#опытным

Любой серьезный сервис нуждается в конфигурации. Файлы конфигурации (JSON, YAML, INI) — популярный способ хранения настроек приложений. Так параметры можно хранить в репозитории, версионировать, да и просто удобно, когда все можно менять в одном месте и никак не менять команду запуска.

Однако не одними конфигами едины. Не всегда они подходят для решения определенных задач.

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

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

{
"data_key": "qwerty123" // Утечка при публикации кода!
}


Да и хранить ключ в открытом виде в файле на сервере такое себе. А если кто-нибудь подглядит?

То же самое можно сказать про креды базы данных, in-memory кэша, брокеров сообщений и прочего. Пароли могут быть скомпрометированы.

# config.yml (попадает в Git)
db:
host: db.example.com
username: admin
password: "P@ssw0rd123!" # Утечка при публикации кода!


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

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

Переменные окружения можно установить видимыми только для конкретного запущенного docker контейнера:

docker run -e MY_VAR=value my_image


В k8s можно брать переменые окружения из отдельно развернутого и защищенного Vault. В этом случае вообще отсутсвует явное указание секрета:

env:
- name: MY_VAR
- value: vault:my_group/my_service#my_var


Переменные окружения не попадают в репозиторий -> нет компрометации секретов.

Можно без изменения конфига на одном и том же сервере тестировать приложение в разных контурах:

# Local
export DB_HOST=localhost

# Dev
export DB_HOST=dev-db.example.com


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

К чему это я и причем здесь С++?

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

Protect your secrets. Stay cool.

#goodpractice #tools
22👍2515🔥8💯4❤‍🔥2
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
​​Распределенные компиляторы
#опытным

Как только ваш проект достигает определенного размера, время компиляции начинает становиться проблемой. В моей скромной практике были проекты, которые полностью собирались с 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
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
​​Тулзы для поиска проблем многопоточности
#опытным

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

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

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

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

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

Надо лишь установить сам cppcheck, а запускается он просто:

cppcheck --enable=all --inconclusive thread_app.cpp


Thread San. Без динамического анализа в многопоточке никуда. ThreadSanitizer - это детектор гонок данных для C/C++. Санитайзер определяет гонку ровно как в стандарте: если у вас много потоков получают доступ к ячейке памяти и хотя бы один из них - несинхронизированная запись. И это же и является принципом детектирования гонок.

Работает на GCC и Clang. Достаточно лишь при сборке указать нужные флаги и ждать прилета сообщений о багах:

clang++ -fsanitize=thread -g -O2 -o my_app main.cpp

g++ -fsanitize=thread -g -O2 -o my_app main.cpp


Helgrind. Это одна из тулзов Valgrind'а, работающая конкретно с багами многопоточности. Достаточно при запуске валгринда указать --tool=helgrind и ждите писем счастья. Главное, чтобы ваши примитивы синхронизации использовали под капотом pthread.

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

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

vtune -collect threading -result-dir my_analysis ./my_application


VTune в принципе очень мощный профилировщик даже не касательно многопоточности. Если есть возможность заморочится с ним, то это стоит сделать.

Test your system. Stay cool.

#concurrency #tools
122👍11🔥5😁1
Увидел тут в одной запрещенной сети такой пост с картинкой выше:

Пожалуй, брошу еще один камень в огород любителей длинных строк в коде.  

На скриншоте первый фрагмент -- это оригинальный код, а второй -- это как бы я его записал. ИМХО, разница очевидна и она не в пользу оригинального 😎

Если же попытаться говорить объективно, то с кодом должно быть комфортно работать в любых условиях. Хоть на 13.3" ноутбуке, хоть на 34" 5K дисплее. А длинные строки этому физически препятствуют.
...


Кажется, что людям свойственно обсуждать давно решенные проблемы😅

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

Я конечно не эксперт, но кажется, что любые вопросы по форматированию решаются настройкой clang-format. Надо его просто установить, поставить нужные правила(вот здесь можете один раз похоливарить всей командой, но один раз!) и радоваться жизни. Для vscode можно поставить расширение и настроить его, чтобы форматирование применялось на каждое сохранение файла. Ну или используйте любой другой линтер на ваш вкус.

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

А вы как считаете: разница очевидна и она не в пользу оригинального?😆

Don't reinvent the wheel. Stay cool.

#tools #goodpractice
🔥17👍1210😁2🤔2
​​Что не так с модулями?
#опытным

Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.

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

Если по простому, то модуль - это такой бинарный черный ящик, у которого четко определен интерфейс, который он экспортирует наружу.

Экспортируемые сущности явно помечаются в коде модуля. Затем модуль компилируется и из бинарного его представления можно дергать только эти экспортируемые сущности.

Короткий пример:

// math.cppm - файл модуля
export module math; // Объявление модуля

import <vector>; // Импорт, а не включение

// Макросы НЕ экспортируются!
#define PI 3.14159

// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);

// Внутренние функции скрыты
void internal_helper();


и его использование:

// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода

// Используем экспортированную функцию
double area = calculate_circle_area(10);

// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются


Модули призваны решать следующие проблемы:

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

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

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

На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...

А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?

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

😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.

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

😡 Очень много усилий нужно потратить на переработку архитектуры и кода существующих проектов, чтобы перевести их на модули.

😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.

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

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

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

Use new features. Stay cool.

#cppcore #compiler #tools
16👍7🔥6