前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity手游实战:从0开始SLG——ECS战斗(六)Unity面向数据技术栈(DOTS)

Unity手游实战:从0开始SLG——ECS战斗(六)Unity面向数据技术栈(DOTS)

作者头像
放牛的星星
发布2020-07-10 17:30:58
2.2K0
发布2020-07-10 17:30:58
举报
文章被收录于专栏:壹种念头壹种念头

什么是DOTS?

DOTS是Unity一个阶段性的转变,也是Unity蓝图上一个非常重要的里程碑节点。Unity的官网为它建立了主题链接,甚至打出了阶段性的口号: 重建Unity的核心!,可见Unity对DOTS的重视程度。

那么DOTS的含义是什么呢?看下官网的截图:

高性能多线程式数据导向性技术堆栈 。可以看到DOTS的几个关键词, 高性能多线程 数据导向 堆栈

那么它用什么去保障这些关键词呢?

C# jobs System

jobs System 命中了DOTS里的高性能、多线程和堆栈关键字。上一篇我们讲过CPU执行代码片段的大体流程,那么CPU执行程序的流程也基本和上一篇展示的一样。把代码编译成EXE,然后加载进内存、送进CPU中执行。

更详细的过程可以查看这里:https://www.cnblogs.com/fengliu-/p/9269387.html

进程、线程和协程

现在的计算机结构大都是面向线程设计的了,但在计算机诞生早期的时候,计算机经历过从单一的程序处理逐步演变为多任务处理的过程。但不管是单一任务还是多任务,计算机执行的基础单位都是进程(如果这部分的基础确实不强,你可以粗略认为一个EXE就是一个进程)。每一个进程之间是有独立的资源分配的,包括但不限于文本区域、数据区域和堆栈区域。

文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。那么计算机又是怎么执行多个程序的呢?答案就是操作系统。

操作系统统一管控计算机的各个硬件资源,然后按照调度需求分别给不同的进程执行指定的时间片段,因为计算机的处理速度非常快,所以会让用户感觉在同时运行多个程序(进程)。但是这种模式也不是没有成本的,当并行的进程数量过多的时候,切换进程的代价就会非常大,因为它必须要先把当前的上下文存储,然后加载新的上下文,然后执行片段时间,备份存储,再执行下一个进程片段。切换上下文的代价有时候比执行本身的代价还要大。

线程是CPU执行的最小单位了,现在我们说多线程都是指这个。线程是进程中的实体表现,一个进程可以拥有很多个线程,每个线程受CPU独立调度和分派,可以想象Unity移动游戏开发中,Unity的主线程和网络的socket线程就是一个多线程的表现。

现在的计算机因为多核的并行计算,所以已经程序设计也更多的基于多线程的方式去设计了。(这里要理解一个概念,并发和并行。并发就是进程的执行模式,指多个任务在同一时间段内交替执行;并行是线程的执行模式,不同的线程在同一时间段同时执行。)

线程的另一个表现就是资源共享,同一个进程里的不同线程共享内存地址和资源。它自己本身不会申请系统资源(除了运行时必须的那一小点儿),所有的资源都来自于包含它的进程空间,这让程序处理资源更加的快捷和便利,利用多线程的优势来提高计算效率,当然这也正是多线程编程的难点所在。即使在多核CPU和面向线程设计的计算机结构面世怎么多年,仍然不能普及多线程编程。

协程可以简单的理解为用户自定义线程。对于进程和线程,你一旦创建了之后,就失去了对它的控制权,只能交由内核去分配时间片和执行。但是协程是用户自己创建的一个“线程”,所以从操作系统的层面来说,它不受内核调度,你可以在一个线程里创建无数个协程(硬件允许)来辅助你的代码逻辑,你可以自己控制它的执行时间和状态,也可以通过一个协程拉起另外的协程,而只需要牺牲很小一部分的切换代价。

所以总结来说,一个进程可以拥有很多个线程,每个线程又可以创建很多个协程。进程负责独立的地址空间和资源管理,线程共享进程的这些资源。线程提高了CPU的并行能力,但是进程方便跨平台移植,但这两个都需要消耗计算机的切换上下文的调度时间。协程在线程内执行,避免了无意义的调度,同样的调度责任转移给了开发者,同样因为寄生在线程内部,不能由内核调配,也无法充分利用硬件资源。

多线程编程

前面说了一个线程是内核调度的最小单元。那么根据运行环境和调度组的身份,又可以分为内核线程和用户线程。顾名思义,一个内核线程就是运行在内核环境,由内核分配和调度的线程。用户线程是运行在用户空间,由线程库来调度的。

当一个进程的内核线程获得了CPU的使用权限之后,它就会加载一个用户线程来执行,所以这么看来,内核线程其实就是用户线程的容器。

由于线程之间是共用同一个进程的资源的,所以线程的安全也是多线程编程最需要注意的问题。简单的来说就是如何管理多线程对于同一个资源的访问和修改,确保它们能按照正常的逻辑执行不出问题。

比如线程1需要改写a的值,而线程2需要读取a的值,因为线程的调度由内核控制,所以如果执行的顺序错了,那么结果就会完全偏离(行业术语叫 竞态条件)。

那么解决的方法大概罗列一下(不详细叙述):

  • synchronized 在关键的方法前面标识synchronized,这会让后面需求的线程等待,直到前面持有的线程完成调用。缺点是如果锁住的方法不是静态,那么就会锁住对象本身。那么所有对这个对象的访问都要等待,如果代码中存在多个synchronized 方法会严重影响性能。
  • Lock 这个方法和synchronized 不同在于,Lock是按需去锁,这种就需要自己对于变量有较强的把控。

jobs System的多线程

严格来说,Jobs System并不属于多线程编程的范畴,因为它不能直接对线程进程操作。相应的,它为了保障线程安全,独立封装了多线程的调度框架,用户只要继承一些类和接口,并且使用满足条件的指定数据类型才能完成高性能的计算,所以我个人认为 jobs是一个多线程的调度框架而不是编程框架。

jobs为了避免和主线程的数据发生冲突,所以避免使用引用类型。另外,还定义了一套自定义的数据结构,使用专门的未托管内存进行管理,称之为原生容器(NativeContainer)。包括以下几种:

一个简单的使用jobs的示例代码:

1、定义一个struct继承自Ijob。

2、添加jobs 使用的数据类型,(Blittable types或者NativeContainer类型)Blittable types可以理解为C#的值类型,包括:

3、重写 Execute 方法。

要非常小心的是,除了NativeContainer,其他都是数据的copy。所以要想从主线程访问计算的结果,唯一的方法就是放到NativeContainer里面。

Jobs的使用其实并不是很方便,有很多需要注意的地方,可以参考官方手册查看常见的坑点:

https://docs.unity3d.com/Manual/JobSystemTroubleshooting.html

Unity ECS

ECS 命中了DOTS里的 高性能 、 数据导向 、和 堆栈 关键字。

前面的一些章节,我们已经详细的讲过ECS的思想,以及高性能的原因,和一个基于Unity比较老的插件,Entitas。那么这一部分我们就不再拓展讲解ECS的原理部分,只看看它和我们之前的Entitas有哪些区别。

Unity的ECS组件叫做entities,和Entitas名字很像。但是实现的架构其实完全不一样。

先来看下创建Entity和设置Component:

上面是Unity的ECS,下面是Entitas。

再看下System:

上面是Unity的ECS,下面是Entitas。

毕竟是亲儿子,UnityECS里的System 那是三管齐下了。[BurstCompile]标签,job已经全部用上了。需要详细了解的,文档在这:

https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html

Burst

Unity目前主推的编译器,号称是比C++编译器还要快的存在。这里直接放官网的描述来看:

这部分的结构主要还是命中 高性能 的关键字。

我们在讲LLVM之前,先简单讲讲Unity一直在使用的技术方案。

打开新版本的Unity(2018.4),在player Setting选项里可以看到这个:

目前默认的是Mono和IL2CPP两个编译选项。

Mono

Mono就不用说了,是Unity跨平台的基础,也是赖以起家的手段。为Unity服务了这么多年,目前已经到了退役的阶段。

作为IL中间件的执行载体,为不同的平台提供了ILR。

看下Mono的执行过程。

虽然为Unity实现了跨平台,但是越来越多的问题累计,导致Unity不得不要抛弃它,另寻出路,主要有几点原因:

  • Mono的版权受限,导致Unity往往不能在最新版中使用C#的最新特性。
  • 性能存在较大问题,毕竟是虚拟机。
  • 维护非常困难,虽然IL是统一标准,但是VM不是!每个新增平台,Unity都需要自己为它们准备VM,Mono是一个开源的项目,但它并不会及时跟进每一个新硬件平台的VM编写,所以Unity得自己移植或者编写。而一些基于Web的平台,几乎要完全重写,比如WEBGL。
  • Mono无法完成64位版本要求。尤其是今年8月谷歌已经强制要求谷歌商店的APP必须同时提供64位版本。IL2CPP是目前满足条件的唯一选择。

IL2CPP

IL2CPP看名字就看出来,这是一个将IL语言转换为CPP语言的工具,看下它的执行方式:

可以看到下面红色的部分,IL2CPP会将编译好的IL代码重写成CPP的代码,这样在使用每个平台的原生编译器,编译为原生平台的可执行文件,由于抛弃了虚拟机,并被原生编译器优化过,所以极大的提升了程序性能。

看下官方给的数据,平均性能提升1.5-2.0倍。

注意,我刚才其实有说IL2CPP抛弃了虚拟机,但是在上面的执行过程图里仍然有I2CPP VM的过程,这是因为C#本身是基于托管代码设计的语言,IL本身也是托管代码执行的,所以IL2CPP即使将IL转为了CPP语言,这部分的设计框架是没法转换的。所以IL2CPP要起一个VM来管理内存,以及分配线程等管理工作。与其说是一个VM其实描述为一个管理器更加贴合。

这里要注意VM和管理器的区别,一个是完全承载代码的解释和执行工作,一个只是负责管理一些内存和特性,所以从大小和复杂程度上后者都远远小于前者。

LLVM

从Unity的专题页面描述可以看到,Burst是基于LLVM来编译的,所以先看下维基百科对LLVM的定义:

LLVM是一个自由软件项目,它是一种编译器基础设施,以C++写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端和后端。它是为了任意一种编程语言而写成的程序,利用虚拟技术创造出编译时期、链接时期、运行时期以及“闲置时期”的最优化。它最早以C/C++为实现对象,而当前它已支持包括ActionScript、Ada、D语言、Fortran、GLSL、Haskell、Java字节码、Objective-C、Swift、Python、Ruby、Rust、Scala[1]以及C#[2]等语言。

链接:https://zh.wikipedia.org/wiki/LLVM

LLVM提供了完整编译系统的中间层,它会将中间语言(Intermediate Representation,IR)从编译器取出与最优化,最优化后的IR接着被转换及链接到目标平台的汇编语言。LLVM可以接受来自GCC工具链所编译的IR,包含它底下现存的编译器。LLVM也可以在编译时期、链接时期,甚至是运行时期产生可重新定位的代码(Relocatable Code)。

大概来看下过程:

LLVM分为前端、中间件、后端三个部分。

前端:

简单来说就是通过对不同语言的词法,语法、语义分析,产生中间件代码。

中间件:

LLVM的核心是中间件表达式(Intermediate Representation,IR),一种类似汇编的底层语言。IR是一种强类型的精简指令集(Reduced Instruction Set Computing,RISC),并对目标指令集进行了抽象。

一个简单的Hello World程序可以表达为如下的汇编形式:

后端:

最关键的就是它支持与与语言无关的指令集架构和类型系统。(还记得我们上一篇讲过简单指令集和复杂指令集的区别嘛?ARM和X86指令集的区别)

到目前为止,LLVM已经支持多种后端指令集,比如ARM、Qualcomm Hexagon、MIPS、Nvidia并行指令集(PTX;在LLVM文档中被称为NVPTX),PowerPC、AMD TeraScale、AMD Graphics Core Next(GCN)、SPARC、z/Architecture(在LLVM文档中被称为SystemZ)、x86、x86-64和XCore。有部分平台功能并没有完全实现。但x86、x86-64、z/Architecture、ARM和PowerPC的基本所有功能都有实现了。

链接器:

lld链接器子项目旨在为LLVM开发一个内置的,平台独立的链接器,去除对所有第三方链接器的依赖。在2017年5月,lld已经支持ELF、PE/COFF、 和Mach-O。在lld支持不完全的情况下,用户可以使用其他项目,如GNU ld链接器。lld支持链接时优化。当LLVM链接时优化被启用时,LLVM可以输出bitcode而不是本机代码,而本机代码生成由链接器优化处理。

看完LLVM的原理,是不是觉得很熟悉?和Mono很像?都是先把第三方语言转化为中间件,然后再对中间件做兼容处理对吧?但是要注意的是,Mono针对的是运行期,而LLVM针对的是编译期!并且前面说了Mono是针对硬件平台的虚拟机,而LLVM是针对指令集的架构!所以无论是从性能还是数量以及扩展性上来说,LLVM都是远远高于Mono的。(据说Burst编译器最好的时候比C++的快30%)

针对Unity的DOTS目前就是这个全家桶,有很多相关技术视频在官方主题网页里,想要了解更多可以去听一听。本次ECS战斗相关的部分目前只计划了6章,已经全部完成。后面会讲讲UI的框架结构。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • C# jobs System
  • 进程、线程和协程
  • 多线程编程
  • jobs System的多线程
  • Unity ECS
  • Burst
  • Mono
  • IL2CPP
  • LLVM
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档