философия - шаблоны c++ хабр




Быстрая реализация простого, виртуального, наблюдателя-типа, шаблона в c++? (2)

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

Как можно реализовать схему следующего кода с наименьшим количеством перенаправления / операций?

Это должно быть сделано в стандартном c ++, до 17.

class A{
    virtual void Update() = 0; // A is so pure *¬*
};

class B: public A
{
    override void Update() final
    {
        // DO B STUFF
    }
}

class C: public A
{
    override void Update() final
    {
        // DO C STUFF
    }
}

// class...

int main()
{
    std::vector<A*> vecA{};

    // Insert instances of B, C, ..., into vecA

    for(auto a: vecA) // This for will be inside a main loop
        a->Update(); // Ridiculous amount of calls per unit of time

    // Free memory
}

PS: Если перечисление, переключатель и макросы действительно лучший вариант, я думаю, я просто попытаюсь освежить свои кеши и придумать лучший дизайн.

PSS: Я знаю, что это микро-оптимизация ... Черт, мне нужно нано или даже пико оптимизировать это (образно говоря), поэтому я просто проигнорирую любые утилитарные ответы, которые могут возникнуть.


Если вам действительно нужна виртуальная отправка, один из способов ускорить отправку для одного и того же виртуального метода в списке объектов разных производных типов - это использовать то, что я назову тип-unswitching .

Скорее аналогично отключению цикла , это преобразует единый цикл, вызывающий метод на каждом объекте, в порядке порядкового номера в N циклов (для N поддерживаемых типов), каждый из которых вызывает метод для всех объектов определенного типа. Это позволяет избежать первичной стоимости непредсказуемой виртуальной отправки: неверные предсказания филиала, подразумеваемые косвенным вызовом неизвестной, непредсказуемой функции в таблице vtable.

Общая реализация этого метода включает в себя первый проход для разбивки объектов по типу: информация об этом разделе используется вторым проходом, который имеет отдельные циклы для каждого каждого типа 1 , вызывая метод. Обычно это не связано с непредсказуемыми ветвями, если их применять осторожно.

В случае двух производных классов B и C вы можете просто использовать растровое изображение для хранения информации о типе. Вот пример реализации, используя типы A , B , C из кода в вопросе:

void virtual_call_unswitch(std::vector<A*>& vec) {

    // first create a bitmap which specifies whether each element is B or C type
    std::vector<uint64_t> bitmap(vec.size() / 64);

    for (size_t block = 0; block < bitmap.size(); block++) {
        uint64_t blockmap = 0;
        for (size_t idx = block * 64; idx < block * 64 + 64; idx++) {
            blockmap >>= 1;    
            blockmap |= (uint64_t)vec[idx + 0]->typecode_ << 63;
        }
        bitmap[block] = blockmap;
    }

    // now loop over the bitmap handling all the B elements, and then again for all the C elements

    size_t blockidx;
    // B loop
    blockidx = 0;
    for (uint64_t block : bitmap) {
        block = ~block;
        while (block) {
            size_t idx = blockidx + __builtin_ctzl(block);
            B* obj = static_cast<B*>(vec[idx]);
            obj->Update();
            block &= (block - 1);
        }
        blockidx += 64;
    }

    // C loop
    blockidx = 0;
    for (uint64_t block : bitmap) {
        while (block) {
            size_t idx = blockidx + __builtin_ctzl(block);
            C* obj = static_cast<C*>(vec[idx]);
            obj->Update();
            block &= (block - 1);
        }
        blockidx += 64;
    }
}

Здесь typecode является общим полем в A которое идентифицирует тип объекта, 0 для B и 1 для C Что-то подобное необходимо для того, чтобы сделать категоризацию по типу выполнимой (она не может быть виртуальным вызовом, так как непредсказуемый вызов - это то, чего мы пытаемся избежать в первую очередь).

Немного оптимизированная версия выше показывает примерно 3,5-кратное ускорение для неперемещенной версии по сравнению с обычным виртуальным циклом с виртуальной версией, синхронизирующей примерно 19 циклов в рассылке, а неперестановленная версия - около 5.5. Полные результаты:

-----------------------------------------------------------------------------
Benchmark                                      Time           CPU Iterations
-----------------------------------------------------------------------------
BenchWithFixture/VirtualDispatchTrue       30392 ns      30364 ns      23033   128.646M items/s
BenchWithFixture/VirtualDispatchFakeB       3564 ns       3560 ns     196712   1097.34M items/s
BenchWithFixture/StaticBPtr                 3496 ns       3495 ns     200506    1117.6M items/s
BenchWithFixture/UnswitchTypes              8573 ns       8571 ns      80437   455.744M items/s
BenchWithFixture/StaticB                    1981 ns       1981 ns     352397    1.9259G items/s

VirtualDispatchTrue - это простой цикл, вызывающий Update() для указателя типа A :

for (A *a : vecA) {
    a->Update();
}

VirtualDispatchFakeB передает указатель на B* (независимо от типа базового типа) перед вызовом Update() . Поскольку B::Update() является окончательным, компилятор может полностью де виртуализировать и встроить вызов. Конечно, это совсем не так: обрабатывает любые объекты C как B и поэтому вызывает неправильный метод (и полностью UB) - но он здесь, чтобы оценить, как быстро вы могли бы вызвать методы для вектора указателей если каждый объект был тем же статически известным типом.

for (A *a : vecA) {
    ((B *)a)->Update();
}

StaticBPtr выполняет StaticBPtr по std::vector<B*> а не std::vector<A*> StaticBPtr . Как и ожидалось, производительность такая же, как и код «поддельный B» выше, поскольку цель Update() статически известна и полностью прослеживается. Это здесь как проверка здравомыслия.

UnswitchTypes - это тип разворота, описанный выше.

StaticB выполняет StaticB по std::vector<B> . То есть смежно выделенные объекты B а не вектор указателей на объекты B. Это устраняет один уровень косвенности и показывает что-то вроде наилучшего случая для этого макета объекта 2 .

Доступен полный источник .

Ограничения

Побочные эффекты и порядок

Ключевым ограничением этой методики является то, что порядок вызовов Update() не имеет значения. Хотя Update() по-прежнему вызывается один раз для каждого объекта, порядок явно изменился. Пока объект не обновляет какое-либо изменяемое глобальное или общее состояние, это должно быть легко удовлетворить.

Поддержка двух типов

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

Это ограничение довольно легко удалить. Во-первых, растровый подход может быть расширен. Например, для поддержки 4 типов могут быть созданы два одинаковых растровых изображения, для которых соответствующие биты каждого растрового изображения, по существу, предназначены для 2-битового поля, кодирующего тип. Петли аналогичны, за исключением того, что во внешнем цикле они & и ~ растровые изображения объединяются таким образом, что по всем 4 типам. Например:

// type 1 (code 11)
for (size_t i = 0; i < bitmap1.size(); i++) {
        block = bitmap1[i] & bitmap2[i];
        ...
}


// type 2 (code 01)
for (size_t i = 0; i < bitmap1.size(); i++) {
        block = ~bitmap1[i] & bitmap2[i];
        ...
}

...

Другой подход заключается в том, чтобы не использовать растровые изображения вообще, а просто хранить массив индексов для каждого типа. Каждый индекс в массиве указывает на объект этого типа в основном массиве. По сути, это 1-х расовая сортировка по типу кода. Это, вероятно, немного упрощает сортировку типов, но потенциально ускоряет логику итерации цикла ( x & (x - 1) и ctz исчезает, ценой другой косвенности).

Исправлено количество поддерживаемых типов

Приведенный выше код поддерживает фиксированное количество известных типов времени компиляции (а именно, B и C ). Если вводится новый тип, приведенный выше код либо сломается, либо, безусловно, не сможет вызвать Update() для этих новых типов.

Тем не менее, просто добавить поддержку для неизвестных типов. Просто сгруппируйте все неизвестные типы, а затем только для этих типов, выполните полную виртуальную отправку в цикле (т. Е. Update() непосредственно на A* ). Вы заплатите полную цену, но только для типов, которые вы явно не поддерживали! Таким образом, техника переносит общность механизма виртуальной диспетчеризации.

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

2 «Объектом макета» я имею в виду объекты B которые все еще имеют виртуальные методы и, следовательно, vtable. Если вы удалите все виртуальные методы и избавитесь от vtable, все будет намного быстрее, так как компилятор затем векторизовать добавление к компактным полям. Дискуссионный стол работает.


Как сказал первый комментарий, у вас есть проблема XY. Сортировка / переупорядочение в порядке, и у вас много объектов, а не огромное количество разных классов, и нет необходимости поддерживать типы, которые ваш код не знает во время компиляции. Полиморфизм + виртуальное наследование - неправильный выбор .

Вместо этого используйте N разных контейнеров, по одному для каждого типа объектов, без косвенности. Предоставление компилятору inline B::Update() в цикл по всем объектам B намного лучше . (Для тривиального примера приращения одного члена int мой статический анализ производительности от взгляда на asm ставит его примерно на 24 раза быстрее на Skylake с горячими данными в кеше L1D. Автоинъекция AVX2 против call в цикле действительно такова огромно.)

Если между объектами, в том числе между различными типами объектов, был некоторый требуемый порядок, тогда был бы подходящим какой-то полиморфизм или ручная диспетчеризация. (например, если важно, какой порядок вы обработали vecA , если все объекты B отделенные от всех объектов C , не были бы эквивалентны.)

Если вы заботитесь о производительности, вы должны понимать, что увеличение источника может упростить работу компилятора / выхода asm. Проверка / диспетчеризация по типу каждого объекта внутри внутреннего цикла является дорогостоящей. Использование любого типа указателя функции или перечисления для отправки по каждому объекту может легко пострадать от неверных предсказаний филиала, когда у вас есть сочетание разных объектов.

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

Списание отдельного цикла для каждого контейнера с другим типом вроде как полностью разворачивает цикл по разным типам после подъема типа диспетчеризации из внутреннего цикла. Это необходимо, чтобы компилятор ввел вызовы, которые вы хотите, если есть много объектов каждого типа. Inlining позволяет сохранять константы в регистре по всем объектам, активировать автоинъекцию SIMD для нескольких объектов и просто избегать накладных расходов на фактический вызов функции. (И сам вызов, и разлив / перезагрузка регистров.)

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

Виртуальная диспетчеризация работает только с уровнем косвенности (например, вектором указателей, как вы используете), что означает, что вам нужно каким-то образом управлять объектами с указателями, например, выделяя их из vector<B> poolB и vector<C> poolC . Хотя я не уверен, что большинство реализаций vector<> используют realloc() когда им нужно расти; new/delete API не имеет realloc , поэтому vector может копировать каждый раз, когда он растет, вместо того чтобы пытаться расширить существующее распределение на месте. Проверьте, что делает ваша реализация на C ++, поскольку она может сосать по сравнению с тем, что вы можете делать с malloc / realloc.

И BTW, должно быть возможно сделать new / delete с помощью RAII без дополнительных накладных расходов для распределения / освобождения, если все ваши классы тривиально разрушаемы. (Но обратите внимание, что unique_ptr может победить другие оптимизации для использования вектора указателей). std::unique_ptr предупреждает, что UB уничтожает его с помощью указателя на базовый класс, поэтому вам, возможно, придется сворачивать самостоятельно. Тем не менее, на gcc на x86-64, sizeof(unique_ptr<class C>) равен только 8, поэтому он имеет только один элемент-указатель. Но что бы то ни было, отдельно выделяя циллионы крошечных объектов, так не делают этого в первую очередь .

Если вам нужна какая-то отправка, как указано в заголовке,

Если объекты имеют одинаковые размеры, вы действительно хотите перебирать объекты, а не указатели на объекты . Это позволило бы избежать лишнего размера кеша вектора указателей, и это позволит избежать дополнительной задержки лазера указателя, которую не удалось выполнить из-за порядка выполнения, чтобы сохранить рабочие блоки. Но виртуальное наследование C ++ не предоставляет никакого стандартного способа получения полиморфизма для union upoly { B b; C c; } poly_array[1024]; union upoly { B b; C c; } poly_array[1024]; Вы можете взломать это с помощью reinterpret_cast<> таким образом, что, вероятно, работает на x86-64 gcc, но вы, вероятно, не должны этого делать. См. @ BeeOnRope's followup: Непрерывное хранение полиморфных типов . (Также более старый Q & A: полиморфизм C ++ объекта в массиве ).

Если вам это нужно, самым высокопроизводительным способом, вероятно, будет создание его с помощью enum чтобы индексировать таблицу указателей функций (или использовать switch() если ваши функции могут быть встроены). Если ваши функции не встроены, switch() в кучу case функциональных вызовов обычно не оптимизируется до таблицы указателей функций, даже если все они имеют одни и те же аргументы (или без аргументов). Обычно вы получаете таблицу перехода в блок инструкций вызова, вместо того, чтобы выполнять косвенный call . Таким образом, в каждой отправке есть дополнительный прыжок.

C ++ 17 std::visit с std::variant<B, C> (используя не виртуальное наследование для B и C), похоже, дает вам отправку на основе внутреннего enum . std::visit использует свою собственную таблицу перехода для отправки даже с двумя возможными типами вместо того, чтобы вставлять их как в условную ветвь. Он также должен постоянно проверять «неинициализированное» состояние. Вы можете получить хороший код, если вручную работаете с B *tmp = std::get_if<B>(&my_variant) и __builtin_unreachable() чтобы сообщить gcc, что nullptr не является возможным. Но в этот момент вы можете просто свернуть свой собственный struct polymorph { enum type; union { B b; C c; }; }; struct polymorph { enum type; union { B b; C c; }; }; (с не виртуальными функциями), если вам не требуется «неинициализированное» состояние. Связанный: полиморфизм C ++ объекта в массиве .

В этом случае у вас есть только одна функция, поэтому вы можете поместить указатель функции внутри каждого объекта в качестве члена . Как void (*m_update)(A* this_object) . В вызывающем, передайте указатель на объект как void* или A* , так как это функция, не являющаяся членом. Реализация функции будет reinterpret_cast<C*>(this_object) . (Не dynamic_cast : мы делаем свою собственную диспетчеризацию, а не используя C ++).

Если вы хотите использовать B и C в других контекстах, где член-указатель функции будет занимать место без каких-либо преимуществ, вы можете сохранить указатели на функции в отдельном контейнере, а не в базовом классе . Таким образом, это было бы for(i=0..n) funcptrs[i]( &objects[i] ); , Пока ваши контейнеры не синхронизируются, вы всегда передаете указатель на функцию, которая знает, что с ней делать. Используйте это с union {B b; C c} objects[] union {B b; C c} objects[] (или vector<union> ).

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

Вы можете использовать std::function<> для хранения указателей на функции-члены экземпляра, но на x86-64 gcc это 32-байтовый объект. Лучше, чтобы ваш кеш мог использовать только 8-байтовые регулярные указатели функций и писать код, который знает, чтобы передать явный указатель, эквивалентный this указателю.

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

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

Накладные расходы:

Я рассмотрел созданный компилятором asm (gcc и clang targeting x86-64) для нескольких способов сделать это.

Источник для нескольких способов сделать это + asm из x86-64 clang 5.0 в проводнике компилятора Godbolt . Вы можете перевернуть его на gcc или архитектуры без архитектуры x86.

class A{
    public:
    virtual void Update() = 0; // A is so pure *¬*
};

struct C : public A {
    int m_c = 0;
    public:
    void Update() override final
    {  m_c++;  }
};
int SC = sizeof(C);  // 16 bytes because of the vtable pointer

C global_c;  // to instantiate a definition for C::Update();

// not inheriting at all gives equivalent asm to making Update non-virtual
struct nonvirt_B //: public A
{
    int m_b = 0;
    void Update() //override final
    {  m_b++;  }
};
int SB = sizeof(nonvirt_B);  // only 4 bytes per object with no vtable pointer

void separate_containers(std::vector<nonvirt_B> &vecB, std::vector<C> &vecC)
{
    for(auto &b: vecB)        b.Update();
    for(auto &c: vecC)        c.Update();   
}

clang и gcc автоматически векторизовать цикл через vecB с помощью AVX2 для обработки 8 элементов int параллельно, поэтому, если вы не узкополотите пропускную способность памяти (то есть горячий в кеше L1D), этот цикл может увеличивать 8 элементов за такт. Этот цикл работает так же быстро, как цикл над vector<int> ; все встраивается и оптимизируется, и это просто увеличение указателя.

Цикл над vecC может выполнять только 1 элемент за такт , потому что каждый объект имеет 16 байтов (8 байтов vtable указатель, 4 байта int m_c , 4 байта заполнения на следующую границу выравнивания, потому что указатель имеет требование выравнивания 8B.) Без final , компилятор также проверяет указатель vtable, чтобы увидеть, действительно ли это C перед использованием встроенного C::Update() , иначе он отправляет. Это похоже на то, что вы получите для цикла над struct { int a,b,c,d; } vecC[SIZE]; struct { int a,b,c,d; } vecC[SIZE]; выполнение vecC[i].c++;

final разрешил полную девиртуализацию, но наши данные смешиваются с указателями vtable, поэтому компиляторы просто делают скалярное add [mem], 1 которое может работать только с 1 за такт (узкое место по 1 на пропускную способность хранилища часов, независимо от размера магазина, если он горячий в кеше L1D). Это в основном проигрывает SIMD для этого примера. (С -march=skylake-avx512 , gcc и clang делают некоторые смешные перетасовки или сбор / разброс, которые еще медленнее, чем скалярные, вместо того, чтобы просто загружать / восстанавливать весь объект и добавлять с помощью вектора, который только изменяет член int . Это разрешено, потому что он не содержит каких-либо летучих или атомных элементов и будет запускать 2 за такт с AVX2 или 4 за один такт с помощью AVX512.) Если ваши объекты размером до 12 байтов являются серьезным недостатком, если они небольшие, и у вас есть многие из них.

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

Поскольку вы упомянули теорему о разделительной оси , я надеюсь, вы не планируете хранить float x,y пар в каждом объекте. Array-of-structs в основном отстой для SIMD, потому что ему нужно много перетасовки, чтобы использовать x с y для одного y того же объекта . То, что вы хотите, это std::vector<float> x, y или подобное, поэтому ваш процессор может загружать 4 x значения в регистр и 4 y значения в другой регистр. (Или 8 одновременно с AVX).

См. Слайды: SIMD в Insomniac Games (GDC 2015) для ознакомления с тем, как структурировать ваши данные для SIMD и некоторые более продвинутые вещи. См. Также wiki для sse tag для получения дополнительных руководств. Кроме того, в wiki-ярлыке x86 имеется множество материалов для оптимизации x86 на уровне низкого уровня. Даже если вы не вручную векторизуете что-либо, с отдельными массивами для x и y есть хороший шанс, что компилятор может авто-векторизовать для вас. (Посмотрите на выход asm или на тест gcc -O3 -march=native vs. gcc -O3 -march=native -fno-tree-vectorize ). Возможно, вам понадобится -ffast-math для некоторых видов векторизации FP.

Виртуальная диспетчеризация C ++:

Написание его так, как вы делаете в вопросе, с виртуальным наследованием и

std::vector<A*> vecA{};

void vec_virtual_pointers() {
    for(auto a: vecA)
        a->Update();
}

Мы получаем этот цикл из clang5.0 -O3 -march=skylake

   # rbx = &vecA[0]
.LBB2_1:                         # do{
    mov     rdi, qword ptr [rbx]   # load a pointer from the vector (will be the this pointer for Update())
    mov     rax, qword ptr [rdi]   # load the vtable pointer
    call    qword ptr [rax]        # memory-indirect call using the first entry in the vtable
    add     rbx, 8                 # pointers are 8 bytes
    cmp     r14, rbx
    jne     .LBB2_1              # }while(p != vecA.end())

Таким образом, конечный указатель функции находится в конце цепочки из трех зависимых нагрузок. Выполнение вне порядка позволяет это совпадение между итерациями (если ветвь предсказывает правильно), но это слишком много накладных расходов только в общих инструкциях для front-end, а также в неправильном прецеденте. ( call [m] равен 3 uops, поэтому только сам цикл равен 8 uops и может выдавать только один за 2 цикла на Skylake. У Call / return тоже есть накладные расходы. Если вызываемый пользователь не является полностью тривиальным, мы, вероятно, не будем узким местом на переадресацию хранилища для того, чтобы нажимать / выталкивать обратный адрес. Цикл с вызовом функции быстрее, чем пустой цикл . (Я не уверен в пропускной способности независимых операций хранения / перезагрузки по одному и тому же адресу. Это обычно требует переименования памяти, что Скайлак не делает, чтобы не быть узким местом, если это называется маленьким, как здесь.)

Определение Клана для C :: Update ()

C::Update():                         # @C::Update()
    inc     dword ptr [rdi + 8]
    ret

Если это необходимо для настройки каких-либо констант, прежде чем вычислять что-то, было бы даже более дорогостоящим, чтобы он не был встроен. Таким образом, с виртуальной диспетчеризацией это, вероятно, работает примерно от одного на 3 до 5 тактов, а не около 1 члена за такт, на Skylake. (Или 8 членов за такт с AVX2 для не виртуального class B который не тратит места и делает автоматическую вектологию хорошо работать.) Http://agner.org/optimize/ говорит, что Skylake имеет пропускную способность по одному на 3 часа, так что скажем, 24x потеря производительности, когда данные горячие в кеш-памяти L1D. Разумеется, разные микроархитектуры будут разными. См. Wiki для x86 для получения дополнительной информации о x86 perf.

Союзный взлом:

Вероятно, вы никогда не должны использовать это, но вы можете видеть из asm, что он будет работать на x86-64 с clang и gcc. Я создал множество союзов и зациклился над ним:

union upoly {
    upoly() {}   // needs an explicit constructor for compilers not to choke
     B b;
     C c;
} poly_array[1024];

void union_polymorph() {
    upoly *p = &poly_array[0];
    upoly *endp = &poly_array[1024];
    for ( ; p != endp ; p++) {
        A *base = reinterpret_cast<A*>(p);
        base->Update();           // virtual dispatch
    }
}

У AB и C все есть их vtable в начале, поэтому я думаю, что это, как правило, будет работать. Мы asm, это в основном то же самое, с одним меньшим шагом в стрельбе. (Я использовал статический массив вместо вектора, так как я делал вещи простыми и C-like, сортируя, что делать.)

    lea     rdi, [rbx + poly_array]       ; this pointer
    mov     rax, qword ptr [rbx + poly_array]   ; load it too, first "member" is the vtable pointer
    call    qword ptr [rax]
    add     rbx, 16                       ; stride is 16 bytes per object
    cmp     rbx, 16384                    ; 16 * 1024
    jne     .LBB4_1

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

std::function from #include <functional>

Он может содержать любую вызывающую вещь. Но у него есть еще больше накладных расходов, чем отправка vtable, потому что это разрешено находиться в состоянии с ошибкой. Поэтому внутренний цикл должен проверять каждый экземпляр для этого и ловушку, если он есть. Кроме того, sizeof(std::function<void()>); 32 байта (на x86-64 System V ABI).

#include <functional>
// pretty crappy: checks for being possibly unset to see if it should throw().
std::vector<std::function<void()>> vecF{};
void vec_functional() {
    for(auto f: vecF)     f();
}

                                # do {
.LBB6_2:                                # =>This Inner Loop Header: Depth=1
    mov     qword ptr [rsp + 16], 0       # store a 0 to a local on the stack?
    mov     rax, qword ptr [rbx + 16]
    test    rax, rax
    je      .LBB6_5           # throw on pointer==0  (nullptr)
    mov     edx, 2            # third arg:  2
    mov     rdi, r14          # first arg: pointer to local stack memory (r14 = rsp outside the loop)
    mov     rsi, rbx          # second arg: point to current object in the vector
    call    rax               # otherwise call into it with 2 args
    mov     rax, qword ptr [rbx + 24]    # another pointer from the std::function<>
    mov     qword ptr [rsp + 24], rax    # store it to a local
    mov     rcx, qword ptr [rbx + 16]    # load the first pointer again
    mov     qword ptr [rsp + 16], rcx
    test    rcx, rcx
    je      .LBB6_5           # check the first pointer for null again (and throw if null)
    mov     rdi, r14
    call    rax               # call through the 2nd pointer
    mov     rax, qword ptr [rsp + 16]
    test    rax, rax
    je      .LBB6_12          # optionally skip a final call
    mov     edx, 3
    mov     rdi, r14
    mov     rsi, r14
    call    rax
.LBB6_12:                               #   in Loop: Header=BB6_2 Depth=1
    add     rbx, 32
    cmp     r15, rbx
    jne     .LBB6_2

.LBB6_13:                       # return
    add     rsp, 32
    pop     rbx
    pop     r14
    pop     r15
    ret

.LBB6_5:
    call    std::__throw_bad_function_call()
    jmp     .LBB6_16
    mov     rdi, rax
    call    __clang_call_terminate

Таким образом, существует до трех команд call если указатель не равен nullptr. Это выглядит намного хуже, чем виртуальная отправка.

Он немного отличается от clang -stdlib=libc++ , а не по умолчанию libstdc++ . ( https://libcxx.llvm.org/ ). Но все же три инструкции call во внутреннем цикле, с условностями, чтобы пропустить их или выбросить.

Если код-gen не отличается для разных видов function<T> , вероятно, даже не стоит смотреть на него на указатели на функции-члены, если вы можете написать более эффективную альтернативу.





micro-optimization