c# - 抛开OO设计模式以在战略游戏中获得更好的性能?

标签 c# performance caching

假设由于某些原因和限制,我们希望进行高效编程。
我们应该搁置OOP吗?
让我们用一个例子来说明

public class CPlayer{
   Vector3 m_position;
   Quaternion m_rotation;
   // other fields
}
public class CPlayerController{
   CPlayer[] players;
   public CPlayerController(int _count){
      players=new CPlayer[_count];
   }
   public void ComputeClosestPlayer(CPlayer _player){
      for(int i=0;i<players.Length;i++){
           // find the closest player to _player
      }
   }
}


如果将类转换为结构,则可以利用该方法将播放器数组缓存在缓存中并获得更好的性能。当我们需要在ComputeClosestPlayer函数中迭代数组时,我们知道播放器结构是连续存储的,因此在读取数组的第一个元素时可以进入缓存。

public struct CPlayerController{
   CPlayer[] players;
   public CPlayerController(int _count){
      players=new CPlayer[_count];
   }
   public void ComputeClosestPlayer(CPlayer _player){
      for(int i=0;i<players.Length;i++){
           // find the closest player to _player
      }
   }
}


如果要获得更高的性能,可以将位置字段与类分开:

public Vector3[] m_positions;


所以现在,当我们调用函数时,只有位置(每个位置12个字节)被缓存在缓存中,而在先前的方法中,我们必须缓存占用更多​​内存的对象。

最后,我不知道这是一种标准方法,还是您避免将其与班级中的某些领域分开以获得更好的性能,并分享您的方法以在拥有很多物品和士兵的战略游戏中获得最佳性能

最佳答案

抛弃OO设计模式以获得更好的战略绩效。
  游戏?


我倾向于采用这种广泛的方法,将OOP用于视觉FX放在中央体系结构上,尤其是像这样的实体组件系统:

enter image description here

...其中蓝色的组件只是数据(structs没有其自身的功能)。如果组件内部完全没有任何功能,那么它就是纯数据结构功能(例如您在C ++中的std::vector或C#中的ArrayList中找到的功能)。

它确实使高效的事情更容易完成,但是对我来说,主要的好处不是效率。这是灵活性和可维护性。当我需要全新的行为时,当广泛的依赖关系流向数据而不是抽象时,我可以对系统进行小的本地修改或添加新的组件或添加新的系统。自从我接受这种方法以来,面对层叠设计更改的需求就已经成为过去。

“没有中央设计”

每个新的设计思想,无论多么疯狂,都倾向于在不破坏任何中央设计的情况下相当容易地扩展和添加到系统中,因为除了ECS之外,首先没有中央抽象设计(或一种包含功能的抽象设计)。数据库本身。除了对ECS数据库的依赖性以及每个系统中的少数几个组件(原始数据)之外,系统都是超级解耦的。

从线程安全之类的一切到何时何地发生的副作用,这使得每个系统都易于推理。每个系统执行一个定义明确的角色,可以非常直接地映射到业务需求。拥有中小型对象之间的通信/交互时,很难拥有设计和职责的理由:

enter image description here

...而以上这张图不是依赖关系图。在耦合方面,每个对象之间可能存在一个抽象,以这种方式解耦(例如:根据IMesh而不是具体的网格对对象进行建模),但是它们仍然可以相互交谈,并且所有交互和通信都可以使很难对正在发生的事情以及最有效的循环代码进行推理。

同时,第一个系统具有每个独立的系统,每个系统都以扁平管道样式的方式处理来自中央数据库的数据,这使我们很容易确定正在发生的事情,并非常有效地实现了循环执行的关键路径。它还使您可以坐下来在一个系统上工作而无需知道其他系统在做什么:要实现一个物理系统,您要做的就是从数据库中读取运动组件之类的东西并正确地转换数据。除了一些组件类型以及如何从ECS“数据库”中获取它们之外,您不需要了解太多有关实施和维护物理系统的知识。

这也使团队工作变得更容易,并雇用了新的开发人员,他们可以迅速投入工作,而无需花费两年的时间来教他们整个系统中的中央抽象如何工作,以便他们可以完成工作。他们可以像在几周内向系统中引入全新的物理引擎或渲染引擎一样,大胆地开始对软件设计产生影响。

效率


  如果我们将类转换为结构,则可以利用它来缓存
  播放器阵列在缓存中并获得更好的性能。


那只是在粒度级别上,对象开始妨碍性能。例如,如果尝试使用封装和隐藏其数据的抽象Pixel对象或IPixel接口表示图像的单个像素,那么即使不考虑动态分配的成本,它也很容易成为性能障碍。 。这样的细粒度对象往往会迫使您一次摸索一个像素的细粒度级别,同时对其公共接口进行摸索,因此,当我们拥有这种细粒度对象时,诸如在GPU上处理图像或在CPU上进行SIMD处理之类的优化就不存在了。我们和像素数据之间的障碍。

除此之外,您通常无法对接口进行编码,并且不能期望在这样的一个级别(一个像素)下获得有效的解决方案。我们无法隐藏像素格式之类的具体细节并将其抽象化,并期望编写高效的视频滤镜,每帧循环遍历数百万个像素,例如在足够低的级别上,我们必须开始针对具体细节编写代码,以实现合理的效率。对接口进行抽象和编码对您进行高级操作很有帮助。

但是,如果将Pixel转换为仅存储在Image中的原始数据,那自然就不适用。图像对象实际上是通常包含数百万个像素的容器,它并不是实现非常有效的解决方案的实际障碍。我们不必放弃OOP来编写非常有效的循环。我们可能只需要为最小的,最细小的对象存储几乎自己的任何数据,就可以这样做。

因此,一种替代策略是仅在较粗糙的级别上对对象建模。您不必设计Human类。您可以设计一个从Humans继承的Creatures类。现在,Humans的实现可能由循环多线程SIMD代码组成,该代码可以一次处理数千个人的数据(例如:存储在并行数组中的SoA字段)。

OOP的替代品

对于我来说,在设计的最广泛的层次上放弃OOP(我仍然使用OOP来实现每个子系统)对我来说很有吸引力,而像在ECS中使用组件一样,在中央层次上放弃了价值聚合。我将数据保持开放状态,并通过中央“数据库”进行访问。它使您可以轻松实施所需的系统,并有效地访问数据,而不必与设计抽象的层次作斗争,而无需绕过障碍并逐层抽象。

当然,有缺点,例如您的数据现在必须非常稳定(我们无法不断更改),否则您的系统将崩溃。而且,您还必须拥有一个不错的系统组织来做到这一点,以便可以在最少的位置访问和修改您的数据,以使您能够有效维护不变性,产生副作用的原因等。但是我发现ECS可以做到这一点很自然,因为很容易分辨出哪些系统访问了哪些组件,因此非常自然。

enter image description here

就我而言,我发现使用ECS进行大型设计非常合适,然后当您放大特定系统(例如物理系统或渲染系统)的实现时,他们使用OOP来帮助实现辅助数据结构和媒介大小的对象,以使系统的实现更易于理解。我发现OOP在中等规模的复杂性方面非常有用,但有时很难在最大规模上进行维护和优化。

热/冷字段拆分和价值汇总


  最后,我不知道这是一种标准方法,还是您避免使用它
  从班级中分离一些字段以获得更好的性能
  分享您的方法,以在策略游戏中获得最佳性能,
  你有很多物品和士兵


这有点特定于C#,我更像是C ++和C程序员,但是我相信我已经读过C#structs,只要不装箱,就可以连续存储在数组中。在减少缓存未命中方面,您获得的连续性可以带来很大的不同。

特别是在GC语言中,通常可以使用顺序分配器(Java中的“ Eden”空间,我相信C#做了类似的事情,尽管我没有阅读有关C#的实现细节的任何文章)来快速完成对象分配的初始集合。 GC实施。但是,在第一个GC周期之后,可以重新整理内存以允许在单个对象的基础上对其进行回收。如果您需要执行非常有效的顺序循环,则空间局部性的损失会真正损害性能。因此,在游戏的某些关键循环区域中,存储structsintfloat之类的原始数据类型的数组可能是有用的优化。

至于分离字段的方法,这对于SIMD处理和热/冷字段拆分很有用。热/冷字段拆分将频繁访问的数据字段与其他字段分开。例如,粒子系统可能会花费大量时间将粒子移至周围并检测它们是否发生碰撞。在关键路径中,对于那种情况下的粒子颜色,它绝对不感兴趣。

因此,在这种情况下,一种有效的优化方法可能是避免将颜色直接存储在粒子内部,而是将颜色提升并存储在其自己的单独并行数组中。这样,可以将不断访问的热数据加载到64字节的缓存行中,而无需将无关的数据(例如颜色)不必要地加载到其中,并通过使关键数据通过更多不相关的数据进行获取来减慢关键路径的速度。相关数据。

所有非平凡的优化都倾向于归结为交换,这些交换使性能偏向普通情况,但代价是罕见情况。为了讨价还价并找到好的交易,您希望使普通情况更快,即使这会使罕见情况慢一些。除了明显的低效之外,您通常无法使所有事情都快速完成,尽管如果您优化了常见情况,关键路径,您确实可以实现那样的效果,并且看起来对用户来说超级快。

内存访问和表示


  如果要获得更高的性能,可以将位置字段分开
  从班级:

public Vector3[] m_positions;



这将有助于实现SoA(阵列结构)方法,并且如果您的大部分关键循环都花时间以顺序访问或随机访问模式访问播放器的position,而不是例如rotation,则是有意义的。如果rotationposition都以随机访问模式同时被频繁访问,则使用AoS(结构数组)方法存储两者的struct可能最有意义。如果它们都以无随机访问的顺序访问模式进行访问,那么SoA可能会更好,不是因为它减少了缓存丢失(与AoS接近),而是因为它可以允许更有效的指令选择例如,通过优化程序可以将8个SPFP位置字段立即加载到YMM寄存器中,而旋转字段不会在处理循环中与更均匀的垂直算术交织在一起。

完善的SoA方法甚至可以分离您的职位组成部分。代替:

xyzxyzxyzxyz...


如果关键路径的访问模式都是顺序的并处理大量数据,则可能会喜欢此方法:

xxxxxxxx...
yyyyyyyy...
zzzzzzzz...


像这样:

float[] m_x;
float[] m_y;
float[] m_z;


这通常是所有SIMD指令的最友好的内存布局(允许您或优化器以与标量代码相同的方式使用SIMD,但一次只能将其应用于4个以上的字段),尽管您通常想要这种布局的顺序访问模式。如果是随机访问,则最终可能会导致高速缓存未命中率接近三倍。

至于您选择的内容,首先必须弄清楚如何在最关键的循环中访问数据,以了解如何非常有效地设计和表示数据。通常,您必须进行一些交换,以减慢罕见情况的速度,转而支持普通情况,因为如果使用SoA设计,则系统中可能仍有一些地方可以从AoS中受益,因此,如果关键路径是顺序的,请使用SoA加快常见情况的速度;如果非关键路径使用随机访问模式,则减慢罕见情况的速度。要想出最有效的解决方案,需要采取很多措施并做出各种折衷,这自然可以帮助进行测量,而且还可以预先有效地进行设计,因此您还必须考虑内存访问模式。一个访问模式的良好内存布局不一定对另一访问模式有利。

如有疑问,我会一直支持AoS,直到您遇到热点为止,因为它通常比并行阵列更容易维护。然后,您可以有选择地在事后应用SoA优化。关键是要找到呼吸室来做到这一点,如果您设计较粗的对象(例如Image,而不是Pixel,例如Humans,不是Human,例如ParticleSystem,不是Particle),就会发现这一点。 。如果您设计了随处可见的小物件,您可能会发现自己陷入了次优的表示形式,如果不破坏所有内容就无法更改。


  最后,我不知道这是一种标准方法,还是您避免使用它
  从类中分离某些字段以获得更好的性能[...]


实际上,它很普遍,并且在使用可以显式控制诸如C ++之类的内存布局的语言时,在计算机图形学和游戏等领域至少进行了相当广泛的讨论(至少不是太深奥)。但是,这些技术甚至适用于使用GC的语言,因为这些语言仍然可以让您以重要的方式对内存布局进行足够的控制,前提是它们至少可以为您提供类似struct这样的内容,并且可以将它们连续存储在数组。

有关高效内存访问模式的所有这些内容主要与连续性和空间局部性有关,因为我们正在处理循环,理想情况下,当我们将某些数据加载到缓存行中时,它不仅涵盖了我们感兴趣的那个元素的数据,而且还涵盖了还有下一个,以及下一个,依此类推。我们希望尽可能多的相关数据,尽可能少的无关数据。如果没有连续存储任何内容,则大多数内容将变得毫无意义(因为时间上的局域性除外),因为每次加载一个元素时,我们都会在各处加载无关的数据,但是几乎每种语言都为您提供了一些存储数据的方法即使您不能使用对象,也可以采用纯连续的方式。

我实际上已经看过一个用Java编写的小型交互式路径跟踪器,该跟踪器的速度可以与任何用C或C ++编写的同样小的交互式路径跟踪器相媲美。最主要的是,它避免对涉及BVH遍历和射线/三角形相交的关键零件使用对象。在那里,它使用了大的浮点数和整数数组,但其他所有东西都使用了OOP。如果您以此类语言谨慎地应用这些类型的优化,那么从性能的角度来看,它们可以开始提供非常令人印象深刻的结果,而不会陷入维护问题的困扰。

关于c# - 抛开OO设计模式以在战略游戏中获得更好的性能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48682268/

相关文章:

c# - 在文本树中表示一个自引用表

c# - 如何设置 SwashBuckle.AspNetCore.Swagger 以使用授权?

php - 删除 POST 数据,使后退按钮不会显示文档已过期

image - 谷歌索引 Magento 中缓存的产品图片

java - SpEL - 错误 : Method cannot be found on type

C# 接口(interface)继承(基础)

c# - 使用 DataGrid/GridView 控件表示分层数据

c# - azure service fabric 可靠字典 linq 查询非常慢

c# - 更快的克隆方式

c++ - C++ 中可忽略的计时测量?