Как сохранить и передать состояние симуляции, минимально влияя на обновления в секунду? - PullRequest
3 голосов
/ 15 февраля 2011

Мое приложение состоит из двух потоков:

  1. Тема графического интерфейса (с использованием Qt)
  2. Тема моделирования

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

В моем потоке графического интерфейса я рендерирую сущности в симе с частотой кадров 30-60 кадров в секунду;тем не менее, я хочу, чтобы мой сим «хрустнул вперед» - так сказать, - и в конечном итоге выстроил в очередь состояние игры (подумайте, потоковое видео, у вас есть буфер).

Теперь для каждого кадрасим, который я рендеринг, мне нужно соответствующее моделирование "состояние".Так что мой поток симов выглядит примерно так:

while(1) {
    simulation.update();
    SimState* s = new SimState;
    simulation.getAgents( s->agents ); // store agents
    // store other things to SimState here..
    stateStore.enqueue(s); // stateStore is a QQueue<SimState*>
    if( /* some threshold reached */ )
        // push stateStore
}

SimState выглядит так:

struct SimState {
    std::vector<Agent> agents;
    //other stuff here
};

И Simulation :: getAgents выглядит так:

void Simulation::getAgents(std::vector<Agent> &a) const
{
    // mAgents is a std::vector<Agent>
    std::vector<Agent> a_tmp(mAgents);
    a.swap(a_tmp);
}

Agent Сами являются несколько сложными классами.Члены - это группа int s и float s и два std::vector<float> s.

С этой текущей настройкой сим не может хрустать должен быстрее, чем рисует поток GUI.Я проверил, что текущим узким местом является simulation.getAgents( s->agents ), потому что даже если я пропущу нажатие, обновления в секунду будут медленными.Если я закомментирую эту строку, я вижу улучшение на несколько порядков в секунду.

Итак, какие типы контейнеров я должен использовать для хранения состояния симуляции?Я знаю, что копирование происходит в банкомате, но некоторые из них неизбежны.Должен ли я хранить Agent* в векторе вместо Agent?

Примечание: На самом деле симуляция не в цикле, а использует Qt QMetaObject::invokeMethod(this, "doSimUpdate", Qt::QueuedConnection);, поэтому я могу использоватьсигналы / слоты для связи между потоками;однако я проверил более простую версию, используя while(1){}, и проблема не устранена.

1 Ответ

5 голосов
/ 15 февраля 2011

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

Простой способ реализовать пул состоит в том, чтобы сначала выдвинуть кучупредварительно выделенные объекты SimState на std::stack<SimState*>.Обратите внимание, что стек предпочтительнее очереди, потому что вы хотите взять объект SimState, который с большей вероятностью будет «горячим» в кэш-памяти (последний использованный объект SimState будет наверху стека).Ваша очередь симуляции выталкивает объекты SimState из стека и заполняет их вычисленным SimState.Эти вычисленные объекты SimState затем помещаются в очередь производителя / потребителя для подачи потока GUI.После рендеринга потоком GUI они возвращаются в стек SimState (т. Е. В «пул»).Старайтесь избегать ненужного копирования объектов SimState при этом.Работайте непосредственно с объектом SimState на каждом этапе вашего «конвейера».

Конечно, вам придется использовать надлежащие механизмы синхронизации в вашем стеке SimState и очереди, чтобы избежать условий гонки.Qt может уже иметь поточно-ориентированные стеки / очереди.Стек / очередь без блокировок может ускорить процесс при наличии большого количества конфликтов (блоки потоков Intel Thread Building Blocks предоставляют такие очереди без блокировок).Видя, что для вычисления SimState требуется порядка 1/50 секунды, я сомневаюсь, что конфликт будет проблемой.

Если ваш пул SimState истощается, то это означает, что ваш поток моделирования слишком "далеко"вперед »и может позволить себе ждать возвращения некоторых объектов SimState в пул.Поток моделирования должен блокироваться (используя переменную условия), пока объект SimState снова не станет доступным в пуле.Размер вашего пула SimState соответствует тому, сколько SimState может быть буферизовано (например, пул из ~ 50 объектов дает вам время ожидания до ~ 1 секунды).

Вы также можете попробовать запустить параллельное моделированиепотоки, чтобы воспользоваться преимуществами многоядерных процессоров.Шаблон Thread Pool может быть полезен здесь.Однако следует позаботиться о том, чтобы вычисленные SimStates ставились в очередь в правильном порядке.Здесь может работать поточная приоритетная очередь, упорядоченная по метке времени.

Вот простая диаграмма конвейерной архитектуры, которую я предлагаю:

pipeline architecture

(Щелкните правой кнопкой мыши и выберите изображение для более четкого просмотра.)

(ПРИМЕЧАНИЕ. Пул и очередь удерживают SimState указателем , а не значением!)

Надеюсь, что этопомогает.


Если вы планируете повторно использовать объекты SimState, то ваш метод Simulation::getAgents будет неэффективным.Это связано с тем, что параметр vector<Agent>& a, вероятно, уже обладает достаточной емкостью для хранения списка агентов.

То, как вы делаете это сейчас, отбросит этот уже выделенный вектор и создаст новый с нуля.

IMO, ваш getAgents должен быть:

void Simulation::getAgents(std::vector<Agent> &a) const
{
    a = mAgents;
}

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


Еще одна идея: вы можете попробовать сделать объекты Agent фиксированного размера, используя вместо этого массив c-style (или boost::array) и переменную "count" std::vector для членов плавающего списка агента.Просто сделайте массив фиксированного размера достаточно большим для любой ситуации в вашей симуляции.Да, вы будете тратить пространство впустую, но вы можете получить большую скорость.

Затем вы можете объединить агентов, используя фиксатор размера объекта (например, * 1048).*) и передать их по указателю (или shared_ptr).Это избавит вас от большого количества выделения и копирования кучи.

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


Еще одна идея: вместо пула потоков для запуска циклов моделирования вы можете разбить моделирование на несколько этапов и выполнить каждый этап в своем собственном потоке. Очереди производителя / потребителя используются для обмена объектами SimState между этапами. Для того чтобы это было эффективным, на разных этапах должны быть примерно одинаковые рабочие нагрузки ЦП (иначе один этап станет узким местом). Это другой способ использовать параллелизм.

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