Как использовать boost::object_pool в качестве распределителя boost::multi_index?

Я пытаюсь реализовать приложение boost::multi_index, и производительность очень плохая: вставка 10 000 объектов занимает почти 0,1 секунды, и это неприемлемо. Поэтому, когда я просмотрел документацию и обнаружил, что boost::multi_index может принимать распределитель памяти в качестве последнего параметра, но я получил много ошибок компиляции, когда пытался реализовать себя. Пожалуйста, помогите мне исправить. Спасибо.

struct order
{
    unsigned int    id;
    unsigned int    quantity;
    double          price;
};

struct id{};
struct price{};

typedef multi_index_container<
  order,
  indexed_by<
    hashed_unique<
      tag<id>,  BOOST_MULTI_INDEX_MEMBER(order, unsigned int, id)>,
    ordered_non_unique<
      tag<price>,BOOST_MULTI_INDEX_MEMBER(order ,double, price),
        std::less<double> >
  >,
  boost::object_pool<order>
> order_sell; 

В общем, компилятору не нравится выражение boost::object_pool в качестве распределителя в объявлении order_sell.


person loudking    schedule 08.05.2016    source источник


Ответы (3)


Позвольте мне повторить предложение Александра о том, чтобы вы профилировали свою программу, чтобы понять, в чем на самом деле заключается проблема. Я сильно сомневаюсь, что Boost.MultiIndex как таковой может быть таким медленным, как вы говорите. Следующая программа измеряет время, необходимое для создания контейнера order_sell (без Boost.Pool), заполнения его 10 000 случайных заказов и его уничтожения:

Текущая демонстрация Coliru

#include <algorithm>
#include <array>
#include <chrono>
#include <numeric> 

std::chrono::high_resolution_clock::time_point measure_start,measure_pause;

template<typename F>
double measure(F f)
{
  using namespace std::chrono;

  static const int              num_trials=10;
  static const milliseconds     min_time_per_trial(200);
  std::array<double,num_trials> trials;
  volatile decltype(f())        res; /* to avoid optimizing f() away */

  for(int i=0;i<num_trials;++i){
    int                               runs=0;
    high_resolution_clock::time_point t2;

    measure_start=high_resolution_clock::now();
    do{
      res=f();
      ++runs;
      t2=high_resolution_clock::now();
    }while(t2-measure_start<min_time_per_trial);
    trials[i]=duration_cast<duration<double>>(t2-measure_start).count()/runs;
  }
  (void)res; /* var not used warn */

  std::sort(trials.begin(),trials.end());
  return std::accumulate(
    trials.begin()+2,trials.end()-2,0.0)/(trials.size()-4);
}

void pause_timing()
{
  measure_pause=std::chrono::high_resolution_clock::now();
}

void resume_timing()
{
  measure_start+=std::chrono::high_resolution_clock::now()-measure_pause;
}

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/member.hpp>

using namespace boost::multi_index;

struct order
{
    unsigned int    id;
    unsigned int    quantity;
    double          price;
};

struct id{};
struct price{};

typedef multi_index_container<
  order,
  indexed_by<
    hashed_unique<
      tag<id>,BOOST_MULTI_INDEX_MEMBER(order, unsigned int, id)>,
    ordered_non_unique<
      tag<price>,BOOST_MULTI_INDEX_MEMBER(order ,double, price),
        std::less<double> >
  >
> order_sell; 

#include <iostream>
#include <random>

int main()
{
  std::cout<<"Insertion of 10,000 random orders plus container cleanup\n";
  std::cout<<measure([](){
    order_sell os;
    std::mt19937                                gen{34862};
    std::uniform_int_distribution<unsigned int> uidist;
    std::uniform_real_distribution<double>      dbdist;

    for(unsigned int n=0;n<10000;++n){
      os.insert(order{uidist(gen),0,dbdist(gen)});
    }
    return os.size();
  })<<" seg.\n";
}

При запуске в режиме -O3 с любым бэкэндом, который использует Coliru, мы получаем:

Insertion of 10,000 random orders plus container cleanup
0.00494657 seg.

Режим выпуска VS 2015 на моей машине (Intel Core i5-2520M @ 2,50 ГГц) дает:

Insertion of 10,000 random orders plus container cleanup
0.00492825 seg.

Итак, это примерно в 20 раз быстрее, чем вы сообщаете, и я включаю в свои измерения разрушение контейнера и генерацию случайных чисел.

Пара дополнительных наблюдений:

  • boost::object_pool не предоставляет интерфейс распределителя, указанный стандартной библиотекой для взаимодействия с контейнерами. Вы можете использовать boost::pool_allocator вместо этого (я немного поиграл с ним и, похоже, скорость не улучшилась, но ваш пробег может отличаться).
  • Ответ Джона, кажется, подразумевает, что Boost.MultiIndex неоптимален в том смысле, что он выделяет узлы отдельно от значений или что-то в этом роде. На самом деле, библиотека настолько эффективна, насколько это возможно с точки зрения распределения памяти, и вы не можете добиться большего успеха с Boost.Intrusive (на самом деле вы можете получить то же самое). Взгляните на этот мой ответ, если вам интересно, как выглядят внутренние структуры данных Boost.MultiIndex. В частности, для вашего контейнера order_sell с хешированным индексом и упорядоченным индексом каждое значение помещается в один собственный узел, плюс у вас есть отдельный так называемый массив ведра (массив указателей) с длина примерно равна количеству элементов. Вы не можете улучшить структуру данных на основе узлов (если вы хотите обойтись без стабильности итератора, вы можете разработать схемы, более эффективные с точки зрения памяти).
person Joaquín M López Muñoz    schedule 08.05.2016

Вы не можете или не должны этого делать по нескольким причинам.

Во-первых, у boost::object_pool есть проблема с производительностью: освобождение объектов от него — это O(N). Если вы хотите сделать это эффективно, вам нужно реализовать свой собственный распределитель непосредственно поверх boost::pool. Причина в том, что object_pool использует семантику "заказано бесплатно", что вам не нужно для вашего варианта использования. Дополнительные сведения об этой ошибке производительности см. здесь: https://svn.boost.org/trac/boost/ticket/3789

Во-вторых, multi_index_container на самом деле нужно выделить несколько разных вещей, в зависимости от выбранных вами индексов. Недостаточно иметь возможность выделить value_type, необходимо выделить узлы дерева и т. д. Это делает его совершенно непригодным для использования с распределителем пула, поскольку распределитель пула обычно предполагает наличие множества экземпляров одного типа (или, по крайней мере, одного размера) .

Если вам нужна наилучшая производительность, вам, возможно, придется «свернуть свою собственную». Boost MIC и Boost Pool определенно не будут хорошо работать вместе. Но другая идея заключается в использовании более производительного распределителя общего назначения, такого как tcmalloc: http://goog-perftools.sourceforge.net/doc/tcmalloc.html

Вы можете рассмотреть Boost Intrusive, который контейнеры, которые хорошо подходят для распределения в пуле. Вы можете добавить хуки к вашему типу order, чтобы позволить хранить их в упорядоченных и неупорядоченных картах, а затем вы можете разместить заказы в boost::pool.

Наконец, поскольку кажется, что вы храните финансовые данные, вы должны знать, что использование double для хранения цен опасно. Подробнее об этом см. здесь: Почему бы не использовать Double или Float представлять валюту?

person John Zwinck    schedule 08.05.2016
comment
Я думаю, вы делаете некоторые неточные предположения о внутренней структуре данных Boost.MultIndex. Подробнее см. мой ответ. - person Joaquín M López Muñoz; 08.05.2016
comment
@JoaquínMLópezMuñoz: Согласны ли вы, если я сформулирую это таким образом? Boost MIC выделяет объекты, тип которых вам неизвестен и размер которых не совпадает с value_type. - person John Zwinck; 08.05.2016
comment
Я согласен с этим последним утверждением, но я не понимаю, как это означает, что библиотека совершенно не подходит для использования с распределителем пула или что Boost MIC и Boost Pool определенно не будут хорошо работать вместе: просто вставьте boost::pool_allocator в определение из order_sell и проверить, что он работает (я сделал). Вы также говорите, что Boost.Intrusive, по сравнению, хорошо подходит для распределения пула: ну, размещение контейнера на основе Boost.Intrusive с хуком unordered_set и хуком multiset даст вам тот же макет, что и Boost.MultiIndex- на базе order_sell с boost::pool_allocator. - person Joaquín M López Muñoz; 08.05.2016
comment
Позвольте мне предположить, что вы думаете, что любой ненавязчивый контейнер (например, Boost-MultiIndex, а также все контейнеры стандартной библиотеки на основе узлов, такие как std::set и std::unordered_set) не поддается выделению пула, потому что тип и размер внутренних узлов не совпадает с value_type. Это, безусловно, так, но на самом деле это не проблема, поскольку контейнер внутри получает правильный экземпляр распределителя через allocator::rebind. Я понимаю, что это может создать некоторые проблемы, поскольку вы не можете объявить пул или создать его явно в своем коде, верно? Это то, что вы имеете в виду? - person Joaquín M López Muñoz; 08.05.2016
comment
Правильно: например, я создаю object_pool<order>, но MIC хочет выделить sizeof(order) + 32, и мой пул может не поддерживать это. - person John Zwinck; 08.05.2016
comment
Да, невозможно создать ненавязчивый контейнер на основе узла для использования вашего object_pool<order>, но через pool_allocator::rebind он в конечном итоге создаст экземпляр object_pool<node_type> (видимого только для контейнера). - person Joaquín M López Muñoz; 08.05.2016
comment
Имейте в виду, я понимаю, что интрузивные контейнеры гораздо более гибкие, чем неинтрузивные, с точки зрения выделения памяти, отслеживания времени жизни объекта и т. д. (вот почему Boost.Intrusive для начала), я только говорю, что а) не -Intrusive контейнеры можно настроить для работы с выделенным пулом и б) multi_index_container имеет ту же структуру узлов, что и эквивалентный контейнер на основе Boost.Intrusive с несколькими хуками. - person Joaquín M López Muñoz; 08.05.2016

Первое, что нужно сделать (как всегда при узких местах в производительности) — это профилировать!

Может оказаться (и, вероятно, так и будет), что распределение — не самое худшее для вас.

person Alexander Bessonov    schedule 08.05.2016