en
Feedback
C++95

C++95

Open in Telegram

640K ought to be enough for anybody Author: @cloudy_district aka izaron.github.io

Show more
595
Subscribers
No data24 hours
No data7 days
No data30 days
Posts Archive
#compiler Теория девиртуализации 😶 Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта. Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный 😐 Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память. void CallDo(TDerived& obj) { obj.Do(); // будет ли девиртуализация? } Компилятор считает, что можно девиртуализовать вызов в таких случаях: 1️⃣ Метод класса помечен как final. Смысл в том, что даже в случае работы с объектами TDerived*/TDerived& (которые могут указывать на наследника TDerived) нужный метод будет одним и тем же, как его определил класс TDerived. struct TDerived : IBase { void Do() final override; // слово `final` тут }; 2️⃣ Класс является финальным. Смысл в том, что указатель на этот класс не будет указывать на какого-то наследника, который что-то мог бы переопределить, потому что у такого класса просто не может быть наследников. struct TDerived final : IBase { // слово final тут void Do() override; }; Однако есть еще одно условие, когда класс считается финальным - если у него финальный деструктор 😁 Этот прикол я обнаружил в исходнике Clang. struct TDerived : IBase { ~TDerived() final = default; // слово final тут void Do() override; }; 3️⃣ Мы работаем с объектом класса, а не с указателем на класс. В этом случае точный класс объекта известен на этапе компиляции. TDerived derived; derived.Do(); // это же TDerived, инфа 100% 4️⃣ Объект является prvalue. В C++ есть укуренная классификация объектов, где prvalue (pure value) это грубо говоря выражение которое создает новый объект. Смысл в том, что в этом случае тоже известен точный класс объекта на этапе компиляции. TDerived MakeDerived(); // просто функция // ... MakeDerived().Do(); // здесь будет девиртуализация TDerived{}.Do(); // здесь тоже девиртуализация На этом всё! Эта оптимизация логичная и скучная, потому что никаких чудес ожидать не приходится. Смысл в том, чтобы доказать что TDerived/TDerived&/TDerived* указывает именно на объект TDerived, а не на какой-то его потомок. В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редких покемона кейса, оба противоречат ООП: (1) работа с TDerived* вместо IBase*; (2) Класс TDerived или нужный метод финальный (не помню когда в последний раз писал final). Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид. Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено. Например, в Apple macOS👩‍💻 есть Kext (Kernel Extension) - расширения ядра, запускающие то или иное несовместимое с оригинальным маком оборудование. Особенность этих Kext в том, что они могут в рантайме менять vtable, поэтому нельзя делать оптимизации, которые обходят обращение к vtable. В Clang есть флаг -fapple-kext для такой настройки. А в "обычных" окружениях vtable лежат в секциях наподобии .rodata. Эта секция защищена на уровне операционной системы - программа обычно сразу падает при попытке сделать туда какую-нибудь запись в рантайме.

#opensource Обзор на Lua 👩‍💻 Lua это классический скриптовый язык, широко известный в некоторых кругах. На нем пишутся аддоны к World of Warcraft, Nmap, Nginx, Adobe Lightroom, Neovim, и еще к сотне других проектов. Я решил сделать обзор и собрал всякую редкую информацию. Этот язык простой как пробка. Основу можно узнать в Learn Lua in 15 minutes. В языке единственная структура данных это хэш-таблица. Там есть многочисленный синтаксический сахар, то есть эти записи:
foo.bar = 1337
function Lib.sum (x, y) return x + y end
list = {"apple", "orange", "banana"}

... аналогичны этим:
foo["bar"] = 1337
Lib["sum"] = function (x, y) return x + y end
list = {[1] = "apple", [2] = "banana", [3] = "orange"}

... то есть "массив" это тоже хэш-таблица с ключами от 1 до n (нумерация массивов в Lua с единицы) Через эти хэш-таблицы имитируется абсолютно всё с использованием разного рода костылей. Даже можно реализовать, с позволения сказать, ООП. Объекту foo (хэш-таблице) можно придать ссылку __index на базовый класс (другую хэш-таблицу). Если какого-то поля foo.bar (ключа bar в таблице foo) нет, то интерпретатор Lua посмотрит в таблицу foo.__index, а если и там нет, то в foo.__index.__index, и так далее. В языке есть корутины, closure (как лямбда-функции в C++), рефлексия, и прочие нужные приколы. В интернете есть многие сотни статей про Lua, даже я написал статью 10 лет назад, но лучше читать книгу от автора Programming in Lua. В книгах обычно самое полное изложение, в то время как статьи в интернете заведомо неполные и обычно пишутся чтобы "показать чето крутое". В книге есть такая информация, которой больше нигде нет, например: 1️⃣ Разные флаги, например флаг LUA_32BITS скомпилирует интерпретатор "Small Lua" с 32-битными числами 2️⃣ Описание условий tail call optimization у функций 3️⃣ Метки и goto 4️⃣ Особенность сборки мусора Изначально Lua состоял только из интерпретатора и годился для интеракции с проектами на C/C++ (хотя Lua можно использовать и сам по себе как самостоятельный язык). За долгое время накопилась куча библиотек и проектов. Есть даже несколько новых интерпретаторов/компиляторов, то есть от авторского Lua там только синтаксис языка. Есть полуживой менеджер пакетов LuaRocks (я туда делал пару коммитов). Конечно, с Python объем всего добра не сравнится, но по сравнению с другими скриптовыми языками Lua выглядит хорошо. Интересно, что в языке есть навороченный сборщик мусора. Это реализация обычного mark-and-sweep, с крутыми особенностями: 1️⃣ Раньше сборщик мусора делал stop-the-world (когда посреди исполнения программа останавливается и сборщик собирает весь мусор), а сейчас сборщик инкрементальный. Каждый раз, когда нужно аллоцировать память в N байт, сборщик мусора заодно делает небольшой объем работы, прямо пропорциональный этому N. В итоге не происходит никаких "фризов", просто аллокация памяти выглядит чуть замедленной. 2️⃣ На объект (= хэш-таблицу) можно повесить функцию, которая вызовется перед "удалением" этого объекта сборщиком мусора. Прикол в том, что внутри этой функции можно сохранить объект в какую-нибудь переменную, и удаления в итоге не произойдет. Это называется воскрешением объекта ✝️. Такого нет в C#/Java/Python. В книге есть разные примеры использования этой техники. 3️⃣ В таблице можно пометить все ключи и/или значения как "weak". Тогда сборщик мусора не будет считать такие ссылки за настоящие ("strong") и в при удалении объекта удалит протухшую пару ключ-значение из таблицы. Кстати, у Вани в канале есть сборник постов о GC, можно подписаться и почитать 😐 Интерпретатор Lua работает так - читает исходник, переводит его в "байткод" и интерпретирует этот байткод (как в Java), это быстрее и удобнее. Поисследовать этот процесс можно, скомпилировав исходники Lua в debug-режиме и запуская его из-под gdb. Лексер (перевод кусков строки в "токены") и парсер (перевод "токенов" в байткод) работают одновременно, компиляция происходит в один проход, достаточно смотреть на следующий токен (это LL(1)-парсер). Это самый простой транслятор, и наверное каждый смог бы реализовать перевод Lua в байткод.

#story Кина не будет: цирк в комитете по C++ 🤹 Как я писал, в C++23 приняли аттрибут [[assume(expr)]]. Просматривая статус поддержки C++23 в Clang, я увидел что этот аттрибут пока не поддержан. Я отправил патч на его поддержку - https://reviews.llvm.org/D144334 Это заняло мало времени и всего несколько строк в коде (не считая тестов и документации), потому что в Clang уже есть __builtin_assume, который сделан аналогично:
    case attr::Assume: {
      llvm::Value *ArgValue = EmitScalarExpr(cast<AssumeAttr>(A)->getCond());
      llvm::Function *FnAssume = CGM.getIntrinsic(llvm::Intrinsic::assume);
      Builder.CreateCall(FnAssume, ArgValue);
      break;
    }

В ревью пришло несколько человек и началась клоунада. Оказывается, что представители Clang и MSVC на встречах комитета по стандартизации заявляли, что отказываются реализовывать эту фичу по разным причинам. Причины понятны - плохо полагаться на не описанное в стандарте поведение компилятора (в своем посте я описывал опасности). Часть переписки по патчу: erichkeane: So one thing to note here: I'm on the fence as to whether we want to implement this feature at all. As was discussed extensively during the EWG meetings on this: multiple implementers are against this attribute for a variety of reasons, and at least 1 other implementer has stated they might 'implementer veto' this. I think there is discussion to be had among the code owners here as to whether we even want this. Izaron: I don't quite understand how it works. The feature has been approved for C++2b, but it should have not been approved if there were concerns from implementers. <...> Could you please elaborate: if you decide to not implement this feature, you will kind of revoke the proposal or just deliberately do not support a part of C++2b in Clang? erichkeane: Just deliberately not support a part of C++2b. Implementers have veto'ed features in the past exactly that way. aaron.ballman: Agreed, (IMO) it should not have been approved given how many implementer concerns were expressed. But what goes into the standard is whatever gains consensus in the committee, so the committee occasionally runs the risk of voting in something that won't be implemented. We try to avoid it whenever possible, but it still happens with some regularity. Вот так! Деды из комитета могут принимать любые изменения, которые Clang и/или GCC и/или MSVC никогда не реализуют, просто по большинству голосов. Комитет всегда умеет удивить.

#story Встраивание файлов в исходники 📦 Иногда удобнее, чтобы бинарь не загружал какие-то файлы из файловой системы, а имел их встроенными прямо в исходный код во время компиляции. Кому это может быть нужно, из разных сфер: ⭕️ Финтех: много коэффициентов и числовых констант для performance-critical алгоритмов ⭕️ Геймдев: иконки, текстуры, код шейдеров и скриптов ⭕️ Embedded: часто это единственный вариант, если микросхема не имеет ОС и соответственно файловой системы ⭕️ Бэкенд: файлы настроек (известных в build-time), SSL/TLS-сертификаты Чаще всего для такой цели используется программа xxd. Посмотрим на пример: файл template.cpp - это шаблон для генерации кода. Запустим команду
xxd -i template.cpp template.cpp.data

Получим такой файл template.cpp.data:
unsigned char template_cpp[] = {
    /* байты */
}
unsigned int template_cpp_len = /* кол-во байтов */;

Потом этот файл можно подключить и сделать из него строку (надо указать длину, так как байты не нуль-терминированы):
#include "template.cpp.data"
const std::string TEMPLATE{(char*)template_cpp, template_cpp_len};

В системе сборки можно автоматизировать, чтобы команда xxd запускалась автоматически каждый раз при изменении шаблона, и сгенерированный файл не попадал в исходники (то есть лежал в build-директории): ссылка на функцию CMake. Подобную функциональность несколько лет пытаются внести в C/C++ в виде директивы препроцессора #embed. Пока удалось это сделать для C23 - крутой блог с примерами:
  static const char sound_signature[] = {
#embed <sdk/jump.wav>
  };

  // verify PCM WAV resource signature
  assert(sound_signature[0] == 'R');
  assert(sound_signature[1] == 'I');
  assert(sound_signature[2] == 'F');
  assert(sound_signature[3] == 'F');