Как предотвратить вызов деструкторов для объектов, управляемых boost::fast_pool_allocator?

Я хотел бы воспользоваться следующей объявленной функцией boost::fast_pool_allocator (см. документацию Boost для Буст-пул):

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

(См. здесь за эту цитату)

Ключевая фраза: предать их забвению. Я не хочу вызывать деструкторы этих объектов.

(Причина в том, что у меня есть миллионы крошечных объектов, которые образуют чрезвычайно сложную сеть собственности в куче, и моей программе требуется около 20 минут, чтобы вызвать все деструкторы, когда единственный родительский объект уходит из стека. Я не нужно вызывать эти деструкторы, потому что нет желаемых побочных эффектов, и вся память содержится в boost::pool.)

К сожалению, несмотря на обещание приведенной выше документации и обещание концепции boost::pool, я не могу найти способ предотвратить вызов деструкторов управляемых объектов.

Проблема легко изолируется в небольшой программе-примере:

class Obj
{
public:
    ~Obj()
    {
        // Placing a breakpoint here indicates that this is *always* reached
        // (except for the crash scenario discussed below)
        int m = 0;
    }
};

typedef std::map<int, Obj, std::less<int>,
                 boost::fast_pool_allocator<std::pair<int const, Obj>>>
        fast_int_to_int_map;

class Foo
{
public:
    ~Foo()
    {
        // When the following line is uncommented, the program CRASHES
        // when the destructor is exited - because the Obj destructors
        // are called on the invalid Obj ghost instances

        //boost::singleton_pool<boost::fast_pool_allocator_tag,
        //                  sizeof(std::pair<int const, Obj>)>::purge_memory();
    }

    fast_int_to_int_map mmap;
};

void mfoo()
{
    // When this function exits, the Foo instance goes off the stack
    // and its destructor is called, in turn calling the destructors
    // of the Obj instances - this is NOT desired!

    Foo foo;
    foo.mmap[0] = Obj();
    foo.mmap[1] = Obj();
}

int main()
{
    mfoo();

    // The following line deallocates the memory of the pool just fine -
    // but does nothing to prevent the destructors of the Obj instances
    // from being called

    boost::singleton_pool<boost::fast_pool_allocator_tag,
                          sizeof(std::pair<int const, Obj>)>::purge_memory();
}

Как отмечено в комментариях к коду, всегда вызываются деструкторы экземпляров Obj, которыми управляет boost::pool.

Что я могу сделать, чтобы многообещающая цитата из документа Boost Pool, drop them off into oblivion, стала реальностью?


person Dan Nissenbaum    schedule 08.04.2014    source источник


Ответы (3)


Вы передаете пользовательский распределитель в свой класс std::map. Итак, этот аллокатор будет использоваться для всего внутри std::map: для всех данных Obj, а также для узлов бинарного дерева std::map. В результате, если вы вызываете purge_memory() в деструкторе Foo, вся эта память становится недействительной, и происходит сбой в деструкторе std::map.

Ваше предположение о том, что за освобождение объектов отвечает пользовательский аллокатор, неверно: задача std::map состоит в том, чтобы освободить все объекты. Таким образом, ~Obj() будет вызываться независимо от того, передадите ли вы пользовательский распределитель или используете его по умолчанию.

Я не вижу никакого элегантного решения для вашего вопроса. Но этот подход должен работать:

  • используйте placement new для создания объекта Foo из памяти пула,
  • используйте этот объект Foo как обычно,
  • вызовите purge_memory(), чтобы освободить всю память из пула. Никакие деструкторы ~Obj или ~std::map вызываться не будут.
person qehgt    schedule 08.04.2014
comment
Спасибо! Я пришел к такому же выводу - в отношении того, что пользовательский аллокатор отвечает за позиционирование памяти, а не за вызов конструктора или деструктора. Теперь мне ясно, что это два СОВЕРШЕННО отдельных процесса. То есть (1) само выделение/освобождение памяти с использованием системных вызовов памяти SDK, new/delete или malloc/free; и (2) вызов ctor's и dtor's. Пользовательские распределители отвечают только за первое (№1), а не за второе. - person Dan Nissenbaum; 09.04.2014
comment
Однако большая часть моего замешательства возникла из-за того, что в приведенном выше коде функция purge_memory() ничего не делает. Если бы это был set, а не map, purge_memory освободил бы память. Однако размер внутренних объектов, созданных map, когда map (внутренне) вызывает new в пользовательском распределителе, не размер пары. Вот почему purge_memory() ничего не делает выше. (В случае set размер внутреннего запроса new равен размеру содержащихся объектов, поэтому в этом случае это работает.) Сейчас я работаю над последней проблемой. - person Dan Nissenbaum; 09.04.2014

По умолчанию пул использует распределитель default_user_allocator_new_delete. Это уничтожит базовые объекты, сначала вызвав деструктор, а затем освободив базовую память. default_user_allocator_malloc_free приведет к освобождению памяти, занятой malloc, без запуска деструктора - следовательно, drop[ing] them off into oblivion.

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

person Andy    schedule 08.04.2014
comment
Существует один объект верхнего уровня, и каждый элемент данных, который является контейнером, использует настраиваемый распределитель и т. д. для всех вложенных объектов, так что не существует ни одного пропущенного пути в иерархии дерева. Затем я просто позволяю одному объекту верхнего уровня удалиться из стека, и его деструктор вызывает purge для всех пулов памяти. Большое спасибо за этот ответ - это очевидно в ретроспективе! Конечно, в этом большая разница между malloc/free и new/delete — первый не вызывает конструкторы и деструкторы. Я никогда не использовал malloc/free, так что не подумал об этом. - person Dan Nissenbaum; 08.04.2014
comment
Я пробую это сейчас - знаете ли вы, что boost::default_user_allocator_malloc_free распределитель будет обеспечивать вызов конструкторов? - person Dan Nissenbaum; 08.04.2014
comment
Этого я не знаю - в вашем примере вы явно вызывали конструкторы. Я предполагаю, что вы сделали бы что-то подобное в своей реализации. - person Andy; 08.04.2014
comment
Пример был бы полезен - кажется, мои деструкторы все еще всегда вызываются, даже с использованием boost::default_user_allocator_malloc_free, к сожалению. - person Dan Nissenbaum; 08.04.2014

Ответ на этот вопрос содержится в комментариях под ответом @qehgt и подробно в этот новый вопрос.

А именно:

Существует четкое и формальное различие между двумя связанными аспектами создания и удаления объекта:

  • (1) Выделение и освобождение памяти

  • (2) Вызов конструкторов и деструкторов

Назначение пользовательского распределителя только (1).

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

Следовательно, способ достижения цели этого вопроса следующий: объявить объект-контейнер через new и НИКОГДА НЕ ВЫЗЫВАТЬ на него delete (но используйте настраиваемый распределитель, чтобы гарантировать, что объекты, которые вы позже создадите в контейнере, будут сохранены в пуле памяти , а затем вручную освободить пул памяти, вызвав purge_memory()):

class Obj { // has operator<() for use with std::set };

typedef std::set<Obj, std::less<Obj>, boost::fast_pool_allocator<Obj>> fast_set_obj;

// Deliberately do not use a managed pointer -
// I will *NOT* delete this object, but instead
// I will manage the memory using the memory pool!!!
fast_set_obj * mset = new fast_set_obj;

// ... add some Obj's to 'mset'
mset->insert(Obj());
mset->insert(Obj());

// Do something desireable with the set ...
...

// All done.
// It's time to release the memory, but do NOT call any Obj destructors.
// The following line of code works exactly as intended.

boost::singleton_pool<boost::fast_pool_allocator_tag, sizeof(Obj const)>::purge_memory();

(Приведенный выше код взят из связанного вопроса выше.)

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

person Dan Nissenbaum    schedule 09.04.2014