我应该为渲染和逻辑设置单独的线程,还是更多?
我知道由于数据同步(更不用说任何互斥锁)导致的巨大性能下降。
我一直在考虑将其发挥到极致,并为每个可以想到的子系统做线程。但是我担心这也会减慢速度。(例如,将输入线程与渲染或游戏逻辑线程分开是明智的做法吗?)所需的数据同步是否会使它毫无意义甚至变得更慢?
我应该为渲染和逻辑设置单独的线程,还是更多?
我知道由于数据同步(更不用说任何互斥锁)导致的巨大性能下降。
我一直在考虑将其发挥到极致,并为每个可以想到的子系统做线程。但是我担心这也会减慢速度。(例如,将输入线程与渲染或游戏逻辑线程分开是明智的做法吗?)所需的数据同步是否会使它毫无意义甚至变得更慢?
Answers:
坦率地说,利用多核的通用方法只是被误导了。将子系统分成不同的线程确实可以将一些工作分散到多个内核中,但这存在一些主要问题。首先,很难合作。当他们只是直接编写渲染代码或物理代码时,谁愿意在锁,同步,通信和其他东西上乱搞呢?其次,这种方法实际上并没有扩大规模。充其量,这将使您可以利用三个或四个核心,这就是您真的知道自己在做什么。一个游戏中只有太多子系统,而其中只有更少的子系统会占用大量CPU时间。我知道有几种不错的选择。
一种是为每个附加CPU配备一个主线程和一个工作线程。不管子系统是什么,主线程都会通过某种队列将隔离的任务委托给工作线程。这些任务本身也可以创建其他任务。辅助线程的唯一目的是每次从队列中抓取一个任务并执行它们。但是,最重要的是,只要线程需要任务的结果,任务完成后就可以得到结果;如果线程不执行任务,则可以安全地从队列中删除任务并继续执行任务任务本身。也就是说,并非所有任务最终都将彼此并行调度。有比并行执行更多的任务是一个很好的选择在这种情况下 这意味着随着您添加更多核心,它可能会扩展。这样做的一个缺点是,除非您可以访问已经为您提供了此功能的库或语言运行时,否则需要进行大量工作才能设计出不错的队列和工作循环。最难的部分是确保您的任务是真正隔离的和线程安全的,并确保您的任务处于粗粒度和细粒度之间的令人满意的中间位置。
子系统线程的另一种替代方法是隔离每个子系统。也就是说,与其在各自的线程中运行渲染和物理,不如编写物理子系统以一次使用所有内核,编写物理子系统以一次使用所有内核,然后让两个系统简单地按顺序运行(或交错运行,取决于您游戏架构的其他方面)。例如,在物理子系统中,您可以获取游戏中的所有点质量,将它们分配到各个核心中,然后让所有核心立即更新它们。然后,每个内核都可以在局部位置紧密的循环中处理数据。这种并行的锁定步骤样式与GPU相似。这里最难的部分是确保将工作分成细粒度的块,以便将其平均分配实际上导致所有处理器的工作量相等。
但是,有时由于政治原因,现有代码或其他令人沮丧的情况,为每个子系统提供线程是最容易的。在那种情况下,最好避免为CPU繁重的工作量而制作比内核更多的OS线程(如果您的运行时带有轻量级线程,而这些线程恰好在内核之间保持平衡,那么这没什么大不了的)。另外,请避免过度交流。一个不错的技巧是尝试流水线。每个主要子系统可以同时处理不同的游戏状态。管道传输减少了子系统之间必要的通信量,因为它们都不需要同时访问同一数据,而且还可以消除瓶颈所造成的某些损害。例如,如果您的物理子系统往往需要很长时间才能完成,并且渲染子系统最终总是在等待它,那么如果在渲染子系统仍在前一个子系统上运行时为下一帧运行物理子系统,则您的绝对帧速率可能会更高帧。实际上,如果您有这样的瓶颈,并且无法通过其他任何方式消除它们,则流水线化可能是困扰子系统线程的最合理的理由。
有几件事情要考虑。每个子系统的线程路由很容易考虑,因为代码的分离从一开始就很明显。但是,根据子系统需要多少相互通信,线程间通信实际上可能会降低性能。此外,这仅可扩展到N个核心,其中N是您抽象为线程的子系统的数量。
如果您只是想对现有游戏进行多线程处理,那么这可能是阻力最小的途径。但是,如果您正在一些可能在多个游戏或项目之间共享的低级引擎系统上工作,我会考虑另一种方法。
这可能需要花些时间才能解决,但是如果您可以将工作分解为带有一组工作线程的作业队列,那么从长远来看,它将更好地扩展。随着最新,最强大的芯片拥有数以百万计的内核,您的游戏性能将随之扩展,从而激发更多的工作线程。
因此,基本上,如果您希望对现有项目进行某种并行处理,那么我将跨子系统进行并行处理。如果您要从头开始构建具有并行可伸缩性的新引擎,那么我将调查一个工作队列。
这个问题没有最佳答案,因为这取决于您要完成的工作。
xbox具有三个核心,可以在上下文切换开销成为问题之前处理几个线程。电脑可以处理更多的东西。
为了简化编程,很多游戏通常都是单线程的。这对于大多数个人游戏都很好。您唯一可能需要另一个线程的是网络和音频。
虚幻具有游戏线程,渲染线程,网络线程和音频线程(如果我没记错的话)。尽管能够支持单独的渲染线程可能很麻烦并且涉及很多基础工作,但这对于许多当前引擎来说是相当标准的。
为Rage开发的idTech5引擎实际上使用任何数量的线程,并且通过将游戏任务分解为由任务系统处理的“任务”来实现。他们的明确目标是当普通游戏系统上的核心数量跃升时,使其游戏引擎具有良好的扩展能力。
我使用(并已编写)的技术具有用于网络,输入,音频,渲染和计划的单独线程。然后,它具有可用于执行游戏任务的任意数量的线程,并且由调度线程进行管理。一个很多工作进入让所有的线程互相发挥很好,但它似乎运作良好,并得到很好的利用了多核系统,也许这是实现(现在的任务,我会打破音频/网络/ input将工作分解为工作线程可以更新的“任务”。
这实际上取决于您的最终目标。
每个子系统有一个线程是错误的方法。突然,您的应用无法扩展,因为某些子系统的需求比其他子系统要大得多。这是Supreme Commander采取的线程化方法,它不能扩展到两个内核以上,因为它们只有两个子系统占用了大量CPU渲染和物理/游戏逻辑,即使它们有16个线程,其他线程仅勉强完成任何工作,因此,游戏仅扩展到两个核心。
您应该做的是使用一种称为线程池的东西。这在某种程度上反映了在GPU上采取的方法-也就是说,您发布工作,然后任何可用线程都会简单地执行并执行,然后返回等待工作,将其视为线程的环形缓冲区。这种方法具有N核扩展的优势,并且非常适合于低核数和高核数的扩展。缺点是使用这种方法很难对线程所有权进行处理,因为不可能在任何给定时间知道哪个线程在做什么,因此必须非常严格地解决所有权问题。这也使使用Direct3D9等不支持多线程的技术变得非常困难。
线程池很难使用,但它们可以提供最佳结果。如果您需要非常好的伸缩性,或者有足够的时间进行处理,请使用线程池。如果您试图将并行性引入具有未知依赖问题和单线程技术的现有项目中,那么这不是您的解决方案。
没错,最关键的部分是尽可能避免同步。有几种方法可以实现此目的。
了解您的数据并根据您的处理需求将其存储在内存中。这使您可以计划并行计算而无需同步。不幸的是,由于通常会在不可预测的时间从不同的系统访问数据,因此在大多数情况下很难做到这一点。
定义明确的数据访问时间。您可以将主要报价分为x个阶段。如果确定线程X仅在特定阶段读取数据,则您还知道该数据可以由处于不同阶段的其他线程修改。
双缓冲您的数据。这是最简单的方法,但是它增加了延迟,因为线程X正在处理来自最后一帧的数据,而线程Y正在为下一帧准备数据。
我的个人经验表明,细粒度计算是最有效的方法,因为这些方法的伸缩性远优于基于子系统的解决方案。如果您对子系统进行线程化,则帧时间将绑定到最昂贵的子系统。这可能会导致所有线程,但会导致一个线程闲置,直到昂贵的子系统最终完成工作为止。如果您能够将游戏的大部分分解为小任务,则可以相应地安排这些任务,以免使内核闲置。但是,如果您已经拥有大量的代码库,那么这是很难实现的。
要考虑一些硬件限制,您应该尝试永远不要过度订购硬件。对于超额订购,我的意思是拥有比平台硬件线程更多的软件线程。特别是在PPC体系结构(Xbox360,PS3)上,任务切换确实非常昂贵。如果您有几个线程被超额订阅,而这些线程仅在很短的时间内触发(例如,一次帧),那当然是完全可以的。如果您以PC为目标,则应牢记内核数(或更好的硬件) -Threads)在不断增长,因此您将需要找到一种可扩展的解决方案,该方案利用了额外的CPU功能。因此,在这一方面,您应该尝试将代码设计为尽可能基于任务。
线程化应用程序的一般经验法则:每个CPU内核1个线程。在四核PC上表示4。如上所述,XBox 360具有3个核,但每个都有2个硬件线程,因此在这种情况下为6个线程。在PS3之类的系统上……祝您好运:)人们仍在设法弄清楚它。
我建议将每个系统设计为一个自包含的模块,如果需要,可以将其线程化。这通常意味着在模块与引擎的其余部分之间具有非常清晰定义的通信路径。我特别喜欢诸如渲染和音频之类的只读过程,以及诸如读取播放器输入以清除内容之类的“我们还在吗”的过程。为了解决AttackingHobo给出的答案,当您渲染30-60fps时,如果您的数据已过时1 / 30th-1 / 60th秒,那么它的确不会降低游戏的响应速度。永远记住,应用软件和视频游戏之间的主要区别是每秒执行30-60次操作。同样,
如果您对引擎的系统设计得足够好,那么每个引擎都可以在一个线程到另一个线程之间移动,以根据游戏等情况更适当地平衡引擎。理论上,如果需要在完全独立的计算机系统运行每个组件的地方,也可以在分布式系统中使用引擎。
我为每个逻辑核心创建一个线程(减去一个,以考虑主线程,该线程顺便负责渲染,但同时也充当辅助线程)。
我在整个帧中实时收集输入设备事件,但是直到帧结束才应用它们:它们将在下一帧中生效。我使用类似的逻辑来渲染(旧状态)与更新(新状态)。
我使用原子事件将不安全的操作推迟到同一帧中的晚些时候,并且我使用多个事件队列(作业队列)来实现内存屏障,从而为操作顺序提供铁腕保证,而无需锁定或等待(按作业优先级锁定空闲的并发队列)。
值得一提的是,任何作业都可以向同一优先级队列或更高优先级队列(在帧的后面提供)中发布子作业(更精细,并且接近原子性)。
给定我有三个这样的队列,除一个线程外,所有线程都可能每帧恰好停顿3次(同时等待其他线程完成以当前优先级发出的所有未完成的作业)。
这似乎是线程不活动的可接受水平!