переключение на другой пользовательский распределитель -> распространение на поля-члены - PullRequest
6 голосов
/ 01 апреля 2019

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

Вот пустышкафрагмент ( coliru link ): -

class Allocator{ //can be stack/heap/one-frame allocator
    //some complex field and algorithm
    //e.g. virtual void* allocate(int amountByte,int align)=0;
    //e.g. virtual void deallocate(void* v)=0;
};
template<class T> class MyArray{
    //some complex field
    Allocator* allo=nullptr;
    public: MyArray( Allocator* a){
        setAllocator(a);
    }
    public: void setAllocator( Allocator* a){
        allo=a;
    }
    public: void add(const T& t){
        //store "t" in some array
    }
    //... other functions
};

Однако у моего однокадрового распределителя есть недостаток - пользователь должен быть уверен, что каждый объект, выделенный одним-распределитель кадров должен быть удален / освобожден в конце временного шага.

Проблема

Вот пример варианта использования.

Я использую однокадровый распределитель для хранения временных результатов M3 (перекрывающаяся поверхность от обнаружения столкновений; вики-ссылка ) вФизический Двигатель.

Вот фрагмент кода.
M1, M2 и M3 - это все многообразия, но с разной степенью детализации: -

Allocator oneFrameAllocator;
Allocator heapAllocator;
class M1{};   //e.g. a single-point collision site
class M2{     //e.g. analysed many-point collision site
    public: MyArray<M1> m1s{&oneFrameAllocator};
};
class M3{     //e.g. analysed collision surface
    public: MyArray<M2> m2s{&oneFrameAllocator};
};

Обратите внимание, что я установилРаспределитель по умолчанию равен oneFrameAllocator (потому что он экономит CPU).
Поскольку я создаю экземпляр M1, M2 и M3 только как временные переменные, он работает.

Теперь, Я хочу кэшировать новый экземпляр M3 outout_m3=m3; для следующего timeStep.
(^ Чтобы проверить, является ли столкновение только началом или только концом)

Другими словами, я хочу скопировать выделен один кадр m3 до выделена куча output_m3 при #3 (показано ниже).

Вот игровой цикл: -

int main(){
    M3 output_m3; //must use "heapAllocator" 
    for(int timeStep=0;timeStep<100;timeStep++){
        //v start complex computation #2
        M3 m3;
        M2 m2;
        M1 m1;
        m2.m1s.add(m1);
        m3.m2s.add(m2);
        //^ end complex computation
        //output_m3=m3; (change allocator, how?  #3)
        //.... clean up oneFrameAllocator here ....
    }
}

enter image description here

Я не могу назначить output_m3=m3 напрямую, потому что output_m3 скопирует использование однокадрового распределителя из m3.

Мое плохое решение - создать output_m3 снизу вверх.
Приведенный ниже код работает, но очень утомительно.

M3 reconstructM3(M3& src,Allocator* allo){
    //very ugly here #1
    M3 m3New;
    m3New.m2s.setAllocator(allo);
    for(int n=0;n<src.m2s.size();n++){
        M2 m2New;
        m2New.m1s.setAllocator(allo);
        for(int k=0;k<src.m2s[n].m1s.size();k++){
            m2New.m1s.add(src.m2s[n].m1s[k]);
        }
        m3New.m2s.add(m2New);
    }
    return m3New;
}
output_m3=reconstructM3(m3,&heapAllocator);

Вопрос

Как элегантно переключить распределитель объекта (не распространяя все вручную)?

Описание награды

  1. Ответ не должен основываться ни на одном из моих фрагментов или каких-либо физических вещей.Мой код не подлежит восстановлению.
  2. ИМХО, передача типа распределителя в качестве параметра шаблона класса (например, MyArray<T,StackAllocator>) нежелательна.
  3. Я не против vtable-стоимости Allocator::allocate() и Allocator::deallocate().
  4. Я мечтаю о шаблоне / инструменте C ++, который может автоматически распространять распределитель среди членов класса.Возможно, это operator=(), как MSalters , но я не могу найти правильный способ достичь этого.

Ссылка : Получив ответ от JaMiT , я обнаружил, что этот вопрос похож на Использование пользовательского распределителядля данных-данных AllocatorAwareContainer класса .

Ответы [ 3 ]

8 голосов
/ 26 апреля 2019

Обоснование

По сути, этот вопрос задает вопрос о том, как использовать пользовательский распределитель с многоуровневым контейнером.Есть и другие условия, но подумав об этом, я решил проигнорировать некоторые из этих условий.Кажется, они мешают решениям без веской причины.Это оставляет открытой возможность ответа из стандартной библиотеки: std::scoped_allocator_adaptor и std::vector.

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

  • Для выделения одного кадра требуется, чтобы объект был уничтожен к концу цикла по timeStep.
  • Распределение кучи должно бытьиспользуется, когда не может быть выделено однокадровое распределение.

То есть, вы можете сказать, какую стратегию размещения использовать, посмотрев на область действия рассматриваемого объекта / переменной.(Это внутри или снаружи тела цикла?) Область видимости известна во время построения и не изменяется (если вы не злоупотребляете std::move).Таким образом, желаемый распределитель известен во время строительства и не изменяется.Однако текущие конструкторы не позволяют указывать распределитель.Это что-то изменить.К счастью, такое изменение является довольно естественным продолжением введения scoped_allocator_adaptor.

Другим большим изменением является отбрасывание класса MyArray.Существуют стандартные контейнеры для облегчения программирования.По сравнению с написанием собственной версии стандартные контейнеры быстрее внедряются (как уже было сделано) и менее подвержены ошибкам (стандарт стремится к более высокому качеству, чем «работает для меня в этот раз»).Итак, с шаблоном MyArray и с std::vector.

Как это сделать

Фрагменты кода в этом разделе можно объединить в один исходный файл, который компилируется,Просто пропустите мой комментарий между ними.(Вот почему только первый фрагмент содержит заголовки.)

Ваш текущий класс Allocator является разумной отправной точкой.Ему просто нужна пара методов, которые указывают, когда два экземпляра являются взаимозаменяемыми (т.е. когда оба способны освободить память, выделенную одним из них).Я также позволил себе сменить amountByte на тип без знака, поскольку выделение отрицательного объема памяти не имеет смысла.(Я оставил только тип align, поскольку нет никаких указаний на то, какие значения это будет принимать. Возможно, это должно быть unsigned или перечисление.)

#include <cstdlib>
#include <functional>
#include <scoped_allocator>
#include <vector>

class Allocator {
public:
    virtual void * allocate(std::size_t amountByte, int align)=0;
    virtual void deallocate(void * v)=0;
    //some complex field and algorithm

    // **** Addition ****
    // Two objects are considered equal when they are interchangeable at deallocation time.
    // There might be a more refined way to define this relation, but without the internals
    // of Allocator, I'll go with simply being the same object.
    bool operator== (const Allocator & other) const  { return this == &other; }
    bool operator!= (const Allocator & other) const  { return this != &other; }
};

Далее следуют дваспециализаций.Однако их детали выходят за рамки вопроса.Поэтому я просто макет что-то, что будет компилироваться (необходимо, так как нельзя напрямую создать экземпляр абстрактного базового класса).

// Mock-up to allow defining the two allocators.
class DerivedAllocator : public Allocator {
public:
    void * allocate(std::size_t amountByte, int)  override { return std::malloc(amountByte); }
    void   deallocate(void * v)                   override { std::free(v); }
};
DerivedAllocator oneFrameAllocator;
DerivedAllocator heapAllocator;

Теперь мы переходим к первому мясному блоку - адаптации Allocator к ожиданиям стандарта,Он состоит из шаблона оболочки, параметром которого является тип создаваемого объекта.Если вы можете проанализировать требования Allocator , этот шаг прост.Надо отметить, что анализ требований не прост, поскольку они предназначены для охвата «причудливых указателей».

// Standard interface for the allocator
template <class T>
struct AllocatorOf {

    // Some basic definitions:

    //Allocator & alloc; // A plain reference is an option if you don't support swapping.
    std::reference_wrapper<Allocator> alloc; // Or a pointer if you want to add null checks.
    AllocatorOf(Allocator & a) : alloc(a) {} // Note: Implicit conversion allowed

    // Maybe this value would come from a helper template? Tough to say, but as long as
    // the value depends solely on T, the value can be a static class constant.
    static constexpr int ALIGN = 0;

    // The things required by the Allocator requirements:

    using value_type = T;
    // Rebind from other types:
    template <class U>
    AllocatorOf(const AllocatorOf<U> & other) : alloc(other.alloc) {}
    // Pass through to Allocator:
    T *  allocate  (std::size_t n)        { return static_cast<T *>(alloc.get().allocate(n * sizeof(T), ALIGN)); }
    void deallocate(T * ptr, std::size_t) { alloc.get().deallocate(ptr); }
    // Support swapping (helps ease writing a constructor)
    using propagate_on_container_swap = std::true_type;
};
// Also need the interchangeability test at this level.
template<class T, class U>
bool operator== (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc == a_u.get().alloc; }
template<class T, class U>
bool operator!= (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc != a_u.get().alloc; }

Далее следуют классы многообразия.Самый низкий уровень (M1) не нуждается в каких-либо изменениях.

Средним уровням (M2) требуется два дополнения для получения желаемых результатов.

  1. Тип элемента allocator_type нуждается вбыть определенным.Его существование указывает на то, что класс распознает распределитель.
  2. Должен существовать конструктор, который принимает в качестве параметров объект для копирования и распределитель для использования.Это делает класс фактически осведомленным о распределителях. (Потенциально могут потребоваться другие конструкторы с параметром allocator, в зависимости от того, что вы на самом деле делаете с этими классами. scoped_allocator работает путем автоматического добавления распределителя к предоставленным параметрам конструкции. Так как пример кода делает копии внутри векторовтребуется конструктор «copy-plus-allocator».)

Кроме того, для общего использования средние уровни должны получить конструктор, у которого единственный параметр является распределителем.Для удобства чтения я также верну имя MyArray (но не шаблон).

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

class M1{};   //e.g. a single-point collision site

class M2{     //e.g. analysed many-point collision site
public:
    using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M1>>;
    using MyArray        = std::vector<M1, allocator_type>;

    // Default construction still uses oneFrameAllocator, but this can be overridden.
    explicit M2(const allocator_type & alloc = oneFrameAllocator) : m1s(alloc) {}
    // "Copy" constructor used via scoped_allocator_adaptor
    //M2(const M2 & other, const allocator_type & alloc) : m1s(other.m1s, alloc) {}
    // You may want to instead delegate to the true copy constructor. This means that
    // the m1s array will be copied twice (unless the compiler is able to optimize
    // away the first copy). So this would need to be performance tested.
    M2(const M2 & other, const allocator_type & alloc) : M2(other)
    {
        MyArray realloc{other.m1s, alloc};
        m1s.swap(realloc); // This is where we need swap support.
    }

    MyArray m1s;
};

class M3{     //e.g. analysed collision surface
public:
    using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M2>>;
    using MyArray        = std::vector<M2, allocator_type>;

    // Default construction still uses oneFrameAllocator, but this can be overridden.
    explicit M3(const allocator_type & alloc = oneFrameAllocator) : m2s(alloc) {}

    MyArray m2s;
};

Давайте посмотрим ... две строки, добавленные к Allocator (можно сократить доодин), четыре раза до M2, три до M3, исключите шаблон MyArray и добавьте шаблон AllocatorOf.Это не огромная разница.Ну, немного больше, чем рассчитывать, если вы хотите использовать автоматически сгенерированный конструктор копирования для M2 (но с преимуществом полной поддержки замены векторов).В целом, изменения не столь существенны.

Вот как будет использоваться код:

int main()
{
    M3 output_m3{heapAllocator};
    for ( int timeStep = 0; timeStep < 100; timeStep++ ) {
        //v start complex computation #2
        M3 m3;
        M2 m2;
        M1 m1;
        m2.m1s.push_back(m1);  // <-- vector uses push_back() instead of add()
        m3.m2s.push_back(m2);  // <-- vector uses push_back() instead of add()
        //^ end complex computation
        output_m3 = m3; // change to heap allocation
        //.... clean up oneFrameAllocator here ....
    }    
}

Присвоенное здесь присваивание сохраняет стратегию выделения output_m3, поскольку AllocatorOf нескажи делать иначе.Похоже, это должно быть желаемым поведением, а не старым способом копирования стратегии распределения.Обратите внимание, что если обе стороны назначения уже используют одну и ту же стратегию распределения, не имеет значения, будет ли стратегия сохранена или скопирована.Следовательно, существующее поведение должно быть сохранено без необходимости дальнейших изменений.

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

5 голосов
/ 22 апреля 2019

Поскольку вы стремитесь к производительности, я подразумеваю, что ваши классы не будут управлять временем жизни самого распределителя, а просто будут использовать его необработанный указатель. Кроме того, поскольку вы меняете хранилище, копирование неизбежно. В этом случае все, что вам нужно, это добавить «конструктор параметризованного копирования» к каждому классу, например ::10000

template <typename T> class MyArray {
    private:
        Allocator& _allocator;

    public:
        MyArray(Allocator& allocator) : _allocator(allocator) { }
        MyArray(MyArray& other, Allocator& allocator) : MyArray(allocator) {
            // copy items from "other", passing new allocator to their parametrized copy constructors
        }
};

class M1 {
    public:
        M1(Allocator& allocator) { }
        M1(const M1& other, Allocator& allocator) { }
};

class M2 {
    public:
        MyArray<M1> m1s;

    public:
        M2(Allocator& allocator) : m1s(allocator) { }
        M2(const M2& other, Allocator& allocator) : m1s(other.m1s, allocator) { }
};

Таким образом, вы можете просто сделать:

M3 stackM3(stackAllocator);
// do processing
M3 heapM3(stackM3, heapAllocator); // or return M3(stackM3, heapAllocator);

для создания копии на основе другого распределителя.

Кроме того, углубляясь в вашу фактическую структуру кода, вы можете добавить некоторые шаблоны магии для автоматизации вещей:

template <typename T> class MX {
    public:
        MyArray<T> ms;

    public:
        MX(Allocator& allocator) : ms(allocator) { }
        MX(const MX& other, Allocator& allocator) : ms(other.ms, allocator) { }
}

class M2 : public MX<M1> {
    public:
        using MX<M1>::MX; // inherit constructors
};

class M3 : public MX<M2> {
    public:
        using MX<M2>::MX; // inherit constructors
};
3 голосов
/ 26 апреля 2019

Я понимаю, что это не ответ на ваш вопрос - но если вам нужен только объект для следующего цикла (а не будущих циклов после него), можете ли вы просто оставить два однокадровых распределителя, уничтожающих их в альтернативных циклах?

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

Ваш код будет выглядеть примерно так:

int main(){
    M3 output_m3; 
    for(int timeStep=0;timeStep<100;timeStep++){
        oneFrameAllocator.set_to_even(timeStep % 2 == 0);
        //v start complex computation #2
        M3 m3;
        M2 m2;
        M1 m1;
        m2.m1s.add(m1);
        m3.m2s.add(m2);
        //^ end complex computation
        output_m3=m3; 
        oneFrameAllocator.cleanup(timestep % 2 == 1); //cleanup odd cycle
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...