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

Basics of Parallel Programming 并行编程的基础

在这里插入图片描述
核达到了上限,无法越做越快,只能通过更多的核来解决问题
在这里插入图片描述
Process 进程
有独立的存储单元,系统去管理,需要通过特殊机制去交换信息
Thread 线程
在进程之内,共享了内存。线程之间会分享很多内存,这些内存就是数据交换的通道。在这里插入图片描述
管理Tasking的方法
Preemptive Multitasking 抢占式多任务:
当这个线程/任务在跑时,调度者scheduler决定中断和返回。任务自身无法决定
Non-preemptive Multitasking 非抢占式多任务:
反过来让任务自身决定何时结束。好处是,如果任务全都是自己给的,控制能力较强。但是容易发生卡死。比较少用,多用于一些实时性非常高的操作系统中
在这里插入图片描述
线程的切换很贵:
一个线程被中断的话是非常费的,至少需要2000 多个CPU cycle
并且新调进来的线程,数据不在各级缓存中,得从内存中重新调用,时间可能需要 10000 ~ 1000000 多个CPU cycle
Job System就是用来解决这些问题在这里插入图片描述
并行化编程的两个问题:
1.Embarrassing Parallel Problem
让线程各自自主运行完毕,不会打断线程,不存在swaping 线程的问题。就是一堆独立的问题,各自算完,然后收口。例如Monte Carlo 积分的算法
2.Non-Embarrassing Parallel Problem
但是真实游戏案例里,无法把一个游戏所需的模拟分的那么清楚,各个系统之间有很多数据的依赖
在这里插入图片描述
Data Race 数据抢占。当读取和写入发生在一个数据,但是不同线程里,会导致读取后和操作时数据不一致在这里插入图片描述
Locking Primitives 锁算法
Critical Section:这段代码只有当前线程可执行,并且相关数据只归于当前线程,其他线程不得修改。
是阻断式编程在这里插入图片描述
阻断式编程
可能产生死锁。线程未解除锁定将卡死,并导致后续相关线程也卡死
在大型系统,上百个任务中,无法保证每个任务一定会成功,一个任务的失败可能导致整个系统的死锁
当高优先级的任务进来后,无法打断正在运行的低优先级任务,任务优先级失去了意义
因此lock尽量少用在这里插入图片描述
原子性操作:
在硬件底层实现,保证对变量的读写操作,不会被多个任务抢占
Load:从内存中加载到一个对于Thread安全的存储空间,再查看数值
Store:将数据写入内存

核心的想法是避免锁住整段代码。保证执行的指令中对这一个数值的内存的操作是原子性的在这里插入图片描述
空洞代表cpu正在等待
lock free编程:避免死锁
wait free:理论上的方式,通过一套数学方法尽可能避免cpu等待
cpu利用率100%几乎不可能。但是具体的,比如堆栈或者队列的操作,是可以做到wait free。比如高频的通讯协议。
需要严格的数学推演证明在这里插入图片描述
高级语言在编译器编译后无法知道具体的汇编语言的顺序,只保证结果是一致的,但是会在多线程中出现很大问题在这里插入图片描述
期望的逻辑是:
线程1里,a先等于2,b在等于0
线程2里,监测b等于0的时候,a应该要等于2
但实际编译器优化后,会任务a和b是两个无关的变量,
线程1里有可能b赋值给一个临时变量,然后b被赋值为0,a还未被赋值2,之后a是用临时变量来赋值2
此时线程2里看到了b等于0时,a等于2是不成立的
这是并行开发非常容易出现问题的地方在这里插入图片描述
每个CPU和每个不同的架构都会导致不同的顺序
常出现的是:PC模拟器上和实机的运行是完全不一致的

C++ 11 中可以通过显式的要求执行顺序,但是性能更低

乱序的原因:有大量的数据存储,读取,因为cpu通常是饥饿的等待数据,不会等待指令一条条执行,根据指令读取数据,而是整块指令和数据混在一整块在cpu中运行

这也是debug正常,但release失效的原因。因为debug往往是顺序的,但是release是乱序

Parallel Framework of Game Engine 游戏引擎的并行框架

在这里插入图片描述
在这里插入图片描述
Fixed Multi-Thread 固定多线程
大部分传统引擎的做法,根据Task分类成固定的线程,彼此之间不侵犯,在每个Frame开始的时候交换数据。
在2-4核的情况下较好。
但是很难保证四个线程的workload是一样的,会有的轻,有的重,是个木桶效应,所有线程要等待最慢的线程完成任务。
并且很难把负载重的任务分出来给其他闲置的线程。因为通常一个线程访问的数据是尽可能地在一块地方。以保证数据是安全的。另一原因是不同场景不同线程的负载不一样,在风景的场景里渲染压力大,战斗场景里模拟线程压力大
大概会有1/3的资源浪费
如果固定的线程高于电脑配置的核数,还是会发生线程抢占问题。而8核16核的电脑会有浪费
在这里插入图片描述
在这里插入图片描述
Thread Fork-Join
提取出游戏中一致性非常高,但是计算量很大,比如动画,物理模拟的一些运算,到一定时间,这些固定好的线程,会Fork一些子任务出来专门传递到Work Thread(预先申请好的),然后计算完后回收Task的结果
可以动态的生成work thread
很多基于unity和unreal的游戏都是这种做法
但还是会造成cpu闲置
在这里插入图片描述
虚幻中提供两种Thread:
Named Thread :明确的告诉是给Game, Logic,Render 等
Worker Thread:用于处理物理,动画,粒子等
在这里插入图片描述
一种更加复杂的架构
可以创建很多任务,设置好任务的dependency,都扔给核去处理,核会自动根据任务之间的dependency来决定执行顺序和并行任务
游戏是有很强的dependency的在这里插入图片描述Task Graph具体实现:
在代码中直接加入Link,构建好dependency后自动生成Graph
问题:
task树的构建是不透明的
真实游戏引擎里task的dependency是动态的,不是静态的(task graph动态的生成节点是非常复杂的,早期并没有wait功能)

Job System

在这里插入图片描述
协程
是非常轻量的多线程执行的一种方式
实际上是:通过Yield可以从函数执行的任意阶段跳出挂起,让出执行权。然后可以通过Invoke激活从跳出的节点继续执行
核心就是,在任务执行中间,能让道出去,并且能回来
现代高级语言里很多原生支持,如c#,go。但c++里协程很难实现在这里插入图片描述
线程是一个调硬件的中断,也就是整个环境context,栈stack都会重置,所以创建和中断的消耗性能非常高,是直接通知到操作系统的 OS
协程是由程序自己定义的,在一个线程里可以在很多的协程里来回切换,从cpu上看还是在一个线程里,通过程序来定义切换和激活,并没有激活核的切换在这里插入图片描述
有栈协程
关键是跳出再重新激活后,本地的变量是不会被污染或清空的
在这里插入图片描述
无栈协程
相当于本地的变量直接清空。c++中实现就相当于汇编中的Go To。对实施者要求很高,一旦没写好,会产生大量的bug
最大的好处是不需要保存和恢复整个栈的状态,切换成本很低。一般是非常底层可能用到在这里插入图片描述
Stackful ,是更适合面向更多开发者的协程
Stackless ,是用在更加底层,只有少数人用的,需要更多知识经验的人

协程一个很大的难点在于,在不同的操作系统,包括原生语言c,c++,汇编语言等底层语言,并不支持协程的机制。不同平台需要不同的机制在这里插入图片描述
基于光纤的任务系统
高速的线程管道,可以高速的载入各种Job并且自由的进行协程切换, 同时Job还可以设置dependency和priority。 在这里插入图片描述
该申请多少个Work Thread?
尽可能的一个work thread 对应一个 核,可以是逻辑核,可以是物理核,一般是逻辑核
让thread的swap几乎为0 在这里插入图片描述
一个个生成的Job直接载入Thread然后处理
Job有优先级和依赖问题在这里插入图片描述
根据Job的优先级,依赖关系和Work Thread 的饱和状态分配Job给working thread
与 Fork-join不同的就是在这里,是一个全并行化的方法在这里插入图片描述
执行模型:先进先出,先进后出
游戏引擎的工程实现中,实现Job System时,一般会有LIFO。因为游戏里很多job的产生,都是前一个job执行到一半,可能会产生多个不同的job。彼此之间是dependency的关系。也就是所,当前job所产生的新的job如果未完成的话,自身是执行不了的。因此是先进后出的模型。类似堆栈在这里插入图片描述
Scheduler会把Yield的Job扔到一个waiting list里。然后执行下一个任务
因为并不能预估每个Job所需要占用的时间
比如 等待IO, 复杂的运算,产生非常多的dependency等
从而导致Work Thread的分配不均
Scheduler会通过Job Stealing 把重负担Work Thread未执行的任务分配给空闲的Work Thread在这里插入图片描述
优点:
容易实现schedule
容易设计依赖关系
每个堆栈彼此独立
不存在 Thread Switch
缺点:
C++不能原生支持,可以参考Boost源码
不同OS实现方法不同
具体的底层工程非常复杂

Programming Paradigms 编程范式在这里插入图片描述

在这里插入图片描述
早期就是POP为主在这里插入图片描述
现代基本都是OOP在这里插入图片描述
OOP问题1:
有很多二义性。也就是设计上的问题,
如上图例子,是在玩家里写攻击敌人还是敌人里写收到攻击? 即一个动作(function)归属于哪个类
而且不同的人对于这个的写法是不一致的在这里插入图片描述
问题2:OOP是一个非常深的继承树。
如上图,就受到魔法伤害这个Function来说,到底是写在Go那一层,还是Monster这一层,还是具体的蜘蛛怪物这一层?
且每个人对这个问题的看法是不一样的。 在这里插入图片描述
问题3:基类会非常庞大臃肿,派生类可能只需要基类的几个功能
在这里插入图片描述
问题4:OOP的性能很低。
内存是分散的,数据会被分散到各个object里
虚函数在内存上各种跳跃,函数的重载,会有很多指针,导致代码执行时跳来跳去在这里插入图片描述
问题5:OOP的可测试性。
OOP测试需要创建所有的环境,所有的对象。来测试其中的某个函数是否正确。因为所有的数据被包含在对象当中。对象一层套一层,很难写单元测试

Data-Oriented Programming 面向数据编程

在这里插入图片描述
CPU的速度越来越快,但是内存的访问速度是跟不上,导致现代计算机有很复杂的缓存机制在这里插入图片描述
Cache 缓存
L1 Cache就是距离CPU最近的缓存,速度最快
L2 Cache是稍微远点的,速度略逊于L1
L3 Cache是直接链接内存的,速度也是Cache里最慢的
cpu从L1开始一层层查询数据f
想cache友好要尊从数据紧凑型。即数据尽量呆在一起在这里插入图片描述
SIMD:对4个float的加减乘除,看作一个vector,一次性做完,一次性读4个空间,一次性写4个空间。大部分硬件都有支持在这里插入图片描述
LRU:Cache被填满后 → 最近还在使用的留住 → 最近没用的东西剔除,
还有一种随机剔除算法,基于概率在这里插入图片描述
每次读写取的是一个cache line。
假设有一个数据,这个数据在各级cache之间。每个cache和memory都有一段,cpu要保证三个cache和memory中的数据都是一致的。因此是一层层读取,一层层写到内存,在这里插入图片描述这也是为什么逐行读取数据的效率会比逐列读取数据的效率快上很多倍原因,原因就是往下读会导致跳跃Cache Line 造成 Cache Miss在这里插入图片描述
DOP的核心思想:游戏世界里所有的表达都是数据
在这里插入图片描述
代码自身也是数据
主要考虑的就是尽量减少Cache Miss的问题在这里插入图片描述
在DOP中,会把数据和代码看作一个整体,尽可能保证数据和代码在cache中紧密地在一起(内存中可能是分开的)。保证代码执行完后,数据刚好能处理完。
相当于数据的处理者和要处理的数据是在一起的
例子:
把Cache看作一家工厂而言,code是工人,数据就是产品的材料,每个工人都只能处理特定的材料
如果发生在工厂里的材料不是工厂里的工人能够处理的,就需要材料等可以处理的工人进来; 或者工厂里的功人不能处理现在工厂里的材料,也就需要等待新的材料进来
所以最佳实践就是工人和对应能处理的材料一起进入工厂,这也就是DOP想要实现的东西

Performance-Sensitive Programming 性能敏感编程

在这里插入图片描述
减少执行顺序的依赖度,让代码之间的执行尽量不存在上下依赖关系
在这里插入图片描述
有两个函数,一个在读,一个在写同一个变量时,当两个线程同时读写的变量在同一个cache line里,会加重系统负载
因此,要尽量避免两个线程会同时读写一个cache line。即不要让两个线程同时访问很碎的数据,尽量让每个线程访问的数据就是自己的一块在这里插入图片描述
现代CPU中会对 Branch 语句,比如If 或者 Switch进行优化,会直接把预测执行的代码直接加载到cache里。如果发现条件不满足,则要swap掉提前加载好的代码,可能需要到冷数据(内存或硬盘)中去读取,会等很久在这里插入图片描述
上述例子,会反复在if,else之间跳动,从而导致反复切换cache,导致性能下降的很低1在这里插入图片描述
优化方式是,执行前先进行一次排序。这样只需要执行一次cache切换在这里插入图片描述
提前将数据分组,避免复杂的 If 和 Else, 使用不同的容器来分组数据,让一组容器对应一个处理代码从而减少处理代码在Cache中的swap

Performance-Sensitive Data Arrangements 性能敏感数据编程

在这里插入图片描述
在这里插入图片描述
当使用OOP的思想去定义一个东西一般使用的是Structure来定义其属性,而每一个这个东西都是使用这个整的structure来定义。如左图所示的粒子的定义。这就是AOS架构
这样来定义的话,假设我希望修改所有粒子的位置和速度,我们却需要去跳跃内存中的其他属性比如颜色和生命周期,这样就不利于高性能编程
但是如果使用SOA就可以直接把一整个数组pass进Cache,处理会高效很多,这样的思想就很像写Shader,因为GPU天生的就是数据驱动的

Entity Component System ECS 架构

在这里插入图片描述
在这里插入图片描述
Component-Based系统最自然的实现方式是使用OOP的方式来实现
但是会导致很多问题:
过多的虚函数指针问题
大量的代码是分散在各个类里
数据也是非常分散的
这样的代码实现效率是非常低的在这里插入图片描述
Entity:非常轻量,就是一个id,id指向了一组component。即用了哪些数据
Component:一块数据。没有任何业务逻辑,没有任何借口。纯数据(注意之前讲的Component base有很多接口,如tick,setProperty,getProperty)。可以进行读写操作,但是他本身并不知道自己的意义
System:用于处理component。业务逻辑所在地。可能会同时处理好几类的数据。例如有一个moving system,根据速度该position。health system会根据damage去调整health的值。

ECS 本质是一个理论框架,目的是充分利用Cache,多线程和DOP的特性达到高效的目的在这里插入图片描述
在这里插入图片描述
是类似于模板或者原型的概念
比如一个NPC就需要几个特定的Component,
Archetype类似 type of GO

目的是当ECS对上千个Entity检查的时候,不能去一个一个与检查Entity是否有特定的Component,这个访问就很慢。但是Archetype就节省了很多步骤。 在这里插入图片描述
Chunk
将内存定义成一个个Chunk, 然后把一类Archetype的所有的Component按照他的类型一个个去放进去。 一个Chunk里一定是同一类的Archetype
好处是当为了需要处理这个Chunk的时候可以直接提取整条的数据,没有损耗的忽略其他数据在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
大部分CPU操作的耗时

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/150389.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux本地docker一键部署traefik+内网穿透工具实现远程访问Web UI管理界面

文章目录 前言1. Docker 部署 Trfɪk2. 本地访问traefik测试3. Linux 安装cpolar4. 配置Traefik公网访问地址5. 公网远程访问Traefik6. 固定Traefik公网地址 前言 Trfɪk 是一个云原生的新型的 HTTP 反向代理、负载均衡软件,能轻易的部署微服务。它支持多种后端 (D…

LabVIEW和NIUSRP硬件加快了认知无线电开发

LabVIEW和NIUSRP硬件加快了认知无线电开发 对于电视频谱,主用户传输有两种类型:广播电视和节目制作和特殊事件(PMSE)设备。广播塔的位置已知,且覆盖电视传输塔(复用器)附近的某个特定地理区域(称为排除区域…

进程管理(四)

管程概念及实现要旨 管程引入了条件变量condition。 wait操作,把当前进程挂到条件变量对应的阻塞队列上去,signal把条件队列上的对手进程唤醒。 注意:条件变量的signal操作和信号量的signal是有区别的。条件变量的signal可能啥都不干,如果有阻塞的进程唤醒,没有啥事都不做…

从零开始的C++(十八)

avl树中insert的模拟实现 avl树特点: 1.是搜索二叉树 2.每个结点的左右子树高度差的绝对值不超过2 inser模拟实现: // 右单旋void RotateR(Node* pParent){Node* parent pParent;Node* pr parent->_pRight;Node* prl pr->_pLeft;//记录父节点…

红海营销时代,内容占位的出海品牌更有机会营销占位

#01 品牌出海:内容占位就是品牌营销占位 红海营销时代,内容信息充斥着用户周边。无论线上还是线下,生活工作、休闲娱乐等不同场景内,广告信息均无孔不入。对于用户来说,能记住的品牌或者商品往往寥寥无几。 占位营销…

全网好评!12个网络工程师必备工具!

你们好,我的网工朋友。 今天来一期久违的工具推荐。工欲善其事必先利其器,好的工具势必会让网工们如虎添翼。 快速掌握正确的工具,意味着你可以轻松地完成复杂的工作。 但市面上的软件太多了,到底选用哪个工具,这还…

【最新Tomcat】IntelliJ IDEA通用配置Tomcat教程(超详细)

前言 IntelliJ IDEA是一个强大的集成开发环境,能够大大简化Java应用程序的开发和部署过程。而Tomcat作为一个流行的Java Web服务器,其与IntelliJ IDEA的整合能够提供便捷的开发环境,让开发人员更专注于代码的创作与优化。 在配置IntelliJ IDE…

linux在非联网、无网络环境下,使用yumdownload、reportrack方法安装rpm包

文章目录 前言1、下载yum-utils​​2、yumdownloader3、repotrack4、安装5、yumdownloader和repotrack的区别总结 前言 当开发者在联网环境下使用Linux时,可以轻松地通过yum或apt-get安装软件。然而,在公司和企业中,由于安全原因&#xff0c…

执行npm的时候报权限问题的解决方案

我们在执行npm操作的过程中,会出现以下权限问题,解决方案: 管理员身份 运行cmd 切换目录到要执行命令的文件下 再进行npm操作即可

社交媒体驱动的独立站流量增长:YouTube引流技巧

随着互联网的不断发展,视频内容在网上变得越来越受欢迎。YouTube作为世界上最大的视频分享平台之一,成为了吸引流量和观众的理想场所。对于跨境卖家来说,利用YouTube来增加独立站流量是一种有效的策略,可以增加知名度、吸引潜在客…

IDEA创建SpringBoot的多模块项目教程

最近在写一个多模块的SpringBoot项目,基于过程总了一些总结,故把SpringBoot多个模块的项目创建记录下来。 首先,先建立一个父工程: (1)在IDEA工具栏选择File->New->Project (2&#xff0…

hypermesh学习总结(一)

1、hypermesh导入导出 2、hypermesh如何使用拓扑命令,连接多个几何体为一个? 3、hypermesh模式选择 分别有显示动力学模式explicit,标准模式Standard3D(静力学及模态等) 4、检查网格单元质量 5、基本平移旋转显示视角操作 6、更改网格划分最小…

【算法挨揍日记】day28——413. 等差数列划分、978. 最长湍流子数组

413. 等差数列划分 413. 等差数列划分 题目描述: 如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。 例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。 给你一个整数数组 nums…

链动2+1模式:创新营销引领白酒产业新潮流

在当今高度竞争的市场环境中,创新营销模式对于企业的发展至关重要。链动21模式作为一种独特的营销策略,将白酒产品与该模式相结合,充分发挥其优势,通过独特的身份晋升和奖励机制,快速建立销售渠道,提高用户…

【C语言.oj刷题】有序#整型矩阵元素查找##{思路+C源码}

目录 题目信息 题目分析: 法一: 遍历二维数组(低效) 思路 源码 局限性 法二: 对每一行二分查找(有所提效) 思路 源码 局限性 法三: 利用一切有利条件使用二分查找 思路 …

由两个独立的高增益运算放大器组成的运放芯片D258,可应用于音频信号处理系统上

D258是由两个独立的高增益运算放大器组成。可以是单电源工作,也可以是双电源工作,电源的电流消耗与电源电压大小无关。应用范围包括变频放大器、DC增益部件和所有常规运算放大电路。 主要特点: ● 可单电源或双电源 工作 ● 在一个封装内的两个经…

vue3项目安装eslint和prettier

【几乎最全/全网最长的 2 万 字】前端工程化完整流程:从头搭到尾(vue3 vite qiankun docker tailwindcss iview......)_前端工程化流程-CSDN博客 vue3tsvite项目中使用eslintprettierstylelinthusky指南 - 掘金 上面两篇文章相互结合操…

网络工程师网络配置经典例题(五)

1、配置SwitchA的单臂静态BFD特性 [SwitchA] bfd [SwitchA-bfd] quit [SwitchA] bfd 1 bind peer-ip 10.2.2.2 interface vlanif 10 source-ip 10.1.1.1 one-arm-echo [SwitchA-bfd-session-1] discriminator local 1 [SwitchA-bfd-session-1] min-echo-rx-interval 200 …

解决:Error: Missing binding xxxxx\node_modules\node-sass\vendor\win32-x64-83\

一、具体报错 二、报错原因 这个错误是由于缺少 node-sass 模块的绑定文件引起的。 三、导致原因 3.1、环境发生了变化 3.2、安装过程出现问题 四、解决方法步骤: 4.1、重新构建 node-sass 模块 npm rebuild node-sass 4.2、清除缓存并重新安装依赖 npm c…

20231117在ubuntu20.04下使用ZIP命令压缩文件夹

20231117在ubuntu20.04下使用ZIP命令压缩文件夹 2023/11/17 17:01 百度搜索:Ubuntu zip 压缩 https://blog.51cto.com/u_64214/7641253 Ubuntu压缩文件夹zip命令 原创 chenglei1208 2023-09-28 17:21:58博主文章分类:LINUX 小工具 文章标签命令行压缩包U…