Telegram Group Search
#madskillz

Итераторы с неопределенным концом 🏁

Итераторы это одна из главных концепций C++. У каждого класса контейнера (set/vector/list/...) в C++ есть свой соответствующий класс итератора (для доступа к своим данным).

Класс, для которого определен итератор, должен иметь методы begin() и end(), по вызову которых отдаются объекты итератора.
Класс итератора должен иметь методы operator++() и operator*().
Наличия этих методов достаточно для использования итератора в разных стандартных методах и в range-based for loop.

Обычно итераторы итерируются по всем объектам от begin() до end():
    const std::vector<int> vec{1, 3, 5, 7, 9};
// внизу аналог выражения `for (int value : vec) { /* ... */ }`
auto __begin = vec.begin();
auto __end = vec.end();
for ( ; __begin != __end; ++__begin) {
int value = *__begin;
/* do something with `value`... */
}

Однако бывают случаи, когда end() нельзя вычислить заранее и нужно делать на каждом шагу проверку, не пора ли выходить из цикла. В стандарте C++20 встречается такой костыль:
1️⃣ Завести пустой мусорный класс std::default_sentinel_t
2️⃣ Метод end() класса-"контейнера" должен отдавать объект мусорного класса
    std::default_sentinel_t end() { 
return {};
}
(а метод begin() продолжает отдавать объект итератора)
3️⃣ Класс итератора должен определить оператор сравнения с объектом мусорного класса:
    bool operator==(std::default_sentinel_t) const { 
return /* какое-то условие */;
}

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

Какие есть реально используемые use cases:
🎯 std::counted_iterator - обертка над каким-нибудь другим итератором, итерируется по не более чем N элементам
🎯 Поддержка range-based for для корутин - чтобы можно было в цикле забирать новые значения от корутины, пока она активна (класс итератора - class Iter)
#compiler

Как компилятор замеряет скорость компиляции? 🕰

В C++ легко сделать так, чтобы проект собирался очень долго. В моем личном топе - юнит-тест (в виде одного .cpp-файла) компилировался 4.5 минуты.

К счастью, скорость компиляции можно дебажить. Во время компиляции одного файла нужно указать настройку -ftime-trace:
-ftime-trace
Turn on time profiler. Generates JSON file based on output filename. Results can be analyzed with chrome://tracing or Speedscope App for flamegraph visualization.
-ftime-trace-granularity=<arg>
Minimum time granularity (in microseconds) traced by time profiler

Команда может выглядеть так:
clang++ main.cpp -c -ftime-trace -ftime-trace-granularity=50

Получившийся json-файл main.json можно визуализовать на 🔬SpeedScope. (На гитхабе есть гифка, как это примерно выглядит)

Что делает компилятор:
⚙️ Перед началом компиляции, если задана настройка -ftime-trace, clang вызовет метод llvm::timeTraceProfilerInitialize.
⚙️ В этом методе проинициализируется объект структуры llvm::TimeTraceProfiler.
⚙️ Когда начинается какое-то событие, нужно вызвать метод llvm::TimeTraceProfiler::begin, чтобы запомнить время начала.
⚙️ Когда событие заканчивается, нужно вызвать метод llvm::TimeTraceProfiler::end, чтобы добавить запись о событии.
⚙️ Как видно по коду, используется стек, потому что события вложены друг в друга (например внутри события "компиляция файла" есть событие "распарсить класс").
⚙️ После компиляции файла вызывается метод llvm::TimeTraceProfiler::write для записи в json-файл.

По умолчанию параметр -ftime-trace-granularity равен 500 (500 микросекунд). Записываются не все события, а только достаточно "долгие", которые длились дольше чем 500μs - участок кода.

В коде нужные методы не вызывают "вручную" - используется стандартная идиома RAII в виде структуры llvm::TimeTraceScope.
Как видно, в момент вызова конструктора "событие начинается", вызова деструктора "событие заканчивается".
(если компиляция вызывалась без флага -ftime-trace, то этот объект не делает ничего)

Можно привести примеры - вот так засекается время на инстанциацию шаблонов (которая происходит после парсинга файла): PerformPendingInstantiations.
Пока происходит инстанциация шаблонов, засекаются всякие "вложенные" события, например InstantiateFunction.

Вот так компилятор нехитрым образом делает нужный flame graph 🙂
По моему опыту наблюдений за скоростью компиляции, "фронтенд" компилятора (парсинг файла в AST) занимает в 3-20 раз больше времени чем "бэкенд" (перевод AST в LLVM IR, оптимизация и перевод в бинарник).
Основная причина этого дисбаланса - огромный объем исходного файла после того, как раскроются все #include (в почти всех современных проектах на C++).

На основе этих данных становится видно, что нужно поправить, чтобы ускорить компиляцию. А впрочем, это уже совсем другая история...
#compiler #advice

Не компилируйте шаблонный код каждый раз ⌨️

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

Я использую собственный индикатор: если параметры шаблона в теории заранее неизвестны, то он нужен (например std::vector<T>).
Если все параметры известны (например void make_sound<TAnimalCat/TAnimalDog/TAnimalBird>()), то лучше сделать виртуальный класс IAnimal.

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

Сам шаблонный код еще ничего не делает. Только когда вызывается метод/класс с некоторыми параметрами шаблона, шаблон "инстанцируется", то есть генерируется уникальный метод/класс под эти параметры.
При компилировании каждого .cpp-файла (которых может быть сотни) мы часто вынуждены компилировать один и тот же участок кода - пример на godbolt.

Инстанцированные шаблонные методы имеют linkage type linkonce_odr, подробнее про него тут - https://www.group-telegram.com/cxx95.com/38

Чтобы избежать компиляции одного и того же кода в каждом .cpp-файле, можно под шаблоном "объявить инстанциации" для всех известных параметров через extern template - пример на godbolt.
В таком случае где-то нужно "определить инстанциации" - для примера выше нужно в условном some_header.cpp написать:
    template int calc<float>();
template int calc<double>();
Теперь код в шаблоне будет компилироваться всего один раз.

Однако можно и весь шаблонный код держать в своем .cpp-файле, часто это упрощает читаемость - пример на godbolt.

Можно оценить полезность разных подходов:
🚬 Подход с extern template является полумерой, потому что обычно выигрыш в скорости компиляции абсолютно незначительный
🤤 Подход с шаблонным кодом полностью в своем .cpp-файле неплох, улучшает читаемость кода
Please open Telegram to view this post
VIEW IN TELEGRAM
#story

Корутины для чайников (таких как я) 🫖

Когда я пытался постигнуть, как работают добавленные в C++20 корутины, я лицезрел невеликое количество понятных объяснений в интернете.
Авторы начинают вспоминать, в каком году впервые появился термин, или впутывать в дело goroutines (из языка Go), или Boost.Fiber, или подсчитывать во сколько квинтиллионов раз корутины быстрее потоков... 😑

Базовую понятную теорию о корутинах я нашел тут: Coroutine Theory. С первой строки понятно, что корутина - это функция, которая может приостанавливать и продолжать (с момента остановки) свое выполнение пример на godbolt:
TGenerator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
a = std::exchange(b, a + b);
}
}
Место для переменных a и b не возникнет из ниоткуда (а стек использовать нельзя), поэтому память под локальные переменные должна аллоцироваться в куче (компилятор должен это поддержать). После этой теории два главных факта:
✍️ Корутины - синтаксический сахар (по типу лямбд), который иногда может упрощать код. Аналог фибоначчи без корутины.
✍️ Корутины как "функции с состояниями" перпендикулярны многопоточности, они никак ее не заменяют и никак ей не мешают.

Теперь настало время узнать задачи, которые решаются корутинами лучше чем другими средствами. Можно приводить в пример http-серверы, но это слишком неочевидный пример.
На мой взгляд, один из лучших постов: How to implement action sequences and cutscenes про сложную логику в играх.
Это не на C++, а на простом скриптовом языке Lua, но не меняет концепции (и корутины там легче выглядят).

C++ как обычно пошел хардкорно-укуренным путем 🚬 Он определяет только интерфейсы для корутин, а программист должен абсолютно сам писать event loop-ы и классы для awaiter/promise.
По этой ссылке есть игрушечный event loop для задач. Без специализации в этой области ловить нечего - нужно использовать уже готовые библиотеки, например lewissbaker/cppcoro или YACLib.

Корутины - вещь неплохая, но подход к реализации в C++ (с поощрением радикального велосипедизма) меня удивил. Общее впечатление (с опытом корутин в Lua/Python) совпало с этим комментарием на Хабре.
Please open Telegram to view this post
VIEW IN TELEGRAM
#advice

std::unreachable - безопасная стрельба в ногу 🔫

В C++23 стандартизировали метод std::unreachable, у которого лютое описание: invokes undefined behavior.

(До C++23 на linux можно использовать __builtin_unreachable)

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

Пусть совершенно точно известно, что метод magic_func принимает только значения 1 или 3:
int magic_func(int value) {
switch (value) {
case 1:
return 100;
case 3:
return 500;
default:
/* ???????????? */
}
}
Нужно написать бесполезный код - что делать при значении не равном 1 или 3. Обычно делают два варианта:
    return 0; // возврат мусорного значения
throw std::exception(); // бросание мусорного исключения

Лишний код генерирует лишние инструкции - ссылка на godbolt.

Инструкция unreachable никакой семантики не имеет, и нужна чтобы показать компилятору, что данный участок кода "недостижим". Компилятор может как-то оптимизировать этот участок кода.
undefined behaviour значит, что в этом участке кода может происходить всё что захочет компилятор.

В нашем случае, если написать unreachable (ссылка на godbolt), компилятор выкинет лишнюю проверку и код станет таким:
int magic_func(int value) {
if (value == 1) {
return 100;
}
return 500;
}
Please open Telegram to view this post
VIEW IN TELEGRAM
#story

Обзор языка HolyC для TempleOS ✝️

TempleOS - операционная система, которую в течении многих лет в одиночку создавал программист Терри Дэвис. Разработка началась после психиатрической госпитализации, в ходе которой у Терри была диагностирована шизофрения. По его словам, Бог приказал ему разработать операционную систему, которая должна стать "Третьим Храмом". 🚬

Почти всю жизнь Терри был безработным, поэтому разрабатывал свою систему целыми днями, в свободное время на различных форумах толкая телеги про ЦРУ, "ниггеров", и богохульников.

Данный мусье распиарен, поклонники писали его биографию, делали ролики на ютубе. Он сам написал свой загрузчик, ядро, менеджер окон, графическую библиотеку, игры - это все на своем языке Holy C (С†) со своим компилятором.

Дизайн С† есть тут, а также тут можно увидеть примерные программы. Он похож на C с добавлением многих фичей.

Исходники компилятора читаются тяжело, но что-то понять можно.

Лексер, который разбирает исходники в токен, делает это с изменением текущего состояния в CCmpCtrl *cc, потому что разбор токенов происходит одновременно с разбором выражений.

Парсер, который разбирает выражения, сделан в виде простого рекурсивного спуска, например так выглядит парсинг if-выражения. Парсер создает блоки "промежуточного кода".

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

Из минусов компилятора можно назвать его однопроходность (из-за этого язык ближе к C, чем к C++), а также поддержка всего одной архитектуры.
А в остальном компилятор неплох, видно что автор был неординарным программистом, чтобы в одиночку писать все программы такого уровня (включая операционку).
Please open Telegram to view this post
VIEW IN TELEGRAM
В последнее время я ничего не писал - решил написать на тему разработки в тонком клиенте 😎

#story

Тонкий клиент для разработки на C++ - с картинками! 😱

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

На примере Clang/LLVM: по воспоминаниям старожилов, ~5 лет назад его весь можно было построить в Debug-режиме на стандартном компьютере. Сейчас это невозможно, так как объем занимаемой памяти при линковке бинарника часто пробивает порог в 16гб RAM.
Приходится иметь билд в режиме Release или RelWithDebInfo - с этими режимами почти нереально дебажить, приходится ставить много дебажных выводов (как cerr() << Expr->size()), и сложно что-либо серьезное сделать. 😏

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

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

Личный виртуальный сервер можно создать много где - Yandex Cloud, Google Cloud, AWS, Microsoft Azure; в зависимости от преимуществ, средств (,и санкционного режима). Сейчас у меня машина на Yandex Cloud, с такими ресурсами:
🔍 ресурсы виртуальной машины
Там процессор Intel Cascade Lake, 32 ядра, 32гб RAM, 200гб SSD.
Сейчас это стоит ~11 540 рублей в месяц (на сайтах есть калькуляторы)

Этих ресурсов достаточно для всего - например весь Clang с нуля можно сбилдить за 13 минут (это во много раз быстрее чем на локальном ноутбуке), и дебажить через gdb/lldb.
Для системы можно выбрать любую ОС любой версии, это тоже в сотни раз быстрее, чем обновлять/ставить их вручную
🔍 список ОС

Зайти с локального ноутбука на виртуалку можно через SSH
🔍 кусок .ssh/config и вход в виртуалку
в моем случае по команде ssh -A mango

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

Для написания кода я использую Neovim (форк Vim) - но это дело привычки. Многие коллеги используют Visual Studio Code, он умеет подключаться к виртуалкам по SSH, и просто более привычен для большинства.
🔍 neovim (у меня включены контекстные подсказки и автокомплит)

Для дебага я использую LLDB (аналог дебаггера GDB)
🔍 lldb в процессе

Система контроля версий тоже только изнутри виртуалки
🔍 git в виртуалке

Таким образом можно в процессе работы совсем не вылезать за пределы мощной виртуалки, а ноутбук иметь средней мощности. Я оптимизирую параметры ноутбука по легкости, чтобы он почти не чувствовался в рюкзаке.
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler

clang-format: царь-костыль 🩼

Я раньше писал про clang-tidy в этом блоге. Он нужен для проверки кода на качество. У него модульный вид - каждый может написать свою проверку и использовать множество независимых друг от друга проверок. Он работает на уровне AST, то есть код проходит лексический и синтаксический анализ до тулзы.

А clang-format это еще одна тулза, нужная для форматирования исходного кода - чтобы было нужное количество пробелов, отсортированные #include и прочее. Он работает на уровне токенов, то есть код проходит только лексический анализ до тулзы.

То есть clang-format очень приблизительно понимает, что перед ним за токен и что нужно сделать. Например, текст
class A: B {
ему это видится как последовательность токенов
(kw_class) (identifier) (colon) (identifier) (l_brace)

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

Например, в какой-то момент в середине работы вызывается метод WhitespaceManager::generateReplacements, который поправляет пробелы, и в нем внутри метод WhitespaceManager::alignArrayInitializers, чтобы поправить пробелы в массивах.

Совсем без семантики форматировать сложно, поэтому clang-format перед форматированием "аннотирует" токены дополнительными данными: сопоставляет каждому Token структуру FormatToken.
Там есть всякие поля, как bool IsArrayInitializer (то что этот токен - начало array initialization);
или FormatToken *MatchingParen (ссылка на закрывающую скобку).

Работает все при таком подходе очень хреново 😣. Из стандартных ошибок - ставит много где лишние пробелы или портит лямбды.

Есть куча issue про clang-format, а чинить их значительно сложнее чем issue для clang-tidy.
Если в clang-tidy область потенциальных правок - код отдельной проверки (максимум несколько сотен строк), то в clang-format это весь clang-format.

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

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

Поэтому старайтесь делать модульные программы, чтобы уменьшить область потенциальных правок при починке бага 😎
Please open Telegram to view this post
VIEW IN TELEGRAM
#madskillz

Простой switch для строк 🎲

В C++ в switch-выражении в case не могут находиться строки или строковые литералы.

Там могут быть только константные значения целочисленного типа или значения enum-ов (а enum это и есть целочисленный тип под прикрытием).

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

Понятно, что для строк branch table сделать нельзя, и эффективность switch-а не будет отличаться от кучи if-ов.

В других языках строки в switch возможны - Java 7, C# 6, но там нет упора на максимальную производительность.

Но можно сделать самописный простой "switch", чтобы упростить такой код:
    Color color = UnknownColor;
if (argv[i] == "red") {
color = Red;
} else if (argv[i] == "orange") {
color = Orange;
} else if (argv[i] == "yellow") {
color = Yellow;
} else if (argv[i] == "green") {
color = Green;
} else if (argv[i] == "violet" || argv[i] == "purple") {
color = Violet;
}
В такой:
    Color color = StringSwitch<Color>(argv[i])
.Case("red", Red)
.Case("orange", Orange)
.Case("yellow", Yellow)
.Case("green", Green)
.Cases("violet", "purple", Violet)
.Default(UnknownColor);

Реализация StringSwitch есть в llvm: StringSwitch.h

Внутри этого класса всего два поля:
1️⃣ std::string_view str - сравниваемая строка (в нашем примере argv[i])
2️⃣ std::optional<T> value - итоговое значение (в нашем случае T = Color)

При вызове метода Case, если value еще не заполнено и строка равна нужной, то value заполняется.
Есть методы EndsWith и StartsWith, которые заполнят value, если часть строки равна нужной.
Есть аналогичные case-insensitive методы, а также методы Cases для нескольких значений.
Наконец есть оператор приведения к нужному типу (в нашем случае к Color).

На мой взгляд, можно еще сделать класс LambdaSwitch, который в отличие от StringSwitch мог бы принимать лямбды, и ставить значение, если лямбда возвращает true. 😐
Please open Telegram to view this post
VIEW IN TELEGRAM
#retro #books #compiler

Рассказ "Редкая профессия" (1997 г.) 📖

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

Рассказ о том, как команда из двух программистов занималась уникальной работой - созданием компилятора C++ в конце 90-х годов по заказу иностранной компании.

Читается легко - произведение передает дух времени, описывает трудности взаимодействия с забугорной конторой, денежный вопрос, проблемы менеджмента, общие вопросы разработки компиляторов и языка C++.

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

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

С компиляторами произошла та же история, что с веб-браузерами, операционными системами, игровыми консолями - раньше их было десятки и их мог сделать кто угодно на коленке, но усложнение сферы оставило лишь единицы выживших.
Please open Telegram to view this post
VIEW IN TELEGRAM
#books

Обзор книги "Team Geek" (2014 г.) 📚

(можно скачать PDF тут)

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

Это книга не про C++ и даже не совсем про разработку, но она может помочь работать в команде более эффективно. Она разбирает типичные поведенческие особенности программистов, правила и принципы успешных команд, и многое другое в корпоративном мире. 😔

Книга толковая, нет инфоцыганщины. Она разбита на микро-главы, некоторые из них оказались лютой жизой, с которой я сам сталкивался 😱

✏️ Efficient Meetings
Чтобы митинги (как "необходимое зло" для многих программистов) не были бесполезными, лучше соблюдать написанные кровью законы:
1) На встрече для дизайна чего-то нового желательно иметь не более 5 человек, с бОльшим числом людей проводить ее сложно.
2) Если есть ежедневные митинги (стендапы), где выступает каждый член команды, они должны быть не длиннее 15 минут.
3) Митинг выбивает контекст работы, поэтому их желательно ставить возле interrupt point: до/после обеда, под конец дня и т.д.

✏️ Working in a “Geographically Challenged” Team
Цитата: "If the discussion didn’t happen on the email list, then it never really happened."

✏️ Code Comments
Цитата: "Comments should be focused on WHY the code is doing what it’s doing, not WHAT the code is doing."

✏️ Be a Catalyst
Цитата: "In many cases, knowing the right people is more valuable than knowing the right answer" - про контакты, которые разработчик должен иметь с другими разработчиками в корпорации.

✏️ Be Honest
Глава про важность конструктивного и понятного фидбека, в том числе про бесполезность сэндвича с говном. Многие люди просто не понимают или не так хорошо читают между строк, поэтому нужна прямота, если в коллеге что-то нужно изменить.

✏️ The Office Politician
Глава про офисных кидал среди коллег, которым нельзя доверяться, чтобы они не выезжали за твой счет и присваивали твои достижения.

✏️ The Bad Organization
Во многих компаниях главный продукт не софт, а что-то другое (банки, еда, одежда...), и программисты там просто вспомогательный персонал. В таких случаях резко выше риск, что с процессами в компании жопа - в главе есть общие признаки.

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

✏️ Learn to Manage Upward
Самая лучшая глава - про визибилити. Чтобы расти вверх, нужно показывать ощутимые всеми вокруг результаты в виде графиков, запусков проектов в прод, и так далее.

Вся работа делится на два сорта - offensive (результаты имеют визибилити, их можно показать) и defensive (рефакторинг/переписывание/миграция БД/техдолг/метрики - это делать жизненно необходимо, но визибилити нулевой).

Хотя без defensive работы не обойтись никак, она не дает никаких очков во время ревью. Цитата:
A team should never spend more than one-third to one-half of its time and energy on defensive work, no matter how much technical debt there is. Any more time spent is a recipe for political suicide.
Я бы дописал в главу это: Если вы все время занимаетесь только defensive работой, которая очевидно никого не впечатлит на ревью, то поздравляю - вас назначили лохом!
Please open Telegram to view this post
VIEW IN TELEGRAM
#story

Ускорение компиляции на C++ 🏃

Очень боянистая тема "ускорение компиляции" - это специальный вид спорта, в котором принимали участие многие разработчики на C++, которым не нравится медленная сборка. Я перечислю основные направления этого спорта.😃

Основным источником проблем является "N*M problem", почти все подходы стараются избежать именно его. Это значит, что если в проекте есть N хидеров и M не-хидеров, то суммарное количество распарсенных файлов в процессе компиляции будет стремиться к N*M, потому что каждый не-хидер компилируется отдельно и транзитивно подключает почти все хидеры, если за этим не следить.
Суммарный размер всех подключенных хидеров может достигать сотен тысяч строк.

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

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

Минусы:
Объект нужного класса придется хранить в куче. Но есть сильное колдунство Fast PImpl (https://www.group-telegram.com/cxx95.com/27), которое впрочем имеет свои минусы.
Сложно поддерживать, нужна сила воли.

💪💪 Jumbo build (он же Unity build, он же Single Translation Unit)
Можно почитать тут. Unity-билд это подход, когда все файлы модуля строятся за один присест. Это значит что если у нас есть файлы "aaa.cpp", "bbb.cpp", ..., "zzz.cpp", то создается (желательно автоматически) файл который их всех подключает:
/* all.cpp */
#include "aaa.cpp"
#include "bbb.cpp"
// ...
#include "zzz.cpp"
И билдится только файл "all.cpp". Смысл в том, что если 90%+ времени компиляции занимает не сам файл, а хидеры которые их подключают, то такой подход почти линейно ускорит компиляцию.

Минусы:
Будут конфликтовать имена статических методов и переменных
Вообще это какая-то дичь и лечит симптомы, а не причины

💪💪 Precompiled headers
Можно почитать тут. Если у нас есть какие-то очень редко меняющиеся хидеры, то их можно "прекомпилировать".
Компилятор Clang делает это так - читает хидеры (например test.h), анализирует и сохраняет в типа уже "распарсенном" формате (например в test.h.pch).
Смысл в том, что если в коде где-то есть инклюд хидера test.h, то компилятор не будет его парсить с нуля, а прочитает test.h.pch, что будет типа быстрее, так как там уже готовое синтаксическое дерево файла.

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

💪💪 Include What You Use
Это тулза для удаления лишних инклюдов. Есть хорошая документация. Иногда бывают баги связанные с тем что математически доказать ненужность инклюда нельзя - они все неявно влияют друг на друга.

💪💪 Быстрая виртуалка
Ускорять компиляцию можно не только руками. Здесь я описал, как можно использовать виртуалки с большими ресурсами для разработки - https://www.group-telegram.com/cxx95.com/60

💪💪 Распределенная сборка
В крупных компаниях с крупными проектами используется распределенная сборка - что-нибудь готовое (distcc) или даже свое (недавняя статья от VK).
Эта большая тема, в статье круто описано как выглядит распределенная сборка 😃 Кроме того, чтобы не собирать всякие объектники по многу раз, можно использовать кэши - если твой коллега уже запускал сборку и ждал результатов, то тебе ждать не придется.

💪💪 Новый модный линкер mold
С линкерами в принципе проблем нет, но если у вас бинарь с Debug-символами в несколько гигабайт, то он может линковать вечность, по 2-3-10 минут.
Сейчас разрабатывается новый линкер mold, который типа быстрее других линкеров, но про него нет внятной документации и кое-кто жалуется на нестабильность и поломанные бинари.
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler #books

Обзор книги "Language Implementation Patterns" (2010 г.) 📚

(можно посмотреть тут - https://pragprog.com/titles/tpdsl/language-implementation-patterns/)

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

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

Книга Language Implementation Patterns супер информативная и содержит real-life теорию с кодом для реализации языковых тулз.
В ней разбираются всякие примеры как: интерпретатор байткода, статический анализатор кода, компилятор C/C++ (урезанный) и многое другое.

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

Паттерны разделены на 4 части: чтение ввода (I), анализ ввода (II), интерпретация ввода (III), генерация вывода (IV).
Самые простые приложения используют только I, сложные используют I+II+III или I+II+IV.
Для примера с моего старого поста (https://www.group-telegram.com/cxx95.com/40): часть I объясняет "лексический анализ", часть II "синтаксический анализ", часть IV "кодогенерацию".

В книге не изобретаются велосипеды, а сразу дается правильная архитектура или вспомогательная тулза 🙂
Например, вся часть I может быть покрыта ANTLR - генератором парсеров (а автор книги - разработчик ANTLR), и свой парсер не придется писать.

Это экономит много времени, потому что у человекочитаемой грамматики (например в форме БНФ) могут быть супер неочевидные правила разбора, и надо самому вычислять множества FIRST и FOLLOW, а этому посвящается университетский курс... Генератор парсеров все делает за программиста по описанию грамматики.

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

Какие есть комментарии к книге:
🤔 Надо помнить, что разрабатываемый в книге примерный "компилятор для C++" делается для урезанного языка. В реальном мире анализатор для C++ это не LL(1) или LL(*), а скорее LL(k), где k - размер файла...
🤔 Весь код в книге написан на Java (ANTLR тоже на нем), возможно кому-то не понравится этот язык.
🤔 В одном из паттернов есть крохоборство для "буфера токенов" - прочитанные из файла токены выкидываются из памяти. Но даже в C++, где после препроцессора файлы могут быть в миллионы строк (и в N раз больше токенов) всё хранится в памяти.
🤔 В книге часто рекламируется генератор парсеров ANTLR, но для ряда сложных задач он выглядит трешово и намного более непонятным чем если бы это делалось в коде. Пример - когда "оптимизируем" код в языке для записи операция с векторами:
4 * [0, 5*0, 3] -> [4*0, 4*5*0, 4*3] -> [0, 0, 4*3]
предлагается сделать это прямо в конфиге ANTLR:
scalarVectorMult : ^('*' INT ^(VEC (e+=.)+)) -> ^(VEC ^('*' INT $e)+) ;
zeroX : ^('*' a=INT b=INT {$a.int==0}?) -> $a ; // 0*x -> 0
xZero : ^('*' a=INT b=INT {$b.int==0}?) -> $b ; // x*0 -> 0
🤔 В книге совсем не описана генерация в машинный код и сложные оптимизации. Это отдельная наука и большинству людей никогда не пригодятся эти знания. Но надо иметь в виду, что книга грубо говоря показывает как код на С++ переводится в LLVM IR.
Буквально на последней странице упоминается LLVM и рекомендуется его использование для компилируемых языков, так как там уже есть все для этого.
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy

Миф о виртуальных деструкторах 🍅

На собеседованиях и в реальной жизни часто встречается вопрос: "Зачем нужен виртуальный деструктор?"

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

Но есть логичный вопрос: почему бы тогда компилятору не генерировать виртуальный деструктор автоматически (для классов с виртуальными методами)? 🤔
Ведь он кучу всего генерирует сам (например move- и copy-конструкторы, move- и copy-операторы присваивания).

Ответ: Потому что утверждение выше неправильное! 😱 Виртуальный деструктор нужен только тогда, когда есть риск, что объект класса-наследника могут разрушить по указателю на класс-предок.

Пусть есть базовый виртуальный класс DrinkMachine и его наследник класс CoffeeMachine.
Каждый объект программы рано или поздно надо разрушить, это делается в 2х разных случаях:
1️⃣ Объект создан на стеке, тогда программа сама вызовет деструктор
{
CoffeeMachine cm;
// перед выходом из scope сам вызывается cm.~CoffeeMachine()
}
В этом случае неважно какой деструктор (виртуальный или нет), потому что в обоих случаях вызовется то что нужно - деструктор реального объекта.

2️⃣ Объект создан в куче, тогда программист сам делает разрушение объекта, обычно так:
DrinkMachine* dm = ...; // возможно вместо `...` здесь был `new CoffeeMachine()`
// ...
delete dm;
(или если у нас объект по типу std::unique_ptr<DrinkMachine> - происходит то же самое)

Оператор delete обычно делает две вещи: вызывает деструктор и освобождает память:
dm->~DrinkMachine();
std::free(dm);

В этом случае важно чтобы вызвался именно деструктор реального объекта, т.е. возможно мы на самом деле хотели бы вызвать ~CoffeeMachine(). В этом случае нужен виртуальный деструктор - он будет лежать во vtable, и как метод будет находиться динамически.

Если второго случая в программе не бывает, то виртуальный деструктор не нужен - например в этой программе все работает без ошибок:
void MakeDrink(DrinkMachine& dm) {
dm.MakeDrink(); // вызов виртуального метода
}
void MakeCoffee() {
CoffeeMachine cm;
MakeDrink(cm);
// cm удалится сам через `cm.~CoffeeMachine()`
}

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

P. S. Есть такой прикол как девиртуализация - когда компилятор сразу понимает какой метод нужно вызвать (не глядя во vtable). Но для этого компилятор должен доказать, что он точно знает, какой метод нужно вызвать. Хорошая статья на эту тему.

Девиртуализация не стандартизирована, поэтому нужно проверять на своем компиляторе самому - оптимизируется ли вызов виртуального деструктора в примере с MakeCoffee выше или нет. Если да - то можно забить и всегда делать виртуальный деструктор.
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy

ABI: три весёлых буквы 🫡

Одна из тем, которая мало заметна во внешнем мире, но вокруг которой происходит регулярный shitstorm среди тех, кто двигает C++ - это вопрос слома ABI 😀

ABI (wiki) это гарантии, которым интерфейс программного модуля (библиотека, операционная система) удовлетворяет на бинарном уровне: соглашение о вызове, размер типов данных, и многое другое.

Самый простой пример, когда это важно - когда бинарь (исполняемая программа, .exe на Windows) требует для работы динамические библиотеки (.dll на Windows или .so на Unix).
Бинарь и динамическая библиотека - две разные программы, которые должны быть всегда совместимы друг с другом. Это значит, что бинарь и библиотека должны уметь обновляться независимо друг от друга таким образом, чтобы они могли продолжать работать вместе.

(про работу динамических библиотек можно прочитать в этой статье или в этой книге)

Проблема в том, что ABI очень просто сломать - есть неполный список ломающих изменений. Одни из понятных примеров:
😀 Добавить новое поле в публичный класс
🙁 Ломает вообще всё, потому что теперь у .exe-файла (который не перекомпилируют!) неправильное смещение стека, размеры зависимых классов и так далее.
😀 Добавить новый виртуальный метод
🙁 .exe-файл теперь неправильно вычисляет адреса старых виртуальных методов.

Есть тулзы, которые проверяют совместимость ABI: abidiff.

Стандарт C++ развивается так, чтобы не ломать ABI в новых версиях стандарта - то есть чтобы перекомпилирование проекта не мешало использовать зависимые .dll/.so.
Это приводит к проблемам, которые описаны в статьях:
😀 День, когда умерла стандартная библиотека - лонгрид
😀 Цифровые демоны - лонгрид посложнее

Есть два класса проблем, связанные с сохранением ABI:
1️⃣ Нельзя сильно поменять язык, например сортировать поля класса, чтобы класс занял меньше места.
2️⃣ Нельзя нормально менять стандартную библиотеку - большинство пропозалов уничтожаются на месте (проблем этого класса в несколько раз больше).

Реализация стандартной библиотеки C++ не является частью компилятора. Стандарт C++ только определяет интерфейс, а реализаций есть несколько, и они предоставляются в виде динамической библиотеки.

Из-за этого стандартная библиотека C++ крайне бедная, ее контейнеры медленнее чем нужно, некоторые классы супер хреновые (std::initializer_list, std::regex), и так далее - и никто не может это исправить. Если задизайнить что-то новое (например парсер JSON), это потом нельзя будет нормально поменять.
Даже дизайн Win32, POSIX, протоколов Ethernet предполагает всевозможные изменения в будущем - но только не стандартная библиотека C++.

С каждым годом стоимость не-слома ABI (застой в языке) постепенно приближается к стоимости слома ABI. Пока непонятно, будет ли разрешено ломать ABI, так как минимум два вендора против:
1️⃣ GCC. Все коммерческие продукты, поставляемые в бинарном виде для Linux, надеются на неизменность ABI, благодаря чему они могут не пересобирать свой продукт под каждый новый дистрибутив.
2️⃣ Microsoft. Visual Studio с 15 версии сохраняет своё ABI, библиотеки скомпилированные в 2015 версии без проблем линкуются в 2017 и 2019.

Некоторые думают, что C++ "сохраняет совместимость" и это типа хорошо. На деле, как мы видим, причины в сохранении совместимости бизнесовые - чтобы не перекомпилировать продукт кучу раз под разное окружение.

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

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

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

(конец в комментарии)
Please open Telegram to view this post
VIEW IN TELEGRAM
#testing #books

Обзор книги "Modern C++ Programming with Test-Driven Development" (2013 г.) 📚

(можно посмотреть тут - https://pragprog.com/titles/lotdd/modern-c-programming-with-test-driven-development/ в интернете есть PDF)

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

Это было мое вступление 😁 А книга описывает реалии test-driven development в C++. TDD это работа в коротких циклах, в каждом цикле сначала пишется тест на новое поведение, потом пишется реализация которая удовлетворяет тесту.
Несмотря на название, TDD это больше про дизайн системы, потому что он заставляет делать интерфейсы программы так, чтобы они были максимально тестируемы.

В принципе из книги можно понять что такое TDD, для тестов используется GoogleTest. К сожалению в книге есть минусы:

😂 Много воды
Повторяется одно и то же по десять раз, как будто тему раздувают. Также есть много капитанства.
Например, в разделе "Running the Wrong Tests" совет - если вы запускали тесты, но новый тест не запустился, то что делать? Ответ: возможно вы запускали не тот test suite, или у вас неправильный фильтр, или вы не скомпилировали тесты, или тест выключен.
В разделе "Testing the Wrong Code" - вы тестируете не тот код, если вы забыли скомпилировать модуль, или компиляция закончилась неуспешно и вы этого не заметили.
После того, как тратишь по 10 минут на какие-то банальности, начинаешь читать книгу по диагонали 😤

😂 Автор борщит
В одной главе на 50 страницах разбирается пример элементарного модуля, который типа сделали по TDD, и вместо первого приблизительного решения (как это делают в реальном мире) автор делает угарные правки, чтобы пройти следующий кейс и не более того - в итоге все переписано по сто раз.
Автор дает тупые крутые советы а-ля "Мартин Фаулер":
Your favorite tests contain one, two, or three lines with one assertion
Tests with no more than three lines and a single assertion take only minutes to write and usually only minutes to implement
Которые не имеют отношения к реальности.

😂 Слабая техническая составляющая
Очень мало написано про code coverage, CI. Время исполнения методов замеряется на коленке, хотя есть Google Benchmark для мелкого кода и Valgrind для всей программы. Dependency Inversion (чтобы можно было подсунуть мок-класс в тестах) называется сложным понятием.

😎 Вывод: Лучше прочитать доку про GoogleTest, а к тестам относиться как во "вступлении", тогда код и так будет близок к TDD без сомнительных советов.
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy

Как обмануть [[nodiscard]] через std::ignore 💩

В C++17 добавили атрибут [[nodiscard]], которым можно помечать функции, чтобы тот, кто вызвал функцию, не игнорировал возвращаемое значение.

Во многих окружениях любой warning ломает компиляцию (с флагом -Werror). Можно ли все-таки проигнорировать значение?

Оказывается - да 😀 В C++17 добавили std::ignore - это объект, которому можно присвоить любое значение, без эффекта.

[[nodiscard]] int status_code() { return -1; }
[[nodiscard]] std::string sample_text() { return "hello world"; }

void foo() {
std::ignore = status_code(); // нет warning/error
std::ignore = sample_text(); // нет warning/error
}

Но в духе C++ будет запретить std::ignore для некоторых типов. Покопавшись в реализации, можно выключить его, например для int:
template<>
const decltype(std::ignore)&
decltype(std::ignore)::operator=(const int&) const = delete;

Пример на godbolt 👦
Please open Telegram to view this post
VIEW IN TELEGRAM
#story

StarForce: привет из прошлого 💿

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

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

Сейчас про StarForce мало кто вспоминает. Системы проверки CD/DVD-носителей давно неактуальны в силу неактуальности самих дисков, а новые ноутбуки много лет производятся без дисковода.

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

Среди продуктов предлагается C++ Obfuscator, система для изменения кода до нереверсируемого хакерами состояния. Эта система добавляет в исходники лишние условия, циклы, вызовы методов. Скачать его просто так нельзя, предлагается заполнить форму, поэтому я не посмотрел его работу. По ссылке есть пример на алгоритме Евклида.

Есть несколько десятков способов обфускации.
Например, можно сделать так, чтобы нигде в исходниках не встречались строковые литералы в "чистом" виде.
В обычной программе запись
const char* key = "abacaba";
означает, что в бинарнике в секции .rodata (или аналоге) будет в открытом виде лежать последовательность байтов abacaba\0 (\0- нулевой байт). Но обфускатор может сделать так, чтобы этот литерал вычислялся хитрым способом в каком-то методе, и тогда его будет невозможно вытащить просто так.

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

После массового изменения исходного кода может произойти дурка - всплывают скрытые багов, время работы многих кусков кода меняется, ломается код с указателями и так далее...
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler

Быстрый Switch: таблица адресов 💨

В C++ оператор switch используется для передачи потока управления в разные места в зависимости от значения переменной.
Оператор switch можно представить как соответствие между значениями переменной, и кодом который должен выполниться для каждого значения.

В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного switch-оператора, код может сгенерироваться в разном виде. Есть два варианта, какой ассемблер сгенерирует компилятор:
1️⃣ Цепочка последовательных if-ов. Это самый простой путь, потому что switch-оператор всегда представим в этом виде.
2️⃣ Таблица адресов (мой перевод), он же branch table, он же jump table.

Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем switch 30 штук case-ов, то в худшем случае произойдет 30 (!) последовательных сравнений (цепочка if-ов), прежде чем программа поймет номер нужной инструкции.
На самом деле в таких случаях компиляторы умеют делать а-ля "бинарный поиск", поэтому вероятно будет log_2(30) сравнений в худшем случае 😁

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

Пример switch с таблицей адресов: https://godbolt.org/z/3debYb4vq
В этом примере в switch сравнивается значение enum-а. Для компилятора enum представляется как underlying type. По умолчанию этот тип int, то есть во всех операциях с enum происходит неявная конвертация в int.
Таким образом, можно представить, что это switch по значениям от 0 до 6 включительно.

В примере компилятор сгенерировал метки LBB0_2, LBB0_3, ..., LBB0_8 для каждого соответствующего кода case X.

Также компилятор сделал таблицу LJTI0_0, где лежат адреса этих меток. Вообще "таблица" это громко сказано, это просто наша абстракция.
"Таблица" представляет из себя несколько последовательных 8-байтовых числа, которые являются адресами меток LBB0_2-LBB0_8.
А метка LJTI0_0 указывает на начало последовательности.

Теперь, имея "таблицу адресов", можно вычислить номер инструкции, куда надо прыгать. Если параметр равен 0, то прыгаем по первому адресу таблицы, если 1 - по второму, и так далее.
        lea     rcx, [rip + .LJTI0_0]
movsxd rax, dword ptr [rcx + 4*rax]
add rax, rcx
jmp rax

Отступление: Как известно, метки имеют смысл только для ассемблера. Метка просто условно указывает на позицию в бинарнике (инструкцию или данные). После процесса линковки, когда в один исполняемый файл (бинарник) утрамбуются отдельные объектные файлы, вместо меток появятся нормальные адреса.
        lea     rcx, [rip + 0x012345678]

Таблица адресов может иметь другую реализацию, но такая общая идея. Например, в примере с Википедии для рандомного 8-битного ассемблера, значение переменной прибавляется к регистру счетчика команд (addwf PCL,F), а сразу после этой инструкции находится таблица с goto до нужной инструкции, и счетчик команд укажет на нужный goto.

Компилятор сам определяет, нужна ли таблица адресов. Обычно она используется для "плотных" switch, где есть case X для последовательных значений X. Если в case X поставить рандомные значения, то таблицы не получится - пример на godbolt, будут последовательные if-ы.
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler #madskillz

[[assume]] - помоги компилятору сам 😎

Раньше я писал про std::unreachable (он же __builtin_unreachable до C++23) - https://www.group-telegram.com/cxx95.com/58.

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

В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же __builtin_assume до C++23).

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

На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8

Самый простой пример - метод, который делит число на 32:
int div32(int x) {
return x / 32;
}
Казалось бы, очевидная оптимизация - не делить на 32, а сделать битовый сдвиг на 5 битов:
int div32(int x) {
return x >> 5;
}
Но будет неправильно работать на отрицательных числах. Компилятор всегда должен учитывать возможность входа отрицательного числа, из-за этого метод больше по размеру: ссылка на godbolt.

Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
int div32_2(int x) {
[[assume(x >= 0)]]; // или __builtin_assume(x >= 0);
return x / 32;
}
И тогда код оптимизируется: ссылка на godbolt.

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

Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:

1️⃣ Нужно действительно сильно зависеть от быстродействия программы, например это могут быть реалтаймовые программы. Я однажды кидал видео-выступление Тимура Думлера (автора "предложения") на эту тему - https://www.group-telegram.com/cxx95.com/16.

2️⃣ Нужно понимать, за счет чего срезаются инструкции. Пример программы, которая ограничивает значения массива через std::clamp:
void limiter(float* data, size_t size) {
[[assume(size > 0)]];
[[assume(size % 32 == 0)]];
for (size_t i = 0; i < size; ++i) {
[[assume(std::isfinite(data[i]))]];
data[i] = std::clamp(data[i], -1.0f, 1.0f);
}
}
Предполагая, что размер буфера всегда больше 0 и кратен 32, а флоаты нормализованные, программист ставит assume.
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.

3️⃣ Нужно постоянно лезть в ассемблер скомпилированной программы и проверять результат - а как иначе? И даже нужно делать юнит-тесты на генерируемый ассемблер (я бы по крайней мере делал). У компиляторов C++ много тестов на получающийся ассемблер, и в отдельных программах с assume они тоже нужны.

4️⃣ Стандарт отмечает, что компиляторы сами вольны оптимизировать код как смогут, никаких требований на них не налагается. Надо проверять, как работает отдельный компилятор и даже отдельная версия, для этого нужны юнит-тесты из 3️⃣ пункта

Можно сделать разные приколы с assume 😁
😱 Фиксируем вариант в switch - ссылка на godbolt.
😱 Решаем простые уравнения с переменной - ссылка на godbolt.
Please open Telegram to view this post
VIEW IN TELEGRAM
2025/06/30 18:50:45
Back to Top
HTML Embed Code: