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

Благодаря reflection можно:
— Легко создавать редакторы, в том числе интерфейсов, так как есть удобный доступ к мета-информации о всех свойствах ваших объектов;
— Добавить binding для многих скриптовых языков сразу (Lua, Python, JavaScript и т.д.);
— Использовать мета-информацию для автоматической сериализаци;
— Использовать как фабрику объектов, создавая нужные экземпляры, имея лишь строку с именем тип;
— Использовать в качестве более легковесной замены dynamic_cast;
— И прочее прочее прочее, в зависимости от фантазии и потребностей.

Дальше идет обзор каждой библиотеки по очереди в силу моих скромных возможностей. Для каждой:
— короткое описание;
— пример binding'а и использования для такого класса:

class Test
{
public:
  int func(int a, int b)
  {
    return a + b;
  }
};

— результаты замера производительности на i5 3570K, Windows 8, Visual Studio 2013, Release x86 (замерялись отдельно 10 000 000 вызовов метода класса и отдельно 10 000 000 поиск метаметода+вызов).

Рассматривались только библиотеки, не требубщие дополнительных шагов построения и инструментов (вроде qt moc и gccxml).

Библиотеки перечислены в порядке возрастания личного интереса к ним.

1) Luabind


image
github.com/rpavlik/luabind

Сейчас для binding'а в Lua используется Luabind (rpavlik's fork), но результирующую мета-информацию больше ни для чего особо не используешь.

Пример
luabind::module(state)
[
  luabind::class_<Test>("Test")
    .def("func", &Test::func)
];


local obj = Test()
obj:func(1, 2)


Benchmark
— Invoke — 1100ms
— FindMetaMethod+Invoke — 1580ms

2) Camp


image
projects.tegesoft.com/pm/projects/camp
github.com/tegesoft/camp

Создавалось французской компанием под вдохновением от luabind. Выглядит довольно культурно и проработано.
Правда заметно не обновлялось уже года 4.

Пример
CAMP_TYPE(Test)

camp::Class::declare<Test>("Test")
  .function("func", &Test::func);

Test obj;
camp::Class t = camp::classByName("Test");
camp::Function m = t->function("func");
camp::Value v = m->call(obj, camp::Args(1, 2));
auto result = v.to<int>()


Benchmark (сбилдить не удалось, взял результаты с сайта другой библиотеки)
— Invoke — 6889ms < — совсем грустно

3) cpgf


image
www.cpgf.org/document/index.html
github.com/cpgf/cpgf

Основной автор вроде китаец. Выглядит проработано, но интерфейс довольно усложнен и выглядит код совсем не лаконично. Много приставок, добавок в именовании, различных интерфейсов, правил использования (например, как и когда передается владение). На простом примере не видно, но если посмотреть tutorial, то становится сильно заметно — github.com/cpgf/cpgf/blob/develop/samples/tutorials/a01_reflect_to_global.cpp
При этом, все сведено к единому интерфейсу, что конечно радует.

Большой плюс — хорошая документация.

Из дополнительных наворотов — сериализация, готовые решения для биндинга в Lua/JavaScript/Python, tweening, своя система событий.

Багофиксы были еще в декабре, то есть проект не мертв.

Пример
cpgf::GDefineMetaClass<Test>
  ::define("Test")
  ._method("func", &CpgfTest::func);

Test obj;
cpgf::GMetaClass* t = cpgf::findMetaClass("Test");
cpgf::GMetaMethod* m = t->getMethod("func");
cpgf::GVariant v = m->invoke(&obj, 1, 2);
auto result = cpgf::fromVariant<int>(v);


Benchmark
— Invoke — 1000ms
— FindMetaMethod+Invoke — 1135ms < — быстрее, чем luabind

4) RTTR


image
www.axelmenzel.de/projects/coding/rttr

Автор, вроде, немец. Ура — C++11. Активно развивается, красивый синтаксис, современные возможности, очень даже радует. В ближайшее время должна появиться новая версия с существенным рефакторингом.

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

Пример
RTTR_DECLARE_STANDARD_TYPE_VARIANTS(Test);

RTTR_REGISTER
{
  rttr::class_<Test>()
    .method("func", &Test::func);
}

Test obj;
rttr::type t = type::get("Test");
rttr::method m = t.get_method("func");
rttr::variant v = m.invoke_variadic(obj, {1, 2});
auto result = v.get_value<int>();


Benchmark
— Invoke — 1780ms
— FindMetaMethod+Invoke — 2290ms

5) uMOF


image
bitbucket.org/occash/umof

Автор русскоговорящий, активно отвечает на все вопросы. Создавалось, как я понял, под большим впечатлением от QT. Снова ура — C++11 (все эти constexpr и прочие радости). Активно развивается. В ближайшее время должна появиться новая версия с существенным рефакторингом и ускорением, она и тестировалась.

Условный минус — для создания мета-информации надо использовать макросы, но это связано с особенностями реализации (обьяснено позже).

Пример
U_DECLARE_API(Test, METHODS);
U_DECLARE_METHODS(Test)
{
  U_METHOD(func)
};

const Api* api = U_API(Test)::api();
Method m = api->method(api->indexOfMethod("func(int,int)"));
int result;
m.invoke(&obj, result, {1, 2}));


Benchmark
— Invoke — 115ms < — магия (в старой версии 420, что тоже на голову выше других)
— FindMetaMethod+Invoke — 1780ms < — уже не так хорошо, но скорее всего и это будет оптимизировано

Invoke почти в 9 раз быстрее самого лучшего результата других библиотек.

Про это писал сам автор, сравнивая свое решение с другими. Статься с графиками и картинками для еще старой версии здесь — www.gamedev.net/page/resources/_/technical/general-programming/implementing-a-meta-system-in-c-r3905
Там же есть сравнение того, как какие библиотеки влияют на время компиляции и линковки проекта и насколько утяжеляют бинарник.

Общий результат
Luabind Camp cpgf RTTR uMOF
Invoke 1100 6889 1000 1780 115
FindMetaMethod+Invoke 1580 x 1135 2290 1780


Заключение


Самые современные, лаконичные и живые библиотеки — uMOF, RTTR.
Наиболее богатая по функциональности — cpgf.
Выдающиеся по производительности:
— uMOF (благодаря особенностям реализации, невероятно быстрый invoke и минимальный overhead в компиляции и размере бинарника);
— cpgf (на данный момент самый быстрый результат по FindMetaMethod+Invoke, что и является наиболее частым сценарием использования).

Предложения для обсуждения


1) Какую все-таки разработчику игр выбрать библиотеку?
cpgf солидно проработан и показывает хорошие результаты, но так ли важен этот overhead для invoke? Может отдать предпочтение, например, более современному RTTR, т.к. 2290ms на 10000000 вызовов это 4366812 вызовов в секунду. 72780 вызовов на каждом кадре при 60 FPS. То есть, если на каждом кадре делается порядка 700 вызовов, то при 60 FPS это составит меньше 1% от времени кадра.
При этом uMOF показывает выдающиеся результаты, что позволило бы использовать его с максимальной интенсивностью (что и планируется). Но он еще не закончен, не хватает некоторого функционала.
2) Может быть, какая-то библиотека пропущена? Можно было бы добавить ее в обзор.
3) Что вы знаете из своего опыта про любую из этих библиотек? Здесь обзор был поверхностный, может ваш опыт говорит о каких-то существенных особенностях в пользу или против какой-то библиотеки.

Заранее большое спасибо за ваши комментарии.

— UPDATE1: Сравнение производительности библиотек с luabind не совсем корректное. Т.к. вызов метода в luabind подразумевает не только поиск мета-метода, но и работу с виртуальной машиной lua. Постараюсь обновить статью, как только будет больше полезной информации на эту тему.

Комментарии (16)


  1. dyadyaSerezha
    30.04.2015 19:39

    «Использовать в качестве более легковесной замены dynamic_cast» — вот тут я не понял. Может быть, есть бенчмарки каста? А то на мой взгляд должно быть все с точностью до наоборот.


    1. nikolayz
      30.04.2015 22:50

      Стандартный dynamic_cast жутко медленный. Во многих реализациях он использует сравнение строк, чтобы найти нужный объект. Поэтому нет ничего удивительного в том, чтобы RTTI-библиотека предлагала собственный, более быстрый вариант.


      1. dyadyaSerezha
        01.05.2015 15:31
        -2

        А сдается мне, что в 99% случаев dynamic_cast должен банально пробежаться по адресам виртуальных таблиц потомков класса. И это явно не 1 сек, как представленные бенчмарки, а раз так в миллион быстрее. Поэтому и хочу конкретных цифр.


        1. nikolayz
          01.05.2015 17:25

          Вынужден вас разочаровать, но на некоторых платформах и компиляторах используется именно сравнение строк. Основная причина — необходимость корректной работы dynamic_cast между динамическими библиотеками.

          Конкретные цифры легко гуглятся, например вот: tinodidriksen.com/2010/04/14/cpp-dynamic-cast-performance
          Там же есть и ссылка на исходник тестовой программы.

          У Visual С++ цифры особо красноречивы. У GCC и Clang все гораздо лучше, но тоже имеются случаи, когда производительность падает примерно в 7-8 раз относительно вызова виртуального метода.

          Если не верите, что используется именно сравнение строк, посмотрите например вот этот пост (в конце есть фрагмент кода из libc++): www.nullptr.me/2014/07/01/libcxxabi-__dynamic_cast-random-strings


          1. dyadyaSerezha
            01.05.2015 20:22

            Два момента.

            1. Я пропустил, что конечные результаты даны для 10 млн вызовов, а не для одного. Что… слегка меняет всю картину :)

            2, Спасибо за первую ссылку. Там данные в неопределенных тиках, я прогнал тот же тест для моего дохлого Селерона, тоже для 10 млн. вызовов. только для чистоты вычел время самого цикла и прочих вчпомогательных операций.
            Результаты для VisualStudio 2013:
            — reinterpret_cast: 16 ms
            — dynamic_cast: 100-1000 ms, в зависимости от пары классов «откуда-куда».

            Ксатати, тот пример по первой ссылке — прекрасный пример того, как не надо писать бенчмарки. ;)

            Вывод: 10 млн. динамических кастов занимает 0.1-1.0 сек на дохлом процессоре. И что, это очень медленно?


            1. nikolayz
              01.05.2015 22:53

              Для многих задач это более, чем приемлемо. Тот же JavaScript исполняется еще медленнее, что, впрочем, не помешало портировать на него Unreal Engine.

              Собственно я пытался донести мысль, что dynamic_cast — это относительно затратная операция, и у авторов приведенных в статье библиотек есть основания, чтобы реализовывать свой велосипед. И что этот велосипед в некоторых случаях будет работать быстрее, чем стандартная реализация.

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


              1. dyadyaSerezha
                02.05.2015 00:02

                Абсолютно согласен. Если dynamic_cast стновится узким местом, то явно «что-то надо в консерватории подправить».


    1. Neovet Автор
      04.05.2015 08:56

      Есть benchmark на сайте библиотеки RTTR — www.axelmenzel.de/articles/rtti
      Сама библиотека показала здесь не лучшие результаты в производительности, но и их достаточно, чтоб обогнать dynamic_cast.
      Хотя, может, у автора специально проводились какие-то оптимизации под это.
      А вообще да, dynamic_cast это явно не важнейшая задача для reflection, добавил в список больше для примера.


  1. NeoCode
    30.04.2015 20:08
    +1

    Интересная тема, хотя статья слишком краткая — чтобы понять, нужно открывать каждую библиотеку и разбираться.
    И еще мысль — рефлексия это достаточно важная фича, эмулирующая отсутствующие возможности языка программирования… хорошо бы что-то наиболее удачное появилось в Бусте. Вот здесь ничего нет, но если погуглить, находятся какие-то неофициальные реализации:
    boost-extension.redshoelace.com
    bytemaster.bitshares.org/boost_reflect/index.html


    1. Neovet Автор
      04.05.2015 09:25

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

      Есть еще boost.mirror и boost.reflex.
      boost.mirror выглядит просто гигантским монстром, разбит на несколько под-мета-библиотек и т.д. (хотя, может, и ошибаюсь).
      boost.reflex — маловато информации, версия 0.1, не совсем понятно что за продукт и какая его судьба хотя бы в ближайшем будущем.


  1. mapron
    01.05.2015 03:17

    Я вообще думаю, что самую эффективную скорость по рефлексии можно получить с помощью кодогенерации.
    Но я рекомендую почитать еще вот эту статью
    woboq.com/blog/reflection-in-cpp-and-qt-moc.html
    Авторы задумались о том, какие возможности стоит добавить в компилятор C++, чтобы генератор moc был не нужен.

    Очень интересный черновик по рефлексии в С++


  1. namespace
    01.05.2015 11:14

    Жаль не упомянули то, что в Qt, за счет мета-объектного компилятора есть встроенные reflections (у всех QObject-классов).


    1. Neovet Автор
      04.05.2015 09:30

      Спасибо за добавление. Не упомянул, т.к. reflection в QT не основная задача и тянет с собой много чего другого.
      Опять же, решения с дополнительным toolchain не рассматривались.
      Кстати, автор библиотеки uMOF как раз таки, похоже, и вдохновлялся QT.
      Его решение тоже хранит мета-информацию в статической памяти, но задается она вручную.


  1. FoxCanFly
    01.05.2015 12:59
    +1

    Все выглядит как костыли (как и любые попытки реализовать библиотеками то, что должно быть в самом языке), ждем когда в C++17 все же добавят compile-time reflection на уровне языка.


    1. Neovet Автор
      04.05.2015 09:27

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


      1. DancingOnWater
        15.05.2015 15:23

        Ну основные компиляторы поддерживали C++14 в году этак 2013-ом…

        P.S. кроме майкрософтовского…