ECS与DOP(面向数据编程)浅析

这篇具有很好参考价值的文章主要介绍了ECS与DOP(面向数据编程)浅析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

序言

以下内容大部分是文字读起来确实会有点繁琐,内容主要是个人对ECS以及DOP的一些理解,如有错误欢迎指出,我觉得如果你正在学习ECS以及DOP,好好地读一下这篇文章是能够给你带来收获的。

什么是DOP

DOP(data-oriented programming),也就是面向数据编程,一种编程范式。与面向对象以及面向函数类似,可以理解为是一种“指导你进行程序设计”的方法。将它与更为熟知的面向对象(OOP object-oriented programming)作比较的话,面向对象更关心“类”之间的关系,而DOP更关心“数据”之间的关系。或者更直白地说,面向数据编程倾向于将“大概率会被一并使用”的数据放在连续的地址空间中,从而提高访问数据时CPU击中的概率,进而提高数据访问的效率。而面向对象则更关心开发人员对抽象关系的理解,也就更易于被理解一点。至于为什么可以提高CPU击中的概率,简单说的话就是CPU也是有缓存的,当一个数据被加载的时候,其附近的数据会被加载到缓存中,而直接从缓存中读取数据要比从内存中读取快得多,这部分不是很理解的朋友们建议还是找一篇文章详细地了解一下,对于理解DOP很有帮助,可以说这就是DOP之所以被提出以及之所以能够实现优化的核心。

举一个简单的例子,当要建立一个森林时,面向对象往往是先建立一个树的类,树包含的树需要的各种信息,比如种植日期,品类,坐标,然后由多个树的对象组成森林。而面向数据可能会选择的方式是,一个叫做森林的数据包含着多个数组,种植日期数组,品类数组,坐标数组,最后我们可以通过一个索引找到一颗树所需的所有数据。这样做的话,一棵树的组成就不像一个类的对象那么直观,但当我们试图访问所有树的种植日期时,面向数据的方式将会比面向对象更加快速,因为种植日期被存储到了连数组中(一段连续的地址空间,支持随机访问)。这只是一个很简单的例子,不要简单地理解为使用数组就是面向数据。个人认为一个优秀的面向数据架构应该是能够在”易于理解“以及优秀的数据读取效率上达到一定平衡的。ECS就是一个很好的例子,在本文的后续会提到。

值得一提的是,虽然都是编程范式,但DOP与OOP并不冲突。或者说我更愿意称其为一种”优化方法“。你大可以在OOP的项目中引入DOP,从而达到优化的目的。这种优化在数据量越大时越明显,特别是在游戏行业中。戴森球是一个典型的DOP项目,具体的内容可以从他们自己的分享文章中查看。

什么是ECS

ECS指的是Entity(实体) Component(组件) System(系统)。简单地描述一下这三者之间地关系,那就是Entity持有component,system通过获取所需地component来完成逻辑运算。一般来说Entity除了Component之外不太会包含别的数据(但还是会保存数据比如版本信息什么的),这应该很好理解,如果Entity把所有数据都直接包含了,那Component还有什么用?换句话说,那他和OOP又有什么区别。对于Component而言,它只关心数据,不包含任何方法,在ECS体系下,方法几乎只应存在与system中。

ECS与DOP的关系

首先明确一点,ECS算是DOP的产物,但一个项目使用了ECS并不意味着这就是一个经过DOP优化的项目。

继续借用之前树的例子,那么在ECS的结构下,位置,品类,种植日期就成了Component,这些component组成了一个Entity,也就是树。值得注意的是,当我们需要一个方法来获取植物的年龄时,如果使用的是OOP,那么我们或许会在Tree这个Class里写一个方法通过传入当前时间来与种植时间计算得出当前年龄。但ECS并不会在Component中写方法,而是会有一个处理这件事的system(或者说能够处理这件事的system,这里我想要明确的是system并不被要求只能做一件事,也不被要求只能使用一种component),这个system会获取到种植日期的component然后计算得到树的年龄。你或许想问,那我创建一个Class Tree,里面只包含属性,没有方法,然后写一个TreeProcesser来处理所有Tree Class的逻辑,那不就和ECS一个意思了吗?我想说,确实。在将数据与方法分开这件事上,二者达成的目的是一样的,但System真正的重点其实是在于当你试图或许一类component时,例如需要计算五棵树的平均年龄时,理想状态下这五个树的种植时间Component在内存中应该是连续的,而TreeProcesser获取到的会是五个tree,再从每个Tree中获取到种植时间,这五个终止时间在内存中必然不是连续地址(这很关键)

那么回到本小节开头提到的问题,为什么一个项目使用了ECS并不意味着这就是一个经过DOP优化的项目?其实刚才的例子已经回答了这个问题,当五个种植时间的Component并非连续的地址时,在数据读取的相率上,其实也就退化为了和普通的OOP一个水平。换句话说,如果同样的创建森林的需求,我们创建了一个森林类,使用了之前用数组来分别保存数据的方法,那么在取用五棵树的种植时间时,他就是连续的(地址上是连续的,但真的被cpu读取时,因为缓存有大小限制,并不一定能一次全读到缓存中,但即便如此,其效率还是要高于分散地址的数据),那么就算这是一个OOP的项目,它同样是经过DOP优化的。

由此可以看出,”连续的地址空间“是DOP中的一个关键点。那么在编写代码时,我们怎么知道数据是否被放在连续的地址空间中呢?这就比较考验大家的基本功了,举个简单的例子,数组是连续空间,链表不是,又比如在c++中,vector是连续空间,list不是,还有一个比较特殊的deque(由多段不连续的连续地址空间构成),感兴趣的可以去了解一下。或者还有一种方法,我们可以自己分配连续的空间。

那么直接都用数组是不是就解决问题了?确实。这个想法在我看来是可以给予肯定的,但就像我之前说的 ”一个优秀的面向数据架构应该是能够在”易于理解“以及优秀的数据读取效率上达到一定平衡的”。在部分情况下,直接大范围地使用数组在我看来完全是一个可取的方法,但撇开可能造成的理解成本,如果这个数组真的非常巨大,那么在数组中间进行插入和删除操作时,效率问题或许也是需要被考虑的。所以使用简单粗暴地使用数组在部分情况下是ok的,那么有没有别的更加优雅的方法呢?有的,Unity的ECS给出了一个不错的答案。

Unity的ECS

这里先直接放一个unity官方文档的链接,感兴趣的可以直接去看,讲的也必然比我详细:Unity的ECS

这里着重讲述以下Unity的ECS中是怎么解决内存分配问题的。

首先Unity的ECS中也有着Entity,Component,System及部分,其各部分之间的关系如下图:
ECS与DOP(面向数据编程)浅析
上图大概意思就是,有三个Entity,其中A和B具有相同的Component,还有一个system能够通过Translation和Rotation计算出LocalToWorld。这里也就能更明确地看出我之前提到的,一个System并非只能接受一个component。

Arhcetypes与ArchetypeChunk

进一步地,可以看到EntityA和EntityB被圈在了一起,这里也就可以引入Unity ECS的内存解决方案,Archetypes以及ArchetypeChunk。

在Unity的ECS中,具有相同component构成的实体会都会属于同一个Archetype,例如上图中,A与B属于一个ArcheType,C属于一个ArcheType。
ECS与DOP(面向数据编程)浅析
有了ArhceType之后,如果你想要新建一个Entity与之前已有的Entity具有相同的components,那就可以直接基于ArcheType生成。为此Unity还提供了不同的方法来生成Entity,你可以通过ArcheType来生成,也可以通过component生成(此时会根据是否已有当前构成的Archetype来决定是否生成新的ArcheType)。当一个Entity的component变化时,例如增加或删除,都会导致其ArcheType的变化。当有了ArcheType之后,就可以通过ArcheType来进行“优雅”的内存分配了。

ArchetypeChunk就是用来保存Entity的容器。一个Archetype对应了一个ArcheTypeChunk,所有的Entity的数据都会被存储在ArcheTypeChunk中。具体的存储方式为,而一个ArcheTypeChunk会持有Chunks,每一个Chunk保存着n个Entity的数据,当有新的Entity被创建时,会判断当前chunk中是否还有空余的空间,如果没有了就新建一个chunk然后把entity放进去。简单的理解,每一个entity的所有数据一定是在同一个chunk中的,其结果是对于每一个chunk来说,大概率都会有无法被使用的chunk。举个极端一点的例子,如果一个Entity占用3k,一个chunk的空间是16k(当前UnityECS给出的大小),那么5个entity加入后,这个chunk一定有1k是无法被使用的。但这一点损失其实还在可接受的范围内,一方面一个entity一般不至于这么大,最终损失的空间往往不大,另一方面通过ArchetypeChunk的方式已经避免了很多内存碎片。

试想如果没有ArcheType以及ArcheTypeChunk,我们也就无法知道一个chunk能够承载几个entity,假设Entity A,B,C,D分别占用40,100,80,90的空间,ABC被分配在一段连续的地址,然后B被释放了,这时候D被加载到原先B的空间,那么就会产生10的内存碎片,在大量的数据频繁地增删之后,这种碎片将会是很大的问题。但ArchetypeChunk解决了这个问题,每一个Entity都能被合理地放到Chunk里,当一个Entity的component发生改变时,他就会从原有的ArchetypeChunk中被移动到新的与之相对应的ArchetypeChunk中。

还有一点值得注意的是,Unity ECS提供了一种sharedComponent,这种component允许持有该component的entity共享其component的数值,而ECS也将为这些entity提供一个新的chunk。如果这个sharedcomponent的值被修改,或者被删除,都将导致这些entity被移动到别的chunk中或者新建一个新的chunk。因此应该尽量少地使用sharedcomponent,减少不必要内存的分配。

一个Chunk中的数据是如何排列的

知道了以上的概念之后,一个核心问题其实还没有被解答,那就是一个chunk内的数据又是如何被分配?

先不将Unity是如何做的,我们自己思考一下大概能想到两种方案:

  1. 一个Entity的数据顺序排列,例如EntityA(C1A,C2A,C3A),EntityB(C1B,C2B,C3B),那么在内存中其排列方式如下:
    EntityAHead, C1A,C2A,C3A, EntityBHead, C1B,C2B,C3B。
  2. 相同的component顺序排列,例如EntityA(C1A,C2A,C3A),EntityB(C1B,C2B,C3B),那么在内存中其排列方式如下:
    EntityAHead, EntityBHead, C1A,C1B,C2A,C2B,C3AC3B。

如果你认真看了之前的内容,大概也就能猜到,Unity将采用的是第二种方案,此时如果要获取所有C1(Component1),C1A和C2A因为在连续地址,是能够被快速访问到的。当然你也可以说当每个Entity够小时,我们也可以认为第一种方案的所有数据都是连续的,那我只能说 “OK Fine”,其实按照这个逻辑的话,所有数据在地址空间上都是连续的了哈哈哈哈。

那么采用的方法二之后,当我想要获取一个Entity的数据时会变得麻烦吗?不会。我们知道所有Entity的头部时在chunk的最前面的,我们知道head在哪,我们知道每一个component的长度,我们可以很轻易地获取到Entity的每一个component数据,并且因为一个entity一定在一个chunk中,我们都不需要考虑跳转chunk的情况。下图能够进一步帮助理解以上内容:
ECS与DOP(面向数据编程)浅析

参照上图,每一个ArcheType具有一些列的Chunk,每个chunk都存储着对应当前ArcheType的Entity,并且每个chunk中相同颜色的方块总是连续的,也就是上文说到的相同compont是按顺序存储的。

至此,Unity ECS的内存是如何分配的也就大概讲完了,回顾之前提到的如果数组非常长时插入的问题,在Chunk下已经被解决了。而且Unity的ECS在理解起来也不困难,我们在使用时只需要考虑entity由哪些component构成,至于怎么顺序存储,交给系统自己解决吧!优雅,实在优雅!事实上哪怕我们没有“巨量”对象这种需求,ECS同样是一个不错的游戏框架,毕竟对于一个游戏而言,能提升性能何乐而不为呢。在大部分情况下,ECS比起OOP的方式在计算效率上只会有过之而无不及,毕竟从ECS结构中单独获取一个Entity的数据也并非是一件难事。而且Unity还提供了很多有用的内容,例如在ECS的基础上使用Job system等,具体的内容就不再赘述了,感兴趣的可以自己去找资料看看。

关于ECS和DOP的一些建议

总而言之,ECS也好,DOP也好,其核心就是通过连续的地址空间来增加数据读取的效率。但并不是说“连续的地址空间”一定能优化项目的效率。只有当我们需要的数据是连续的时这种优势才能够被很好的体现。举个例子,一个Person类,其核心方法就是一个person需要根据自身的各种属性例如饥饿度,心情,性格来决定自己接下来的行动。这种时候如果你把person当作entity,把饥饿度,性格等当作component,在计算的时候获取到的主要是“一个Entity”的数据而不是“一类component”,这时ECS的优点其实并没有被很好的发挥。相反继续使用OOP的方式,项目可能更好理解,同时OOP易拓展易维护的特性也能给项目带来好处。合理地将OOP与DOP搭配使用同样是可以的,例如一个person类同样可以拥有类似于component的结构(所有的该类Component被统一管理顺序存储)。或者有需要的话person用oop,Tree用ecs也是可以的哈哈哈哈(当然了个人认为一个项目内的框架还是同一比较好,只是指出如果确实有需要,DOP,OOP以及ECS并不是什么互相矛盾的东西)。文章来源地址https://www.toymoban.com/news/detail-429765.html

到了这里,关于ECS与DOP(面向数据编程)浅析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Games104现代游戏引擎笔记 面向数据编程与任务系统

    核达到了上限,无法越做越快,只能通过更多的核来解决问题 Process 进程 有独立的存储单元,系统去管理,需要通过特殊机制去交换信息 Thread 线程 在进程之内,共享了内存。线程之间会分享很多内存,这些内存就是数据交换的通道。 管理Tasking的方法 Preemptive Multitasking 抢占

    2024年02月04日
    浏览(59)
  • 1 请使用js、css、html技术实现以下页面,表格内容根据查询条件动态变化。

            注意:         1.背景颜色用ppt的取色器来获取:                 先点击ppt的形状轮廓,然后点击取色器,吸颜色,然后再点击形状轮廓的其他轮廓颜色,即可获取到对应颜色。           2.表格间的灰色线是在th和td中用border属性设置的;         3.在js中拼

    2024年02月16日
    浏览(43)
  • Angular安全专辑之二——‘unsafe-eval’不是以下内容安全策略中允许的脚本源

    一:错误出现 这个错误的意思是,拒绝将字符串评估为 JavaScript,因为‘unsafe-eval’不是以下内容安全策略中允许的脚本源。 二:错误场景 类似的不安全的表达式还有: eval() Function() ——When passing a string literal like to methods like: setTimeout(\\\"alert(\\\"Hello World!\\\");\\\", 500); setTimeout() s

    2024年02月12日
    浏览(48)
  • 百度交易中台之内容分润结算系统架构浅析

    作者 | 交易中台团队 导读 随着公司内容生态的蓬勃发展,内容产出方和流量提供方最关注的“收益结算”的工作,也就成为重中之重。本文基于内容分润结算业务为入口,介绍了实现过程中的重难点,比如千万级和百万级数据量下的技术选型和最终实现,满足了业务需求的

    2024年02月07日
    浏览(42)
  • 02-ET框架的ECS编程思想

    TIPS: 本系列贴仅用于博主学习ET框架的记录 今天来学习OOP以外的另一种编程思想—ECS。 ECS:实体(Entity)、组件(Component)、系统(System),同时在框架中(实体即组件、组件即实体)类似电脑是一个实体,键盘是电脑的一个组件,但同时键盘也是一个实体,因为其下面还有按键这种

    2024年02月08日
    浏览(35)
  • Java 学习路线:基础知识、数据类型、条件语句、函数、循环、异常处理、数据结构、面向对象编程、包、文件和 API

    Java 是一种由 Sun Microsystems 于 1995 年首次发布的编程语言和计算平台。Java 是一种通用的、基于类的、面向对象的编程语言,旨在减少实现依赖性。它是一个应用程序开发的计算平台。Java 快速、安全、可靠,因此在笔记本电脑、数据中心、游戏机、科学超级计算机、手机等领

    2024年03月24日
    浏览(87)
  • 【Java 编程】文件操作,文件内容的读写—数据流

    平时说的文件一般都是指存储在 硬盘 上的普通文件 形如 txt, jpg, mp4, rar 等这些文件都可以认为是普通文件,它们都是在硬盘上存储的 在计算机中,文件可能是一个 广义的概念 ,就不只是包含普通文件,还可以包含 目录 (把目录称为目录文件) 操作系统中,还会使用文件来描

    2023年04月08日
    浏览(47)
  • AIGC内容分享(四十):生成式人工智能(AIGC)应用进展浅析

    目录 0   引言 1   以ChatGPT为代表的AIGC发展现状 1.1  国外AIGC应用发展现状 1.2  国内AIGC应用发展现状 2   AIGC的技术架构 (1)数据层 (2)算力基础设施层 (3)算法及大模型层 (4)AIGC能力层 (5)AIGC功能层 (6)AIGC应用层 3   AIGC面临的机遇与挑战 3.1  AIGC带来的机遇 (

    2024年02月19日
    浏览(55)
  • 【SQL Server】数据库开发指南(三)面向数据分析的 T-SQL 编程技巧与实践

    本系列博文还在更新中,收录在专栏:#MS-SQL Server 专栏中。 本系列文章列表如下: 【SQL Server】 Linux 运维下对 SQL Server 进行安装、升级、回滚、卸载操作 【SQL Server】数据库开发指南(一)数据库设计的核心概念和基本步骤 【SQL Server】数据库开发指南(二)MSSQL数据库开发对

    2023年04月12日
    浏览(92)
  • 响应式编程理论篇:源码浅析WebClient

    WebFlux系统中,如何请求第三方或其他内部兄弟系统提供的接口? 当然,可以直接使用OKhttp/Apache HttpClient/SpringMVC RestTemplate, 在WebFlux中同样提供了请求接口的工具:WebClient, 本篇文章主要讲解WebClient的功能,理论篇。 实践篇中讲解如何实战。 Spring WebClient官方地址: https:/

    2024年02月09日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包