如何存储和推送模拟状态,同时最大限度地减少每秒更新的影响?

2024-01-05

我的应用程序由两个线程组成:

  1. GUI线程(使用Qt)
  2. 模拟线程

我使用两个线程的原因是保持 GUI 的响应能力,同时让 Sim 线程尽可能快地旋转。

在我的 GUI 线程中,我以 30-60 的 FPS 渲染 sim 中的实体;然而,我希望我的模拟卡能够“向前推进”——可以这么说——并排队最终绘制的游戏状态(想想流视频,你有一个缓冲区)。

现在,对于我渲染的模拟的每一帧,我需要相应的模拟“状态”。所以我的 sim 线程看起来像这样:

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);
}

The Agents 本身是有些复杂的类。成员是一群ints and float和两个std::vector<float>s.

在当前的设置下,SIM 的处理速度必须比 GUI 线程的绘制速度快。我已经验证当前的瓶颈是simulation.getAgents( s->agents ),因为即使我忽略推送,每秒更新速度也很慢。如果我注释掉该行,我会发现每秒更新数有几个数量级的提高。

那么,我应该使用什么类型的容器来存储模拟的状态?我知道自动取款机上有大量的复制行为,但其中一些是不可避免的。我应该存储吗Agent*在向量中而不是Agent ?

Note:实际上,模拟不是循环的,而是使用 Qt 的QMetaObject::invokeMethod(this, "doSimUpdate", Qt::QueuedConnection);所以我可以使用信号/槽在线程之间进行通信;但是,我已经使用验证了一个更简单的版本while(1){}并且问题仍然存在。


尝试重新使用您的 SimState 对象(使用某种池机制),而不是每次都分配它们。经过几次模拟循环后,重新使用的 SimState 对象的向量将增长到所需的大小,从而避免重新分配并节省时间。

实现池的一种简单方法是首先将一堆预先分配的 SimState 对象推送到std::stack<SimState*>。请注意,堆栈比队列更可取,因为您想要获取缓存中更有可能“热”的 SimState 对象(最近使用的 SimState 对象将位于堆栈的顶部)。您的模拟队列将 SimState 对象从堆栈中弹出,并用计算出的 SimState 填充它们。然后,这些计算出的 SimState 对象被推送到生产者/消费者队列中以提供给 GUI 线程。由 GUI 线程渲染后,它们被推回 SimState 堆栈(即“池”)。在执行所有这些操作时,尽量避免不必要地复制 SimState 对象。在“管道”的每个阶段直接使用 SimState 对象。

当然,您必须在 SimState 堆栈和队列中使用正确的同步机制以避免竞争条件。 Qt 可能已经有线程安全的堆栈/队列。如果存在大量争用,无锁堆栈/队列可能会加快速度(英特尔线程构建模块提供了此类无锁队列)。考虑到计算 SimState 大约需要 1/50 秒,我怀疑争用会成为问题。

如果您的 SimState 池耗尽,则意味着您的模拟线程太“超前”并且可以等待一些 SimState 对象返回到池中。模拟线程应该阻塞(使用条件变量),直到 SimState 对象在池中再次可用。 SimState 池的大小对应于可以缓冲的 SimState 数量(例如,约 50 个对象的池可为您提供长达约 1 秒的紧急处理时间)。

您还可以尝试运行并行模拟线程以利用多核处理器。这线程池 http://en.wikipedia.org/wiki/Thread_pool_pattern模式在这里很有用。但是,必须注意计算出的 SimState 必须按正确的顺序排队。按时间戳排序的线程安全优先级队列可能在这里起作用。

这是我建议的管道架构的简单图:

(右键单击并选择查看图像以获得更清晰的视图。)

(注意:池和队列通过以下方式保存 SimStatepointer,不是按值!)

希望这可以帮助。


如果您打算重复使用您的 SimState 对象,那么您的Simulation::getAgents方法将是低效的。这是因为vector<Agent>& a参数可能已经有足够的容量来容纳代理列表。

您现在这样做的方式会丢弃这个已经分配的向量并从头开始创建一个新的向量。

国际海事组织,你的getAgents应该:

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

是的,您会失去异常安全性,但您可能会获得性能(特别是使用可重用的 SimState 方法)。


另一个想法:您可以尝试使用 c 样式数组(或boost::array) 和“count”变量代替std::vector对于代理的浮动列表成员。只需使固定大小的数组足够大以适应模拟中的任何情况即可。是的,你会浪费空间,但你可能会获得很多速度。

然后,您可以使用以下方式汇集您的代理:固定大小的对象分配器(例如boost::pool http://www.boost.org/doc/libs/release/libs/pool/doc/index.html)并通过指针传递它们(或shared_ptr)。这将消除大量的堆分配和复制。

您可以单独使用这个想法,也可以与上述想法结合使用。这个想法似乎比上面的管道更容易实现,所以你可能想先尝试一下。


还有另一个想法:您可以将模拟分解为多个阶段,并在其自己的线程中执行每个阶段,而不是使用线程池来运行模拟循环。生产者/消费者队列用于在阶段之间交换 SimState 对象。为了使其有效,不同阶段需要具有大致相似的 CPU 工作负载(否则,一个阶段将成为瓶颈)。这是利用并行性的不同方式。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何存储和推送模拟状态,同时最大限度地减少每秒更新的影响? 的相关文章

随机推荐