Pull to refresh

Comments 34

Я не совсем понял, зачем прибивать гвоздями классы к их отражению в виде enum - разве что вам очень сильно хочется вынести это в constexpr
В своей реализации я вынес этот "айди" класса в статический член, который генерирует новый айди для каждого класса, и делает это при запуске программы - автоматически, конечно
В итоге всё работает только на виртуальных методах без хранимого объекта айдишника вообще, и при этом очень легко расширяемо - нужно только добавить макрос в класс, примерно как в qt

"На виртуальных методах"... Тогда проще RTTI?

Вы имеете в виду производительность? Нет. RTTI зачастую вообще реализуется через сравнение строк (названий классов), оверхед использования vtable по сравнению с этим - пыль.

Зачем сравнивать строки, чем это поможет? Для dynamic_cast нужно получить указатель на истиное начало класса в памяти, обладая указателем на какой-то базовый класс. И так же знать оффсеты производных классов относительно базового. И эта информация должна быть доступна в момент исполнения. Следовательно, она хранится в каких-то структурах, на которые в конечном счёте имеется ссылка из каждой таблицы виртуальных функций реализуемых данным классом. Как я понимаю. Остальное детали. Возможно в этом алгоритме и есть какая-то итерация по спискам, вызывающая замедление для 100500 раз переунаследованных классов, но уж сравнение строк точно ни за чем и типичном случае там алгоритмическая сложность не зашкаливающая.

Речь скорей о динамических библиотеках, где проверить на совпадение типов через сравнение ссылок на std::type_info нельзя, и приходится сравнивать сами std::type_info по-значению, что заканчивается strcmp. Да, вот она плата за универсальность. В принципе такая-же проблема постигнет любой "самодельный RTTI" как дойдёт до динамических библиотек (нет возможности назначить какой-то числовой идентификатор известный в двух разных библиотеках, например, так как одна о другой может быть вовсе не в курсе).

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

Да, я имел в виду именно проблему с динамическими библиотеками

Честно говоря, я сам не копал так глубоко, чтобы самостоятельно ковырять все компиляторы и пилить бенчмарки, но читал некоторые статьи, плюс видел, что на каждом моём месте работы (геймдев) была своя похожая замена RTTI, в т.ч. это UE.

Конечно, возможно, что все эти люди и замеры неправы, а рандомный чел из инета - прав, но что то я сомневаюсь =)

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

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

А не помню =) Стоит поправить, да, или вынести инициализацию из ленивой в "при старте программы"

Есть и другое решение вопроса производительности - кэш. Мы реализовали банальный map по typeid и это стало тоже в разы быстрее. Да, всё ещё требует rtti, зато работает с любыми наследованиями (множественным виртуальным), не требует модификации базового класса и тд. https://github.com/CrazyPandaLimited/panda-lib/blob/master/clib/src/panda/cast.h

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

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

А как вы бы писали AST без наследования, ну, например, на С++?

Можно пойти немного дальше и носить в классе не enum, а референс на некую static const структуру описывающую свойства конкретного класса. Но фактически это получается таблица виртуальных функций сделанная вручную. И там могут быть конструкторы копирования, перемещения и т.п., информация и размере/выравнивании класса и т.п.

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

Мне кажется, что вы только что придумали RTTI из std)

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

Или помимо dynamic_cast у вас меняется что-то ещё, например, происходит отказ от виртуальных функций, что существенно влияет на работу оптимизатора (который не может заглянуть в тело виртуальной функции).

Отвечая на ваш вопрос: исключения в работе мы не используем, виртуальные функции в некоторых местах есть. По приведённым бенчмаркам видно, что предложенный способ, на некоторых проектах даёт по 10-15% перфы. Если вам этого недостаточно, то возможно вы не на том языке пишете.

Есть ещё пара вариантов. Первый это std::variant с автоматическим kind. Второй - написать один шаблонный visit(Stmt&, Visitor) со switch/function table и повыкидывать все цепочки if...else if.

Дерево на вариантах - это наверное очень весело. Надо будет попробовать. Вообще, по поводу ast_visit был хороший доклад моего коллеги с С++ Russia 2022. К сожалению, он ещё не вышел, но я могу поделиться слайдами. Кстати, про кейс со switch: на сколько я помню, он не очень хорошо с ним дружит, т.к. достать индекс типа в variant стандартными средствами нельзя.

Ну с variant надо накладные считать, экономит указатель и косвенную адресацию но всегда ест max(sizeof). Ну и таблицы в нем обычно если не boost. А switch я имел в виду именно по Stmt, не по variant, простой перебор всех kind: template<Vis> visit(Stmt&, Vis) ... case If: vis(static_cast<If&>)... Чтобы не писать цепочки if dyn_cast, а использовать перегрузки. Например деструктор без virtual: visit(stmt, (auto& s) { delete &s; });

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

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

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

То есть это абсолютно бесполезная затея.

Насчёт визитов - внезапно std::visit / variant. Или же ещё лучше(в плане расширяемости и удобства) вариант с стиранием типов
https://github.com/kelbon/AnyAny

Динамик каст делает больше чем просто енамчик, там сохраняется информация о иерархии. Например с вашей реализацией вы не сможете привести к другой базе, то есть это не точно тот тип, но находящийся в той иерархии
На самом деле мало кто задумывается как работает dynamic_cast. С его помощью можно привести не просто к другому типу, который является наследником, а к типу вообще находящемуся во вне иерархии того или иного класса.
Ну например, можно рассмотреть простейшую реализацию COM на dynamic_cast<>:
#include <iostream>
#include <memory>
#include <string>

// a.h include

struct a_base {

    virtual std::string who() = 0;
    virtual ~a_base() = default;
};

struct a : a_base {

    std::string who() override { return "a class"; }
};

// b.h include

struct b_base {

    virtual std::string who() = 0;
    virtual ~b_base() = default;
};

struct b : b_base {

    std::string who() override { return "b class"; }
};

// c.h include

struct c : a, b 
{
};

// somewhere else

b_base* create_b()
{
    return new c;
}

// main

int main()
{
    b_base* b = create_b();
    std::cout << b->who() << "\n";

    a_base* a = dynamic_cast<a_base*>(b);

    if (a != nullptr) {

        std::cout << "receive a_base from pointer to b_base\n";
        std::cout << a->who() << "\n";

    } else {

        std::cout << "can't receive a_base from pointer to b_base\n";
    }

    delete b;
    b = nullptr;

    return 0;
}
Обратите внимание, что иерархия класса a и класса b могут друг о друге ничего не знать, а также оба вообще могут не знать, что в программе есть класс, который наследует их оба. Однако это не мешает dynamic_cast отслеживать эту связь в рантайме и получать через b_base указатель на a_base.
В С++ всё это называет cross cast. Идея думаю понятна.

Безусловно, иерархии класса a и класса b могут ничего не знать друг о друге, но о них знает класс c так как наследует и а, и b. А он, в свою очередь, является динамическим типом объекта который находится за указателем b (b_base*) и содержит информацию об обоих иерархиях на основании которой и работает dynamic_cast.
Просто изначальный Ваш посыл воспринимается как-будто эти две иерархии вообще никак не связаны, но это совсем не так для класса c.

Ну и что с того, что с наследует a и b? От этого связывание не становится статическим, а всё-равно происходит динамически в рантайме, при работе программы. Пофантазируйте: определения с у вас вообще может не быть, если подлинкована либа или подгружена dll с неизвестным вам классом.
(вопросы совместимости RTTI и целесообразности такого подхода на практике уберем за скобки)

Извиняюсь, я сказал статический тип, на самом деле имел ввиду динамический тип объекта на который указывает указатель b со статическим типом b_base.
И не спорю что связывание не является статическим, а лишь говорю, что для независимых иерархий dynamic_cast не работает, что, как мне показалось вы имели ввиду в своем комментарии. Если это не так, то ок.

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

Нет, там всё правильно написано. Пугает что ваш комментарий кто то лайкнул

Но много ли людей использует виртуальное наследование в 2022 году?


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

А что плохого в виртуальном наследовании?

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

А, ну просто прозвучало как-то слегка уничижительно, мол «много ли в 2022 используют new/delete» я уж думал это кошмарный ужас) так да, я понимаю что это zero overhead а не zero cost — нас cost устраивает.
Sign up to leave a comment.