В последнее время я ничего не писал - решил написать на тему разработки в тонком клиенте 😎
#story
Тонкий клиент для разработки на C++ - с картинками!😱
С каждым годом размер крупных программ растет, и их становится невозможно или очень трудно разрабатывать на личном компьютере.
На примере Clang/LLVM: по воспоминаниям старожилов, ~5 лет назад его весь можно было построить в Debug-режиме на стандартном компьютере. Сейчас это невозможно, так как объем занимаемой памяти при линковке бинарника часто пробивает порог в 16гб RAM.
Приходится иметь билд в режиме Release или RelWithDebInfo - с этими режимами почти нереально дебажить, приходится ставить много дебажных выводов (как😏
Можно иметь мощный домашний компьютер, но это неудобно (если нравится работать лежа) и его нельзя быстро перевезти.
Я уже много лет не разрабатываю что-то серьезное на самом ноутбуке. На работе использую рабочий виртуальный сервер, а в свободное время свой личный виртуальный сервер.
Личный виртуальный сервер можно создать много где - 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 и вход в виртуалку
в моем случае по команде
Для эффективной работы я использую byobu, это терминальный оконный менеджер.
🔍 оконный менеджер byobu внутри виртуалки
Для написания кода я использую Neovim (форк Vim) - но это дело привычки. Многие коллеги используют Visual Studio Code, он умеет подключаться к виртуалкам по SSH, и просто более привычен для большинства.
🔍 neovim (у меня включены контекстные подсказки и автокомплит)
Для дебага я использую LLDB (аналог дебаггера GDB)
🔍 lldb в процессе
Система контроля версий тоже только изнутри виртуалки
🔍 git в виртуалке
Таким образом можно в процессе работы совсем не вылезать за пределы мощной виртуалки, а ноутбук иметь средней мощности. Я оптимизирую параметры ноутбука по легкости, чтобы он почти не чувствовался в рюкзаке.
#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 -A mango
Конечно, тем кто не работал много времени с виртуалкой, будет сложно эффективно в ней работать. Я опишу используемые мной много лет программы.Для эффективной работы я использую byobu, это терминальный оконный менеджер.
Для написания кода я использую Neovim (форк Vim) - но это дело привычки. Многие коллеги используют Visual Studio Code, он умеет подключаться к виртуалкам по SSH, и просто более привычен для большинства.
Для дебага я использую LLDB (аналог дебаггера GDB)
Система контроля версий тоже только изнутри виртуалки
Таким образом можно в процессе работы совсем не вылезать за пределы мощной виртуалки, а ноутбук иметь средней мощности. Я оптимизирую параметры ноутбука по легкости, чтобы он почти не чувствовался в рюкзаке.
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler
clang-format: царь-костыль 🩼
Я раньше писал про clang-tidy в этом блоге. Он нужен для проверки кода на качество. У него модульный вид - каждый может написать свою проверку и использовать множество независимых друг от друга проверок. Он работает на уровне AST, то есть код проходит лексический и синтаксический анализ до тулзы.
А clang-format это еще одна тулза, нужная для форматирования исходного кода - чтобы было нужное количество пробелов, отсортированные
То есть
Например, в какой-то момент в середине работы вызывается метод WhitespaceManager::generateReplacements, который поправляет пробелы, и в нем внутри метод WhitespaceManager::alignArrayInitializers, чтобы поправить пробелы в массивах.
Совсем без семантики форматировать сложно, поэтому
Там есть всякие поля, как bool IsArrayInitializer (то что этот токен - начало array initialization);
или FormatToken *MatchingParen (ссылка на закрывающую скобку).
Работает все при таком подходе очень хреново😣 . Из стандартных ошибок - ставит много где лишние пробелы или портит лямбды.
Есть куча issue про clang-format, а чинить их значительно сложнее чем issue для
Если в
Например, очень сложно починить такой пустяк как неработающее форматированое во вложенном внутри скобок array initializer. Дело в том, что форматирование полагается на "аннотацию" токенов, а оно именно такое для вложенных скобок, какое есть. Надо править "аннотатор", но это сложно и есть риск сломать что-то еще.
И так для многих issue - начинаешь разбираться в мелкой проблеме - почему ставится лишний пробел, раскрываешь всю цепочку причин, и получаешь мега-проблему, которую нереально починить.
Поэтому старайтесь делать модульные программы, чтобы уменьшить область потенциальных правок при починке бага😎
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-выражении в
Там могут быть только константные значения целочисленного типа или значения enum-ов (а enum это и есть целочисленный тип под прикрытием).
Такое жесткое ограничение сделано из практических соображений - switch-выражения в бинарнике могут трансформироваться в супер-оптимизированный вид с помощью branch table, когда по целочисленному значению аргумента просто вычисляется адрес кода, куда надо прыгнуть.
Понятно, что для строк branch table сделать нельзя, и эффективность switch-а не будет отличаться от кучи if-ов.
В других языках строки в switch возможны - Java 7, C# 6, но там нет упора на максимальную производительность.
Но можно сделать самописный простой "switch", чтобы упростить такой код:
Внутри этого класса всего два поля:
При вызове метода Case, если
Есть методы EndsWith и StartsWith, которые заполнят
Есть аналогичные case-insensitive методы, а также методы Cases для нескольких значений.
Наконец есть оператор приведения к нужному типу (в нашем случае к
На мой взгляд, можно еще сделать класс😐
Простой 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++ с тех пор многократно усложнился, и работоспособных компиляторов единицы.
С компиляторами произошла та же история, что с веб-браузерами, операционными системами, игровыми консолями - раньше их было десятки и их мог сделать кто угодно на коленке, но усложнение сферы оставило лишь единицы выживших.
Рассказ "Редкая профессия" (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
Цитата:✏️ Code Comments
Цитата:✏️ Be a Catalyst
Цитата:
✏️ Be Honest
Глава про важность конструктивного и понятного фидбека, в том числе про бесполезность сэндвичас говном. Многие люди просто не понимают или не так хорошо читают между строк, поэтому нужна прямота, если в коллеге что-то нужно изменить.
✏️ The Office Politician
Глава про офисных кидал среди коллег, которым нельзя доверяться, чтобы они не выезжали за твой счет и присваивали твои достижения.
✏️ The Bad Organization
Во многих компаниях главный продукт не софт, а что-то другое (банки, еда, одежда...), и программисты там просто вспомогательный персонал. В таких случаях резко выше риск, что с процессами в компании жопа - в главе есть общие признаки.
А топ-менеджеры просто не знают зачем ты здесь нужен как класс и зачем тебе платить в несколько раз больше чем уборщику.
✏️ Learn to Manage Upward
Самая лучшая глава - про визибилити. Чтобы расти вверх, нужно показывать ощутимые всеми вокруг результаты в виде графиков, запусков проектов в прод, и так далее.
Вся работа делится на два сорта - offensive (результаты имеют визибилити, их можно показать) и defensive (рефакторинг/переписывание/миграция БД/техдолг/метрики - это делать жизненно необходимо, но визибилити нулевой).
Хотя без defensive работы не обойтись никак, она не дает никаких очков во время ревью. Цитата:
Обзор книги "Team Geek" (2014 г.) 📚
(можно скачать PDF тут)
Эту книгу мне подарили в сан-францисском офисе Google 5.5 лет назад, с тех пор она лежала без дела до недавнего времени. Но это хорошо - без длинного рабочего опыта я бы ее просто не понял.
Это книга не про C++ и даже не совсем про разработку, но она может помочь работать в команде более эффективно. Она разбирает типичные поведенческие особенности программистов, правила и принципы успешных команд, и многое другое в корпоративном мире.
Книга толковая, нет инфоцыганщины. Она разбита на микро-главы, некоторые из них оказались лютой жизой, с которой я сам сталкивался
Чтобы митинги (как "необходимое зло" для многих программистов) не были бесполезными, лучше соблюдать написанные кровью законы:
1) На встрече для дизайна чего-то нового желательно иметь не более 5 человек, с бОльшим числом людей проводить ее сложно.
2) Если есть ежедневные митинги (стендапы), где выступает каждый член команды, они должны быть не длиннее 15 минут.
3) Митинг выбивает контекст работы, поэтому их желательно ставить возле interrupt point: до/после обеда, под конец дня и т.д.
Цитата:
"If the discussion didn’t happen on the email list, then it never really happened."
Цитата:
"Comments should be focused on WHY the code is doing what it’s doing, not WHAT the code is doing."
Цитата:
"In many cases, knowing the right people is more valuable than knowing the right answer"
- про контакты, которые разработчик должен иметь с другими разработчиками в корпорации.Глава про важность конструктивного и понятного фидбека, в том числе про бесполезность сэндвича
Глава про офисных кидал среди коллег, которым нельзя доверяться, чтобы они не выезжали за твой счет и присваивали твои достижения.
Во многих компаниях главный продукт не софт, а что-то другое (банки, еда, одежда...), и программисты там просто вспомогательный персонал. В таких случаях резко выше риск, что с процессами в компании жопа - в главе есть общие признаки.
А топ-менеджеры просто не знают зачем ты здесь нужен как класс и зачем тебе платить в несколько раз больше чем уборщику.
Самая лучшая глава - про визибилити. Чтобы расти вверх, нужно показывать ощутимые всеми вокруг результаты в виде графиков, запусков проектов в прод, и так далее.
Вся работа делится на два сорта - 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.tgoop.com/cxx95/27), которое впрочем имеет свои минусы.
⏰ Сложно поддерживать, нужна сила воли.
💪 💪 Jumbo build (он же Unity build, он же Single Translation Unit)
Можно почитать тут. Unity-билд это подход, когда все файлы модуля строятся за один присест. Это значит что если у нас есть файлы
Минусы:
⏰ Будут конфликтовать имена статических методов и переменных
⏰ Вообще это какая-то дичь и лечит симптомы, а не причины
💪 💪 Precompiled headers
Можно почитать тут. Если у нас есть какие-то очень редко меняющиеся хидеры, то их можно "прекомпилировать".
Компилятор Clang делает это так - читает хидеры (например
Смысл в том, что если в коде где-то есть инклюд хидера
Минусы:
⏰ Как я ни пробовал, у меня это нифига не работает значительно быстрее. В интернете много споров с этим подходом.
💪 💪 Include What You Use
Это тулза для удаления лишних инклюдов. Есть хорошая документация. Иногда бывают баги связанные с тем что математически доказать ненужность инклюда нельзя - они все неявно влияют друг на друга.
💪 💪 Быстрая виртуалка
Ускорять компиляцию можно не только руками. Здесь я описал, как можно использовать виртуалки с большими ресурсами для разработки - https://www.tgoop.com/cxx95/60
💪 💪 Распределенная сборка
В крупных компаниях с крупными проектами используется распределенная сборка - что-нибудь готовое (distcc) или даже свое (недавняя статья от VK).
Эта большая тема, в статье круто описано как выглядит распределенная сборка😃 Кроме того, чтобы не собирать всякие объектники по многу раз, можно использовать кэши - если твой коллега уже запускал сборку и ждал результатов, то тебе ждать не придется.
💪 💪 Новый модный линкер mold
С линкерами в принципе проблем нет, но если у вас бинарь с Debug-символами в несколько гигабайт, то он может линковать вечность, по 2-3-10 минут.
Сейчас разрабатывается новый линкер mold, который типа быстрее других линкеров, но про него нет внятной документации и кое-кто жалуется на нестабильность и поломанные бинари.
Ускорение компиляции на C++
Очень боянистая тема "ускорение компиляции" - это специальный вид спорта, в котором принимали участие многие разработчики на C++, которым не нравится медленная сборка. Я перечислю основные направления этого спорта.
Основным источником проблем является "N*M problem", почти все подходы стараются избежать именно его. Это значит, что если в проекте есть N хидеров и M не-хидеров, то суммарное количество распарсенных файлов в процессе компиляции будет стремиться к N*M, потому что каждый не-хидер компилируется отдельно и транзитивно подключает почти все хидеры, если за этим не следить.
Суммарный размер всех подключенных хидеров может достигать сотен тысяч строк.
Это актуально не только при билде с нуля. Если изменить какой-нибудь важный хидер, который включается почти везде, то это почти равносильно билду с нуля. В запущенных случаях изменения в почти всех хидерах триггерят пересборку с нуля.
В идеале надо стремиться к N+M, как это сделано во многих языках.
PImpl это супер боянистая идиома (почитать можно тут), чья цель изначально скрыть API класса, но от ее использования есть хороший побочный эффект - API может не подключать хидеры нужного класса, если там используется только указатель на класс!
Минусы:
Можно почитать тут. Unity-билд это подход, когда все файлы модуля строятся за один присест. Это значит что если у нас есть файлы
"aaa.cpp", "bbb.cpp", ..., "zzz.cpp"
, то создается (желательно автоматически) файл который их всех подключает:/* all.cpp */И билдится только файл
#include "aaa.cpp"
#include "bbb.cpp"
// ...
#include "zzz.cpp"
"all.cpp"
. Смысл в том, что если 90%+ времени компиляции занимает не сам файл, а хидеры которые их подключают, то такой подход почти линейно ускорит компиляцию.Минусы:
Можно почитать тут. Если у нас есть какие-то очень редко меняющиеся хидеры, то их можно "прекомпилировать".
Компилятор Clang делает это так - читает хидеры (например
test.h
), анализирует и сохраняет в типа уже "распарсенном" формате (например в test.h.pch
).Смысл в том, что если в коде где-то есть инклюд хидера
test.h
, то компилятор не будет его парсить с нуля, а прочитает test.h.pch
, что будет типа быстрее, так как там уже готовое синтаксическое дерево файла.Минусы:
Это тулза для удаления лишних инклюдов. Есть хорошая документация. Иногда бывают баги связанные с тем что математически доказать ненужность инклюда нельзя - они все неявно влияют друг на друга.
Ускорять компиляцию можно не только руками. Здесь я описал, как можно использовать виртуалки с большими ресурсами для разработки - https://www.tgoop.com/cxx95/60
В крупных компаниях с крупными проектами используется распределенная сборка - что-нибудь готовое (distcc) или даже свое (недавняя статья от VK).
Эта большая тема, в статье круто описано как выглядит распределенная сборка
С линкерами в принципе проблем нет, но если у вас бинарь с 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/)
После прохождения в университете стандартных условно "компиляторских" курсов ("формальные языки и грамматики" и "конструирование компиляторов"), я увидел что они слабо относятся к реальному дело.
Реальные компиляторы написаны не совсем так, как сказано в кондовых теоретических учебниках. Мне как будто не хватало какой-то информации - например, ни одна грамматика не разберет сходу, что означает запись😟
Книга
В ней разбираются всякие примеры как: интерпретатор байткода, статический анализатор кода, компилятор C/C++ (урезанный) и многое другое.
В книге есть 31 "паттерн" от простых к сложным, каждый паттерн описывает структуру данных, алгоритм, или типичную архитектуру для языковых тулз.
Паттерны разделены на 4 части: чтение ввода (
Самые простые приложения используют только
Для примера с моего старого поста (https://www.tgoop.com/cxx95/40): часть
В книге не изобретаются велосипеды, а сразу дается правильная архитектура или вспомогательная тулза🙂
Например, вся часть
Это экономит много времени, потому что у человекочитаемой грамматики (например в форме БНФ) могут быть супер неочевидные правила разбора, и надо самому вычислять множества FIRST и FOLLOW, а этому посвящается университетский курс... Генератор парсеров все делает за программиста по описанию грамматики.
Казалось бы - разве так много людей часто делают компилятор чего-либо? Но эта книга все равно будет полезна всем, кто делает свой DSL, тулзы для рефакторинга кода, форматирования, статического анализа, метрик.
Какие есть комментарии к книге:
🤔 Надо помнить, что разрабатываемый в книге примерный "компилятор для C++" делается для урезанного языка. В реальном мире анализатор для C++ это не LL(1) или LL(*), а скорее LL(k), где k - размер файла...
🤔 Весь код в книге написан на Java (ANTLR тоже на нем), возможно кому-то не понравится этот язык.
🤔 В одном из паттернов есть крохоборство для "буфера токенов" - прочитанные из файла токены выкидываются из памяти. Но даже в C++, где после препроцессора файлы могут быть в миллионы строк (и в N раз больше токенов) всё хранится в памяти.
🤔 В книге часто рекламируется генератор парсеров ANTLR, но для ряда сложных задач он выглядит трешово и намного более непонятным чем если бы это делалось в коде. Пример - когда "оптимизируем" код в языке для записи операция с векторами:
🤔 В книге совсем не описана генерация в машинный код и сложные оптимизации. Это отдельная наука и большинству людей никогда не пригодятся эти знания. Но надо иметь в виду, что книга грубо говоря показывает как код на С++ переводится в LLVM IR.
Буквально на последней странице упоминается LLVM и рекомендуется его использование для компилируемых языков, так как там уже есть все для этого.
Обзор книги "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.tgoop.com/cxx95/40): часть
I
объясняет "лексический анализ", часть II
"синтаксический анализ", часть IV
"кодогенерацию".В книге не изобретаются велосипеды, а сразу дается правильная архитектура или вспомогательная тулза
Например, вся часть
I
может быть покрыта ANTLR - генератором парсеров (а автор книги - разработчик ANTLR), и свой парсер не придется писать.Это экономит много времени, потому что у человекочитаемой грамматики (например в форме БНФ) могут быть супер неочевидные правила разбора, и надо самому вычислять множества FIRST и FOLLOW, а этому посвящается университетский курс... Генератор парсеров все делает за программиста по описанию грамматики.
Казалось бы - разве так много людей часто делают компилятор чего-либо? Но эта книга все равно будет полезна всем, кто делает свой DSL, тулзы для рефакторинга кода, форматирования, статического анализа, метрик.
Какие есть комментарии к книге:
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 и рекомендуется его использование для компилируемых языков, так как там уже есть все для этого.
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy
Миф о виртуальных деструкторах🍅
На собеседованиях и в реальной жизни часто встречается вопрос: "Зачем нужен виртуальный деструктор?"
В очень достоверном источнике знаний (то есть в интернете) практически везде написано в таком ключе:
🤔
Ведь он кучу всего генерирует сам (например move- и copy-конструкторы, move- и copy-операторы присваивания).
Ответ: Потому что утверждение выше неправильное!😱 Виртуальный деструктор нужен только тогда, когда есть риск, что объект класса-наследника могут разрушить по указателю на класс-предок.
Пусть есть базовый виртуальный класс
Каждый объект программы рано или поздно надо разрушить, это делается в 2х разных случаях:
1️⃣ Объект создан на стеке, тогда программа сама вызовет деструктор
2️⃣ Объект создан в куче, тогда программист сам делает разрушение объекта, обычно так:
Оператор
Если второго случая в программе не бывает, то виртуальный деструктор не нужен - например в этой программе все работает без ошибок:
P. S. Есть такой прикол как девиртуализация - когда компилятор сразу понимает какой метод нужно вызвать (не глядя во vtable). Но для этого компилятор должен доказать, что он точно знает, какой метод нужно вызвать. Хорошая статья на эту тему.
Девиртуализация не стандартизирована, поэтому нужно проверять на своем компиляторе самому - оптимизируется ли вызов виртуального деструктора в примере с
Миф о виртуальных деструкторах
На собеседованиях и в реальной жизни часто встречается вопрос: "Зачем нужен виртуальный деструктор?"
В очень достоверном источнике знаний (то есть в интернете) практически везде написано в таком ключе:
Если у вас в классе присутствует хотя бы один виртуальный метод, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будет, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут UB (скорее всего в виде утечек памяти).Но есть логичный вопрос: почему бы тогда компилятору не генерировать виртуальный деструктор автоматически (для классов с виртуальными методами)?
Ведь он кучу всего генерирует сам (например move- и copy-конструкторы, move- и copy-операторы присваивания).
Ответ: Потому что утверждение выше неправильное!
Пусть есть базовый виртуальный класс
DrinkMachine
и его наследник класс CoffeeMachine
.Каждый объект программы рано или поздно надо разрушить, это делается в 2х разных случаях:
{В этом случае неважно какой деструктор (виртуальный или нет), потому что в обоих случаях вызовется то что нужно - деструктор реального объекта.
CoffeeMachine cm;
// перед выходом из scope сам вызывается cm.~CoffeeMachine()
}
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) это гарантии, которым интерфейс программного модуля (библиотека, операционная система) удовлетворяет на бинарном уровне: соглашение о вызове, размер типов данных, и многое другое.
Самый простой пример, когда это важно - когда бинарь (исполняемая программа,
Бинарь и динамическая библиотека - две разные программы, которые должны быть всегда совместимы друг с другом. Это значит, что бинарь и библиотека должны уметь обновляться независимо друг от друга таким образом, чтобы они могли продолжать работать вместе.
(про работу динамических библиотек можно прочитать в этой статье или в этой книге)
Проблема в том, что ABI очень просто сломать - есть неполный список ломающих изменений. Одни из понятных примеров:
😀 Добавить новое поле в публичный класс
🙁 Ломает вообще всё, потому что теперь у
😀 Добавить новый виртуальный метод
🙁
Есть тулзы, которые проверяют совместимость ABI: abidiff.
Стандарт C++ развивается так, чтобы не ломать ABI в новых версиях стандарта - то есть чтобы перекомпилирование проекта не мешало использовать зависимые
Это приводит к проблемам, которые описаны в статьях:
😀 День, когда умерла стандартная библиотека - лонгрид
😀 Цифровые демоны - лонгрид посложнее
Есть два класса проблем, связанные с сохранением ABI:
1️⃣ Нельзя сильно поменять язык, например сортировать поля класса, чтобы класс занял меньше места.
2️⃣ Нельзя нормально менять стандартную библиотеку - большинство пропозалов уничтожаются на месте (проблем этого класса в несколько раз больше).
Реализация стандартной библиотеки C++ не является частью компилятора. Стандарт C++ только определяет интерфейс, а реализаций есть несколько, и они предоставляются в виде динамической библиотеки.
Из-за этого стандартная библиотека C++ крайне бедная, ее контейнеры медленнее чем нужно, некоторые классы супер хреновые (
Даже дизайн 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 каждый релиз, и никто от этого не умер.
(конец в комментарии)
ABI: три весёлых буквы
Одна из тем, которая мало заметна во внешнем мире, но вокруг которой происходит регулярный shitstorm среди тех, кто двигает C++ - это вопрос слома ABI
ABI (wiki) это гарантии, которым интерфейс программного модуля (библиотека, операционная система) удовлетворяет на бинарном уровне: соглашение о вызове, размер типов данных, и многое другое.
Самый простой пример, когда это важно - когда бинарь (исполняемая программа,
.exe
на Windows) требует для работы динамические библиотеки (.dll на Windows или .so
на Unix).Бинарь и динамическая библиотека - две разные программы, которые должны быть всегда совместимы друг с другом. Это значит, что бинарь и библиотека должны уметь обновляться независимо друг от друга таким образом, чтобы они могли продолжать работать вместе.
(про работу динамических библиотек можно прочитать в этой статье или в этой книге)
Проблема в том, что ABI очень просто сломать - есть неполный список ломающих изменений. Одни из понятных примеров:
.exe
-файла (который не перекомпилируют!) неправильное смещение стека, размеры зависимых классов и так далее..exe
-файл теперь неправильно вычисляет адреса старых виртуальных методов.Есть тулзы, которые проверяют совместимость ABI: abidiff.
Стандарт C++ развивается так, чтобы не ломать ABI в новых версиях стандарта - то есть чтобы перекомпилирование проекта не мешало использовать зависимые
.dll
/.so
.Это приводит к проблемам, которые описаны в статьях:
Есть два класса проблем, связанные с сохранением ABI:
Реализация стандартной библиотеки C++ не является частью компилятора. Стандарт C++ только определяет интерфейс, а реализаций есть несколько, и они предоставляются в виде динамической библиотеки.
Из-за этого стандартная библиотека C++ крайне бедная, ее контейнеры медленнее чем нужно, некоторые классы супер хреновые (
std::initializer_list
, std::regex
), и так далее - и никто не может это исправить. Если задизайнить что-то новое (например парсер JSON), это потом нельзя будет нормально поменять.Даже дизайн Win32, POSIX, протоколов Ethernet предполагает всевозможные изменения в будущем - но только не стандартная библиотека C++.
С каждым годом стоимость не-слома ABI (застой в языке) постепенно приближается к стоимости слома ABI. Пока непонятно, будет ли разрешено ломать ABI, так как минимум два вендора против:
Некоторые думают, что 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. К сожалению в книге есть минусы:
😂 Много воды
Повторяется одно и то же по десять раз, как будто тему раздувают. Также есть много капитанства.
Например, в разделе
В разделе
После того, как тратишь по 10 минут на какие-то банальности, начинаешь читать книгу по диагонали😤
😂 Автор борщит
В одной главе на 50 страницах разбирается пример элементарного модуля, который типа сделали по TDD, и вместо первого приблизительного решения (как это делают в реальном мире) автор делает угарные правки, чтобы пройти следующий кейс и не более того - в итоге все переписано по сто раз.
Автор даеттупые крутые советы а-ля "Мартин Фаулер":
😂 Слабая техническая составляющая
Очень мало написано про code coverage, CI. Время исполнения методов замеряется на коленке, хотя есть Google Benchmark для мелкого кода и Valgrind для всей программы. Dependency Inversion (чтобы можно было подсунуть мок-класс в тестах) называется сложным понятием.
😎 Вывод: Лучше прочитать доку про GoogleTest, а к тестам относиться как во "вступлении", тогда код и так будет близок к TDD без сомнительных советов.
Обзор книги "Modern C++ Programming with Test-Driven Development" (2013 г.) 📚
(можно посмотреть тут - https://pragprog.com/titles/lotdd/modern-c-programming-with-test-driven-development/
В последние несколько лет у меня такое правило: набор тестов описывает все возможные кейсы, которые система должна уметь делать.
Это значит, что если нет теста, который описывает какое-то поведение, то этого поведения нет или оно не запланировано (или тесты неполные).
Звучит супер банально, но такой подход уменьшает количество сюрпризов на квадратный метр и скорее всего не даст заниматься такой тупостью, как фиксить одни и те же баги по несколько раз.
Также это значит, что если вы написали какой-то код, то по умолчанию нужно считать он не работает, пока не будет тестов, которые доказывают обратное. Нормальная ситуация, когда на написание тестов нового кода потрачено в несколько раз больше времени, чем на сам новый код, потому что потери времени и ресурсов из-за бага в проде все равно во много раз выше времени на тесты. (конец вступления)
Это было мое вступление
Несмотря на название, 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 (чтобы можно было подсунуть мок-класс в тестах) называется сложным понятием.
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy
Как обмануть [[nodiscard]] через std::ignore💩
В C++17 добавили атрибут [[nodiscard]], которым можно помечать функции, чтобы тот, кто вызвал функцию, не игнорировал возвращаемое значение.
Во многих окружениях любой warning ломает компиляцию (с флагом
Оказывается - да😀 В C++17 добавили std::ignore - это объект, которому можно присвоить любое значение, без эффекта.
👦
Как обмануть [[nodiscard]] через std::ignore
В C++17 добавили атрибут [[nodiscard]], которым можно помечать функции, чтобы тот, кто вызвал функцию, не игнорировал возвращаемое значение.
Во многих окружениях любой warning ломает компиляцию (с флагом
-Werror
). Можно ли все-таки проигнорировать значение?Оказывается - да
[[nodiscard]] int status_code() { return -1; }Но в духе C++ будет запретить
[[nodiscard]] std::string sample_text() { return "hello world"; }
void foo() {
std::ignore = status_code(); // нет warning/error
std::ignore = sample_text(); // нет warning/error
}
std::ignore
для некоторых типов. Покопавшись в реализации, можно выключить его, например для int
:template<>Пример на godbolt
const decltype(std::ignore)&
decltype(std::ignore)::operator=(const int&) const = delete;
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
StarForce: привет из прошлого 💿
Недавно я посмотрел ностальгическое видео на ютубе про древнее зло - StarForce😨 Это система защиты ПО (чаще компьютерных игр) от незаконного копирования и даже распространения. Она была настолько эпичной, что могла испортить физические компоненты компьютера в попытках защитить издателя.
Я и сам пострадал от этой системы - много лет назад я не смог установить крутые игры с дисков школьного друга, потому что на них стояла эта защита.
Сейчас про StarForce мало кто вспоминает. Системы проверки CD/DVD-носителей давно неактуальны в силу неактуальности самих дисков, а новые ноутбуки много лет производятся без дисковода.
Поискав в интернете компанию, я обнаружил что она жива до сих пор! А в списке клиентов есть с детства знакомые логотипы давно почивших компаний.
Среди продуктов предлагается C++ Obfuscator, система для изменения кода до нереверсируемого хакерами состояния. Эта система добавляет в исходники лишние условия, циклы, вызовы методов. Скачать его просто так нельзя, предлагается заполнить форму, поэтому я не посмотрел его работу. По ссылке есть пример на алгоритме Евклида.
Есть несколько десятков способов обфускации.
Например, можно сделать так, чтобы нигде в исходниках не встречались строковые литералы в "чистом" виде.
В обычной программе запись
Какие есть минусы у обфускации? В большой программе всегда есть куча скрытых багов, которые себя по счастливой случайности не проявляют в заметных местах, и все тесты проходятся. Также более-менее понятен расклад по профайлу.
После массового изменения исходного кода может произойти дурка - всплывают скрытые багов, время работы многих кусков кода меняется, ломается код с указателями и так далее...
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
YouTube
Star Force - проклятие геймеров нулевых
Начни играть на новом сервере «Кракен» в ArcheAge с подарками: https://bit.ly/3dvJ41s
По вопросам рекламы - restart@topbro.ru || Или - zhironkin.ilya@gmail.com
Патреон - https://www.patreon.com/ilya_commander
Бусти - https://boosty.to/ilya_commander
Телега…
По вопросам рекламы - restart@topbro.ru || Или - zhironkin.ilya@gmail.com
Патреон - https://www.patreon.com/ilya_commander
Бусти - https://boosty.to/ilya_commander
Телега…
#compiler
Быстрый Switch: таблица адресов💨
В C++ оператор
Оператор
В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного
1️⃣ Цепочка последовательных
2️⃣ Таблица адресов (мой перевод), он же branch table, он же jump table.
Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем
На самом деле в таких случаях компиляторы умеют делать а-ля "бинарный поиск", поэтому вероятно будет log_2(30) сравнений в худшем случае 😁
Во втором варианте номер инструкции, куда надо перепрыгнуть, вычисляется в зависимости от значения переменной, в процессе чего не выполнится ни одного сравнения.
Пример
В этом примере в
Таким образом, можно представить, что это
В примере компилятор сгенерировал метки
Также компилятор сделал таблицу
"Таблица" представляет из себя несколько последовательных 8-байтовых числа, которые являются адресами меток
А метка
Теперь, имея "таблицу адресов", можно вычислить номер инструкции, куда надо прыгать. Если параметр равен 0, то прыгаем по первому адресу таблицы, если 1 - по второму, и так далее.
Компилятор сам определяет, нужна ли таблица адресов. Обычно она используется для "плотных"
Быстрый Switch: таблица адресов
В C++ оператор
switch
используется для передачи потока управления в разные места в зависимости от значения переменной.Оператор
switch
можно представить как соответствие между значениями переменной, и кодом который должен выполниться для каждого значения.В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного
switch
-оператора, код может сгенерироваться в разном виде. Есть два варианта, какой ассемблер сгенерирует компилятор:if
-ов. Это самый простой путь, потому что switch
-оператор всегда представим в этом виде.Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем
switch
30 штук case
-ов, то в худшем случае произойдет 30 (!) последовательных сравнений (цепочка if
-ов), прежде чем программа поймет номер нужной инструкции.Пример
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]] - помоги компилятору сам😎
Раньше я писал про
Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.
В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же
Эта штука делает указание компилятору, что в данной ветке исполнения выражение
На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8
Самый простой пример - метод, который делит число на 32:
Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
В более сложных примерах, которые показываются в "предложении", можно в несколько раз уменьшить количество инструкций ассемблера, особенно в математических программах.
Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:
1️⃣ Нужно действительно сильно зависеть от быстродействия программы, например это могут быть реалтаймовые программы. Я однажды кидал видео-выступление Тимура Думлера (автора "предложения") на эту тему - https://www.tgoop.com/cxx95/16.
2️⃣ Нужно понимать, за счет чего срезаются инструкции. Пример программы, которая ограничивает значения массива через std::clamp:
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.
3️⃣ Нужно постоянно лезть в ассемблер скомпилированной программы и проверять результат - а как иначе? И даже нужно делать юнит-тесты на генерируемый ассемблер (я бы по крайней мере делал). У компиляторов C++ много тестов на получающийся ассемблер, и в отдельных программах с assume они тоже нужны.
4️⃣ Стандарт отмечает, что компиляторы сами вольны оптимизировать код как смогут, никаких требований на них не налагается. Надо проверять, как работает отдельный компилятор и даже отдельная версия, для этого нужны юнит-тесты из 3️⃣ пункта
Можно сделать разные приколы с😁
😱 Фиксируем вариант в switch - ссылка на godbolt.
😱 Решаем простые уравнения с переменной - ссылка на godbolt.
[[assume]] - помоги компилятору сам
Раньше я писал про
std::unreachable
(он же __builtin_unreachable
до C++23) - https://www.tgoop.com/cxx95/58.Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.
В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же
__builtin_assume
до C++23).Эта штука делает указание компилятору, что в данной ветке исполнения выражение
expr
следует считать равным true
, и делать разные оптимизации на основе этих данных. Выражение expr
вычисляться во время работы программы не будет, это подсказка времени компиляции.На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8
Самый простой пример - метод, который делит число на 32:
int div32(int x) {Казалось бы, очевидная оптимизация - не делить на 32, а сделать битовый сдвиг на 5 битов:
return x / 32;
}
int div32(int x) {Но будет неправильно работать на отрицательных числах. Компилятор всегда должен учитывать возможность входа отрицательного числа, из-за этого метод больше по размеру: ссылка на godbolt.
return x >> 5;
}
Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
int div32_2(int x) {И тогда код оптимизируется: ссылка на godbolt.
[[assume(x >= 0)]]; // или __builtin_assume(x >= 0);
return x / 32;
}
В более сложных примерах, которые показываются в "предложении", можно в несколько раз уменьшить количество инструкций ассемблера, особенно в математических программах.
Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:
void limiter(float* data, size_t size) {Предполагая, что размер буфера всегда больше 0 и кратен 32, а флоаты нормализованные, программист ставит assume.
[[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);
}
}
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.
Можно сделать разные приколы с
assume
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Самое простое объяснение std::function за 15 минут😦
Этот пост был написан под влиянием крутого видео от Jason Turner "A Simplified std::function Implementation"
Часто люди не задумываются, как работает
Можно сказать, что в C++ есть два типа объектов, на которых работает семантика вызова как функции. Можно условно назвать их
1️⃣ Сами функции:
2️⃣ Объекты типов с определенным
А
2️⃣ может иметь неопределенный размер. Например, размер структуры у лямбды зависит от того, какие captures он делает.
Поэтому, к сожалению,
Также нужно использовать виртуальный класс, который для каждого отдельного типа как бы вычислит адрес вызываемого метода:
Виртуальный класс и указатель на кучу:
👍
Самое простое объяснение std::function за 15 минут
Этот пост был написан под влиянием крутого видео от Jason Turner "A Simplified std::function Implementation"
Часто люди не задумываются, как работает
std::function
. Чаще всего знают, что эта штука - обертка над чем-то, что можно "вызвать" как функцию. Кто-то смутно помнит, что std::function
вроде как лезет в динамическую память. cppreference не сильно раскрывает внутренности реализации.Можно сказать, что в C++ есть два типа объектов, на которых работает семантика вызова как функции. Можно условно назвать их
Callable
. Это:int foo(int a, int b) { return a + b; }
operator()
, часто их называют "функторы":struct foo {Все остальные
int operator()(int a, int b) { return a + b; }
}
Callable
являются производными от этих двух типов. В том числе лямбды - компилятор их переделывает в структуры с operator()
. Про лямбды есть хорошая книга - https://www.tgoop.com/cxx95/48.А
std::function<Signature>
должен уметь хранить все возможные Callable
с данной сигнатурой. template<typename Ret, typename... Param>Возникает проблема - у
class function<Ret(Param...)> {
// код реализации
};
std::function
должен быть фиксированный размер, но Callable
типа Поэтому, к сожалению,
std::function
хранит Callable
в куче.Также нужно использовать виртуальный класс, который для каждого отдельного типа как бы вычислит адрес вызываемого метода:
Виртуальный класс и указатель на кучу:
struct callable_interface {Реализация для каждого отдельного типа
virtual Ret call(Param...) = 0;
virtual ~callable_interface() = default;
};
std::unique_ptr<callable_interface> callable_ptr;
Callable
держит в себе сам объект Callable
и метод для вызова operator()
по правильному адресу:template<typename Callable>Конструктор
struct callable_impl : callable_interface {
callable_impl(Callable callable_) : callable{std::move(callable_)} {}
Ret call(Param... param) override { return std::invoke(callable, param...); };
Callable callable;
}
std::function
принимает Callable
и создает объект в куче:template<typename Callable>И наконец вызов
function(Callable callable)
: callable_ptr{std::make_unique<callable_impl<Callable>>(std::move(callable))}
{}
operator()
у самого std::function
перенаправляет вызов в содержимый Callable
:Ret operator()(Param... param) { return callable_ptr->call(param...); }Вот так выглядит один из способов type erasure в C++
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
C++ Weekly - Ep 333 - A Simplified std::function Implementation
☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
► https://ndctechtown.com/workshops/c-best-practices/4ceb8f7cf86c
Upcoming Workshop: Applied constexpr: The Power of Compile-Time Resources, C++…
Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
► https://ndctechtown.com/workshops/c-best-practices/4ceb8f7cf86c
Upcoming Workshop: Applied constexpr: The Power of Compile-Time Resources, C++…
#creepy #compiler
Самое мерзкое правило в C++ для модульных программ и как его обойти🤢
Недавно я в своем pet project снова столкнулся с тем, что могу назвать самым мерзким правилом C++ в своем опыте, по крайней мере для модульных программ. Оно связано с особенностями работы линковщика и требует всяких тайных знания для решения.
В больших модульных проектах для скорости разработки иногда используется такая схема - каждому модулю бизнес-логики соответствует статическая библиотека (static library).
Если какой-то модуль с помощью всяких флагов системы сборки не линковать в итоговый бинарник, то этот модуль не будет билдиться, а в итоговом бинарнике ничего не сломается. Просто в бинарнике будет меньше фичей.
С другой стороны, если модуль слинкован, то он должен как-то "зарегистрировать" себя в списке модулей.😐
Есть файл
Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из
Процесс решения проблемы зависит от системы сборки, операционной системы и многих других вещей. Общепринятого решения проблемы нет. Как эту проблему решают в разных случаях:
1️⃣ Windows - указать на переменные, которых нельзя выбросить - ссылка
2️⃣ Помещение переменных в отдельные секции и пометка этих секций как невыбрасываемых - ссылка
3️⃣ Linux - также указать на невыбрасываемые переменные через параметр командной строки - ссылка1, ссылка2, переменная не должна быть
4️⃣ Linux - сделать link-скрипт, который указывает что и как надо линковать - ссылка
Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (
С библиотеками (
В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
Поэтому "регистрация модуля" сработает и проблема будет решена😁
Самое мерзкое правило в C++ для модульных программ и как его обойти
Недавно я в своем pet project снова столкнулся с тем, что могу назвать самым мерзким правилом C++ в своем опыте, по крайней мере для модульных программ. Оно связано с особенностями работы линковщика и требует всяких тайных знания для решения.
В больших модульных проектах для скорости разработки иногда используется такая схема - каждому модулю бизнес-логики соответствует статическая библиотека (static library).
Если какой-то модуль с помощью всяких флагов системы сборки не линковать в итоговый бинарник, то этот модуль не будет билдиться, а в итоговом бинарнике ничего не сломается. Просто в бинарнике будет меньше фичей.
С другой стороны, если модуль слинкован, то он должен как-то "зарегистрировать" себя в списке модулей.
Есть файл
module.h
с такими методами (упрощенно)struct IModule { virtual void Do() = 0; };Файл
void AddModule(std::unique_ptr<IModule> module);
const std::vector<std::unique_ptr<IModule>>& GetModules();
main.cpp
должен использовать GetModules()
, а модуль должен зарегистрировать сам себя через AddModule
.Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из
.cpp
-файлов модуля должно быть такое:struct Dummy {Дальше начинается кино. Переменная
Dummy() {
AddModule(std::make_shared<MyCoolModule>());
}
};
static Dummy dummy;
dummy
и код для ее инициализации попадает в статическую библиотеку libcoolmodule.a
(можно проверить через objdump), но при линковке бинарника эта переменная выбрасывается линкером как неиспользуемая. В итоге модуль не зарегистрируется.Процесс решения проблемы зависит от системы сборки, операционной системы и многих других вещей. Общепринятого решения проблемы нет. Как эту проблему решают в разных случаях:
__pragma comment(linker,"/include:?variable_name@")
static Var_t g_DumbVar __attribute__((__used__, section(".var_section.g_DumbVar"))) = (const Var_t) X_MARKER;
static Var_t* g_DumbVarGuard[] __attribute__((__used__, section(".guard"))) = { &g_DumbVar };
static
и volatile
.Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (
.o
) в команде к линкеру линкуются по порядку и ничто не выбрасывается.С библиотеками (
.a
, архив из .o
) есть "оптимизация": .o
-файл из библиотеки линкуется только если в нем находится определение какого-нибудь undefined symbol, который требуется в уже слинкованных прежде .o
-файлах. В противном случае считается, что этот .o
-файл не нужен и в бинарник он не попадает.В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
add_library(enum_serializer STATIC module.cpp helper.cpp)на такую:
add_library(enum_serializer OBJECT module.cpp helper.cpp)И тогда, если какой-то бинарник зависит от
enum_serializer
, он будет линковать не libenum_serializer.a
, а module.o
и helper.o
.Поэтому "регистрация модуля" сработает и проблема будет решена
Please open Telegram to view this post
VIEW IN TELEGRAM
#longread
Кодогенератор Waffle++ для C++ 🧇
https://habr.com/ru/post/710744/
Я рассказал о своем проекте расширяемого кодогенератора для С++, где каждый может написать свой модуль быстро и с надежными тестами.
Уже сейчас можно получить бесплатный перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, систему слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов...
Это может быть интересно!🥞
https://github.com/Izaron/WafflePlusPlus
Кодогенератор Waffle++ для C++ 🧇
https://habr.com/ru/post/710744/
Я рассказал о своем проекте расширяемого кодогенератора для С++, где каждый может написать свой модуль быстро и с надежными тестами.
Уже сейчас можно получить бесплатный перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, систему слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов...
Это может быть интересно!
https://github.com/Izaron/WafflePlusPlus
Please open Telegram to view this post
VIEW IN TELEGRAM
#advertisement
Работа на C++ в Алисе🎙
Алиса это один из самых быстрорастущих сервисов Яндекса, где можно заниматься уникальными задачами на C++ - от embedded-разработки до машинного обучения. Я сам работаю над Алисой уже несколько лет и могу это гарантировать🦆
Сейчас Алиса расширяется и нанимает много разработчиков на C++ (от стажеров до сениоров), поэтому я решил помочь коллегам найти крутых разработчиков из читателей этого блога.
Пишите мне (@cloudy_district) если захотите попробовать себя в разработке сервиса с миллионами пользователей!😀
Работа на C++ в Алисе
Алиса это один из самых быстрорастущих сервисов Яндекса, где можно заниматься уникальными задачами на C++ - от embedded-разработки до машинного обучения. Я сам работаю над Алисой уже несколько лет и могу это гарантировать
Сейчас Алиса расширяется и нанимает много разработчиков на C++ (от стажеров до сениоров), поэтому я решил помочь коллегам найти крутых разработчиков из читателей этого блога.
Пишите мне (@cloudy_district) если захотите попробовать себя в разработке сервиса с миллионами пользователей!
Please open Telegram to view this post
VIEW IN TELEGRAM
#story #compiler
Порог входа для коммитов в компиляторы🍗
Однажды в одном чате про C++ один участник высказал сомнение - имеет ли смысл делать багфиксы в существующие компиляторы, разве там нет "проблем" с тем, что желающих исправить огромно, и issues просто моментально исчезают?😱
На деле ситуация с Clang/LLVM (и много где еще) - обратная.
Тысячи висящих issue, которых никто годами не исправляет. Ревью тоже медленное, pull request висят неделями-месяцами. Активных разработчиков - максимум несколько десятков, и то там большой перекос в единицы супер-активных.🏃♂️
Многие из упомянутого актива работают "на зарплате", то есть работают в Google/Apple/%company_name% и за деньги разрабатывают Clang/LLVM, иначе наверное разработка вообще не будет двигаться.
Кроме багфиксов, реально каждый может зайти на https://clang.llvm.org/cxx_status.html#cxx20, посмотреть на то, что из C++20/23 не реализовано в Clang и поддержать новую фичу. Я такое делал пару раз. Но это были простые коммиты. Сложные предложения реализовать почти нереально, это нужно делать как фултайм работу. Поэтому не только лишь все это делают.
Например, сравнительно просто можно сделать фичу C++23 [P2324R2] Labels at the end of compound statements. У меня ушло 2 коммита - коммит 1, коммит 2.
Также какие-то подвижки можно сделать для [P0533R9] constexpr for <cmath> and <cstdlib>. В этом предложении куча новых методов помечена как
Запись
Также я добавил тест, в котором можно следить за реализацией
Однако, спустя несколько месяцев, дело продвинулось лишь ненамного - blame теста, видно всего два коммита. Там пацан, который в основном занимается libcxx, поддержал часть функций.
Поэтому контрибьютить в компиляторы сможет любой, если есть время и желание сделать что-то новое🚬
Порог входа для коммитов в компиляторы
Однажды в одном чате про C++ один участник высказал сомнение - имеет ли смысл делать багфиксы в существующие компиляторы, разве там нет "проблем" с тем, что желающих исправить огромно, и issues просто моментально исчезают?
На деле ситуация с Clang/LLVM (и много где еще) - обратная.
Тысячи висящих issue, которых никто годами не исправляет. Ревью тоже медленное, pull request висят неделями-месяцами. Активных разработчиков - максимум несколько десятков, и то там большой перекос в единицы супер-активных.
Многие из упомянутого актива работают "на зарплате", то есть работают в Google/Apple/%company_name% и за деньги разрабатывают Clang/LLVM, иначе наверное разработка вообще не будет двигаться.
Кроме багфиксов, реально каждый может зайти на https://clang.llvm.org/cxx_status.html#cxx20, посмотреть на то, что из C++20/23 не реализовано в Clang и поддержать новую фичу. Я такое делал пару раз. Но это были простые коммиты. Сложные предложения реализовать почти нереально, это нужно делать как фултайм работу. Поэтому не только лишь все это делают.
Например, сравнительно просто можно сделать фичу C++23 [P2324R2] Labels at the end of compound statements. У меня ушло 2 коммита - коммит 1, коммит 2.
Также какие-то подвижки можно сделать для [P0533R9] constexpr for <cmath> and <cstdlib>. В этом предложении куча новых методов помечена как
constexpr
. Я придумал схему для реализации - метод должен быть помечен как constexpr, если его константное вычисление поддерживается компилятором. Условный пример для std::fmax:inline _LIBCPP_CONSTEXPR_CXX23_IF_CONSTEXPR_BUILTIN(__builtin_fmax) double fmax(double __x, double __y) {Макрос
return __builtin_fmax(__x, __y);
}
_LIBCPP..._BUILTIN
внутри себя обращается к функции __has_constexpr_builtin
(из моего коммита) для проверки, поддерживается ли функция в аргументе в константном вычислении.Запись
__builtin_XXX
в рантайм-вычислении обычно преобразуется в просто вызов метода/какой-то код, а в константном вычислении выполняется прямо в компиляторе. Для каждой функции нужен свой коммит в компилятор (мой коммит для fmax).Также я добавил тест, в котором можно следить за реализацией
P0533
(мой коммит). Как только какой-то метод помечается как constexpr (автоматическим образом как выше), обязательно нужно поменять макрос в тесте (иначе тест упадет). Как только все нужные методы помечены как constexpr, то тест скажет что фича P0533
полностью реализована (и надо поменять статус на cxx_status.html
).Однако, спустя несколько месяцев, дело продвинулось лишь ненамного - blame теста, видно всего два коммита. Там пацан, который в основном занимается libcxx, поддержал часть функций.
Поэтому контрибьютить в компиляторы сможет любой, если есть время и желание сделать что-то новое
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Чем исключения в C++ похожи на сборщик мусора в других языках ♻️
Имеется в виду не техническая похожесть, а сам принцип наличия фичи. Изначально все фичи выглядят как что-то крутое, что есть "из коробки" и можно использовать без задней мысли.
Но когда возникает нужда что-то поправить в этой фиче, то программист замучается глотать пыль, копаясь в технической реализации.🔍
Например, для Java и других языков со сборкой мусора это необходимость разбираться в алгоритмах сборки мусора (mark-and-sweep, etc.), в более хитрой работе с памятью чтобы сборщик собирал меньше мусора ("пул объектов"), понимать нужные настройки чтобы сборщик не фризил программу посередине HTTP-запроса, и так далее. Это все долго и нудно описывается в разных статьях.
В C++ есть подобная история с исключениями. Но здесь требуются еще более нетривиальные познания для полного понимания. Основных проблем две, первая встречается чаще, но вторая более жесткая:
1️⃣ Оверхед по времени исполнения и хуже с компиляторными оптимизациями. Есть супер крутой лонгрид Исключения C++ через призму компиляторных оптимизаций.
Вместо того, чтобы просто взять и вызвать функцию, которая теоретически может выбросить исключение, приходится делать дополнительный блок кода для обработки потенциального исключения, и переводить исполнение туда в случае исключения (лишняя проверка на КАЖДЫЙ вызов).
Также во многих оптимизациях исключения не участвуют. Например, оптимизация "удаление мертвого кода" считает, что код для обработки исключений нужен всегда, а там по транзитивности нужны и все переменные внутри него, и в итоге все вообще плохо оптимизируется.
2️⃣ Сложность в разработке, в том числе в обеспечении Strong Exception Guarantee.
Strong Exception Guarantee это гарантия, что если вызвать метод, который вдруг выбросит исключение, то состояние программы будет таким же, как было до вызова метода. Контейнеры по возможности реализуют SEG, и это очень муторно. В моей статье про контейнеры я описывал, когда работает SEG.
В обычных программах сделать нормальный SEG очень сложно. Не работает история о том, что мы приняли запрос, положили из него данные в структуру, что-то записали в базу данных, залогировали, а потом вдруг поймали исключения и надо все вернуть обратно.
Есть понятие гарантии Basic Exception Guarantee, программа продолжает работать, но данные немного поломаны. Например, в примере выше, в зависимости от свойств класса😐
Объяснение сути BEG на примере саппорта Microsoft Word (😢 - клиент, 🎧 - саппорт):
😢 - Это поддержка MS Word? Я писал книгу, но Word вдруг все удалил.
🎧 - Все нормально, просто мы предоставляем только базовые гарантии при ошибках программы.
😢 - Большое спасибо, удачного дня!
Во многих code style (например от Google) исключения использовать запрещено.
В компиляторах есть флаг
Чем исключения в C++ похожи на сборщик мусора в других языках ♻️
Имеется в виду не техническая похожесть, а сам принцип наличия фичи. Изначально все фичи выглядят как что-то крутое, что есть "из коробки" и можно использовать без задней мысли.
Но когда возникает нужда что-то поправить в этой фиче, то программист замучается глотать пыль, копаясь в технической реализации.
Например, для Java и других языков со сборкой мусора это необходимость разбираться в алгоритмах сборки мусора (mark-and-sweep, etc.), в более хитрой работе с памятью чтобы сборщик собирал меньше мусора ("пул объектов"), понимать нужные настройки чтобы сборщик не фризил программу посередине HTTP-запроса, и так далее. Это все долго и нудно описывается в разных статьях.
В C++ есть подобная история с исключениями. Но здесь требуются еще более нетривиальные познания для полного понимания. Основных проблем две, первая встречается чаще, но вторая более жесткая:
Вместо того, чтобы просто взять и вызвать функцию, которая теоретически может выбросить исключение, приходится делать дополнительный блок кода для обработки потенциального исключения, и переводить исполнение туда в случае исключения (лишняя проверка на КАЖДЫЙ вызов).
Также во многих оптимизациях исключения не участвуют. Например, оптимизация "удаление мертвого кода" считает, что код для обработки исключений нужен всегда, а там по транзитивности нужны и все переменные внутри него, и в итоге все вообще плохо оптимизируется.
Strong Exception Guarantee это гарантия, что если вызвать метод, который вдруг выбросит исключение, то состояние программы будет таким же, как было до вызова метода. Контейнеры по возможности реализуют SEG, и это очень муторно. В моей статье про контейнеры я описывал, когда работает SEG.
void TryAddWidget(std::vector<Widget>& widgets) {В Стандарте есть велосипеды специально для SEG: std::move_if_noexcept.
// выполняем некий код...
Widget w;
try {
widgets.push_back(std::move(w));
} catch (...) {
// поймали исключение...
// ожидаем, что `widgets` остался юзабельным
}
}
В обычных программах сделать нормальный SEG очень сложно. Не работает история о том, что мы приняли запрос, положили из него данные в структуру, что-то записали в базу данных, залогировали, а потом вдруг поймали исключения и надо все вернуть обратно.
Есть понятие гарантии Basic Exception Guarantee, программа продолжает работать, но данные немного поломаны. Например, в примере выше, в зависимости от свойств класса
Widget
, в векторе widgets
могут стереться все элементы после выброса исключения Объяснение сути BEG на примере саппорта Microsoft Word (
Во многих code style (например от Google) исключения использовать запрещено.
В компиляторах есть флаг
-fno-exceptions
, которые удаляют весь оверхед, связанный с исключениями, не генерируют лишний код. Исключения до сих пор можно будет бросать, но они просто будут крашить программу, и никак не будут обрабатываться.Please open Telegram to view this post
VIEW IN TELEGRAM