聊聊DOTS

Unity DOTS

DOTS - Data Oriented Tech Stack - 面向数据的技术栈


前言

公司1024节日时,邀请人来介绍Unity DOTS,有点兴趣,自己也查阅了一番资料,简述一下我的理解。

背景

  • 硬件发展瓶颈
    • CPU发展,趋于平缓;后续发展由单核转向多核
    • 内存发展,整体量级不足,无法满足CPU需求
  • 缺少能充分榨干多核CPU的引擎框架

方案

  • Cache的充分利用
    • 减少碎片化内存
    • 减少Cache Miss
  • 减少数据关联性,多核批量处理数据(解耦关联,可以不加锁)



技术栈

  • Entity Component System(ECS)
  • Burst Compiler
    • 现在主流CPU都有专用浮点运算单元(尤其做 向量运算),过去的通用的编译器无法充分利用这块
  • C# Job System
    • 面向多线程编程的方案
    • 与ECS框架配合使用
  • Native Containers
    • 配合ECS、C# Job System使用
    • ECS需要用大量数组、队列等容器,传统的容器被封装过,性能低


ECS相关

结构类型:

  • Entity, 唯一ID, 指向所属Component位置, 作为类似指针但并不是指针
  • Component, 保存对象的数据, 可当作struct, 不支持托管类型(例如引用)
  • Archetype, N种Compoent的组合, 每个Archetype的内存是连续排布, Archetype的变化,是通过内存拷贝来进行(由于是纯拷贝,速度很快)
  • Chunk, Component存储在Chunk中,多个Chunk隶属于同一个Archetype
  • System/Job System, 处理数据的方法,Job System负责多线程处理

流程

  • 每个Entity有位置和速度两个属性,分别由 Component Velocity 和 Component Position 代表;
  • 在计算坐标的update中,所有 Component Velocity 与 Component Position 传递给 system 计算出坐标,从而更新 Component Position。

为什么性能高:

  • 正常模式下,在更新坐标的update中,遍历所有对象,获取对象的坐标及位置,计算出最终坐标,再更新位置
  • ECS中,获取所有速度及坐标组件,计算初最终坐标,更新坐标
  • 内存存储中,正常模式的对象的对应字段不一定在一块,因此Cache Miss相对高一些
  • 在DOTS中,提供内置容器及结构,进行更细致的内存管理;降低数据的耦合性后,可以执行多线程的操作



应用及发展

Physics
Hybrid Render(HDRP, URP)

Unity-ECS 渲染后端;相机culling、LOD计算,全用DOTS进行物理优化
未来 - GPU Driven渲染管线

我的理解:

对于当前的硬件趋势及问题的重要解决方案。
但是,要充分利用多核的无锁并行,就需要进行很大精力的数据解耦,这方面目前还是依赖开发者自行解决,因此,开发者的门槛其实很高。
所以,感觉并不是很适合在游戏开发内全部采用,更适合在一些关键模块中进行使用,也便于部分更专业的开发者维护。



笔记

以下是我在听介绍时的部分记录

解决性能问题,目标为真正的 AAA GAME

技术栈

  • Entity Component System(ECS)
  • Burst compiler(High Performance C# - HPC#)
    • 编译器
    • 现在主流CPU都有专用浮点运算单元(尤其做 向量运算),过去的通用的编译器无法充分利用这块
  • C# Job System
    • 面向多线程编程的方案
    • 与ECS框架配合使用
  • Native Containers
    • 配合ECS、C# Job System使用
    • ECS需要用大量数组、队列等容器,传统的容器被封装过,性能低

内存满足不了CPU

  • CPU开始开辟很多Cache,但是Cache有限
  • CPU从Cache读数据很快,没找到就产生一个Cache Miss,就要从RAM读取很慢
  • CPU读取Cache,一次读一行,每行基本 64字节

目标

  • 性能,充分利用多核
    • 市面上还没有哪款引擎能充分利用多核(前提,硬件性能濒近极限)
  • 高度模块化,选配

解决问题

  • Cache Miss的优化
  • 多线程的编程,批量处理数据,等比划分很多份,每个线程跑一部分(重点在于解耦,避免依赖性,因此访问内存可以不加锁)

Entity

  • 唯一ID,指向位置
  • Entity有数组,删除的时候,与最后一位进行交换(不会进行整体位移)
  • 不要通过EntityManager遍历数组查找entity,性能差;通过query访问

Archetype

  • n种component组合形式;每个archetype内存是连续排布,archetype发生变化,进行内存拷贝(仅仅拷贝,速度快,没有性能问题)
  • archetype太复杂,每个chunk存的entity有限,会剩余部分空间用不上

Chunks

  • 存Entity多个component,存储的性能问题
  • 固定大小 16KB(性能优化的时候,要注意)
  • 1个Archetype是chunk的数组
  • 解决内存碎片化

System

  • 处理数据,返回想要的内存数组

Job System

  • 多线程处理数据

Queries

  • 返回一组chunk,进行连续访问(先拿一个archetype,再筛想要的chunk返回)

Component

  • 保存对象的数据,类似于C的struct,不支持托管类型(例如引用),必须是实型
  • 希望每个component尽量小

Special Component

  • Tag Component,标记,不占内存
  • Shared Component,共享内存;Read Only;可以是托管类型(比如引用)
  • Chunk Component,Chunk内共享内存,可读可写,不可是托管类型
  • Class Component,让老结构更好转换到ECS框架;对引用类型数据存储;不推荐
  • System State Component,destroy的时候不被清空
    • 检测某个对象数据是否有改变,通过版本号是否变化

Dyncmic Buffers

  • 动态数组,无限扩充,默认128字节

World

  • 隔离,Entity ID可重复了

提供GameObject和ECS混用方式,编辑器用GameObject,会使用工具流自动转换为ECS





参考资料