Должен ли я передавать распределитель в качестве параметра функции? (мое недоразумение о распределителе)

После того, как я несколько дней буду изучать allocator, читая несколько статей
(cppreference и у нас не хватает памяти),
Я запутался в том, как управлять структурой данных, чтобы распределять память определенным образом.

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

Вот что я (неправильно) понимаю:

отрывок

Предположим, что B::generateCs() является функцией, которая генерирует список C из списка CPrototype.
B::generateCs() используется в конструкторе B(): -

class C          {/*some trivial code*/};
class CPrototype {/*some trivial code*/};
class B {
    public: 
    std::vector<C> generateCs() {  
        std::vector<CPrototype> prototypes = getPrototypes();
        std::vector<C> result;                     //#X
        for(std::size_t n=0; n < prototypes.size(); n++) {
            //construct real object  (CPrototype->C)
            result.push_back( makeItBorn(prototypes[n]) ); 
        }
        return result;
    }
    std::vector<C> bField;    //#Y
    B() {
        this->bField = generateCs();    //#Y  ; "generateCs()" is called only here
    }
    //.... other function, e.g. "makeItBorn()" and "getPrototypes()"
};

Из приведенного выше кода std::vector<C> настоящее время использует стандартный std::allocator умолчанию.

Для простоты, скажем, теперь есть только 2 распределителя (кроме std::allocator),
который я могу кодировать сам или изменить откуда- то:

  • HeapAllocator
  • StackAllocator

Часть 1 (#X)

Этот фрагмент может быть улучшен с помощью специального распределителя типов.
Это может быть улучшено в 2 местах. (#X и #Y)

std::vector<C> в строке #X представляется переменной стека,
поэтому я должен использовать stack allocator: -

std::vector<C,StackAllocator> result;   //#X

Это приводит к увеличению производительности. (#X закончен.)

Часть 2 (#Y)

Далее, более сложная часть находится в конструкторе B(). (#Y)
Было бы хорошо, если бы у переменной bField был соответствующий протокол распределения.

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

std::allocator<B> bAllo;   
B* b = bAllo.allocate(1);   

который не имеет никакого влияния на протокол распределения bField.

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

Часть 3

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

Если я знаю, какой это (куча или стек), я могу изменить объявление bField:

std::vector<C,StackAllocator> bField;     //.... or ....
std::vector<C,HeapAllocator> bField;     

К сожалению, с ограниченной информацией (я не знаю, какая это будет куча/стек, это может быть и то и другое),
этот путь (используя std::vector) ведет в тупик.

Часть 4

Следовательно, лучше всего передать в конструктор распределитель:

MyVector<C> bField; //create my own "MyVector" that act almost like "std::vector"
B(Allocator* allo) {
    this->bField.setAllocationProtocol(allo);  //<-- run-time flexibility 
    this->bField = generateCs();   
}

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

Более того, это единственный практический способ получить указанное ниже преимущество согласованности данных, когда есть много вызывающих абонентов, каждый из которых использует свой собственный блок памяти: -

class System1 {
    Allocator* heapForSystem1;
    void test(){
        B b=B(heapForSystem1);
    }
};
class System2 {
    Allocator* heapForSystem2;
    void test(){
        B b=B(heapForSystem2);
    }
};

Вопрос

  • Где я начал идти не так, как?
  • Как я могу улучшить фрагмент, чтобы использовать соответствующий распределитель (#X и #Y)?
  • Когда я должен передать allocator в качестве параметра?

Трудно найти практический пример использования распределителя.

Редактировать (ответить Уолтер)

... использование другого, чем std: allocator <>, редко рекомендуется.

Для меня это ядро ответа Уолтера.
Это было бы ценным знанием, если оно надежно.

1. Существуют ли какие-либо книги/ссылки/ссылки/доказательства, которые поддерживают это?
Список не поддерживает претензию. (Это на самом деле немного поддерживает противоположность.)
Это из личного опыта?

2. Ответ как-то противоречит многим источникам. Пожалуйста, защита.
Есть много источников, которые рекомендуют не использовать std:allocator<>.

Точнее говоря, это просто "ажиотаж", который редко стоит использовать в реальном мире?

Еще один маленький вопрос: -
Может ли утверждение быть расширено до "В большинстве качественных игр редко используется собственный распределитель"?

3. Если я нахожусь в такой редкой ситуации, я должен оплатить стоимость, верно?

Есть только 2 хороших способа:

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

Это правильно?

Ответ 1

В C++ распределитель, используемый для стандартных контейнеров, привязан к типу контейнера (но см. Ниже). Таким образом, если вы хотите контролировать поведение выделения вашего класса (включая его элементы-контейнеры), распределитель должен быть частью типа, то есть вы должны передать его как параметр template:

template<template <typename T> Allocator>
class B
{
public:
  using allocator = Allocator<C>
  using fieldcontainer = std::vector<C,allocator>;
  B(allocator alloc=allocator{})
  : bFields(create_fields(alloc)) {}
private:
  const fieldcontainer bFields;
  static fieldcontainer create_fields(allocator);
};

Однако обратите внимание, что существует экспериментальная поддержка полиморфного распределителя, которая позволяет изменять поведение распределителя независимо от типа. Это, безусловно, предпочтительнее, чем создание собственного MyVector<>.

Обратите внимание, что использование отличного от std::allocator<> рекомендуется только при наличии веской причины. Возможные случаи следующие.

  1. Распределитель стека может быть предпочтительным для небольших объектов, которые часто выделяются и выделяются, но даже распределитель кучи может быть не менее эффективным.

  2. Распределитель, который обеспечивает память, выровненную, скажем, до 64 байтов (подходит для выровненной загрузки в регистры AVX).

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

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


примечание добавлено в ответ на дополнительные вопросы.

Статья " Нам не хватает памяти" датируется 2008 годом и не применяется к современной практике C++ (с использованием стандарта C++ 11 или более поздней версии), когда управление памятью осуществляется с использованием контейнеров std и интеллектуальных указателей ( std::unique_ptr и std::shared_ptr) избегает утечек памяти, которые являются основным источником увеличения спроса на память в плохо написанном коде.

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

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

Ответ 2

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

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

template<typename C, typename Allocator = std::allocator<C> >
class B {

   vector<C, Allocator> bField;

   void generateCs() {  
     std::vector<CPrototype> prototypes = getPrototypes();
     for(std::size_t n=0; n < prototypes.size(); n++) {
         //construct real object  (CPrototype->C)
         bField.push_back( makeItBorn(prototypes[n]) ); 
     }
   }

   B(const Allocator& allo = Allocator()) : bField(allo) {
       generateCs();
   }
}

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