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操作的耗时

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何在 Nginx Proxy Manager(NPM)上部署静态网站

前言 众所周知,我们在之前介绍过 Nginx Proxy Manager(以下简称 NPM) 这个反向代理的神器,对于一些 Docker 搭建的 Web 项目,NPM 能够很轻松地给他们做反向代理。 然而对于一些静态网站,小伙伴们可能不知道怎么用 NP…

15分钟,不,用模板做数据可视化只需5分钟

测试显示,一个对奥威BI软件不太熟悉的人来开发数据可视化报表,要15分钟,而当这个人去套用数据可视化模板做报表,只需5分钟! 数据可视化模板是奥威BI上的一个特色功能板块。用户下载后更新数据源,立即就能获…

LeetCode【560】和为k的子数组

题目: 思路: 转化为前缀和问题,和为k,即为:前缀和差值为k的情况统计; 为什么要转化为前缀和呢?因为和为k的子数组可能有n个元素,但是前缀和差值为k,只有两个元素&#…

微机原理_9

一、单项选择题(本大题共15小题,每小题3分,共45分。在每小题给出的四个备选项中,选出一个正确的答案。 1.当运算结果的最高位为1时,标志位() A. CF1 B. OF1 C. SF1 D. ZF1 2、汇编语言源程序中,每个语句由四项组成,如语句要完成一定功能,那么该语句中不可…

HTTP1.1协议详解

目录 协议介绍协议的特点存在的问题协议优化方案与HTTP 1.0协议的区别 协议介绍 HTTP 1.1是一种基于文本的互联网实体信息交互协议,是Web上任何数据交换和客户端-服务器交互的基础。它允许获取各种类型的资源,如HTML文档,并支持在互联网上交…

系列三、双亲委派机制

一、概述 当一个类收到了类加载的请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一层的类加载器都是如此,因此所有的请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个…

arcgis--创建多分辨率DEM

方法一:技术链为【栅格转点】-【创建TIN】-【TIN转栅格】。首先需要将栅格转成点数据,再根据点数据创建TIN,再将TIN转栅格。 1、打开一幅DEM影像图,如下: 在【转换工具】-【由栅格转出】 -【栅格转点】工具中&#xf…

设计模式(4)-行为型模式

行为型模式 行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。 行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间…

LeetCode(18)整数转罗马数字【数组/字符串】【中等】

目录 1.题目2.答案3.提交结果截图 链接: 12. 整数转罗马数字 1.题目 罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X …

SpringBoot--中间件技术-4:整合Shiro,Shiro基于会话SessionManager实现分布式认证,附案例含源代码!

SpringBoot整合安全中间件Shiro 技术栈&#xff1a;SpringBootShiro 代码实现 pom文件加坐标 Springboot版本选择2.7.14 &#xff1b;java版本1.8 &#xff1b; shiro做了版本锁定 1.3.2 <properties><java.version>1.8</java.version><!--shiro版本锁定…

鸿蒙:从0到“Hello Harmony”

效果展示 一.概述 明年华为鸿蒙就不再兼容Android生态了&#xff0c;作为拥有7亿终端用户的华为&#xff0c;建立自己的生态也是理所当然。 所以对HarmonyOS的研究也是众多开发者绕不开的坎了。 今天这篇博文主要实现一个“Hello Harmony&#xff01;”的Demo。 二.官方链接…

ChatGLM3-6B:新一代开源双语对话语言模型,流畅对话与低部署门槛再升级

项目设计集合&#xff08;人工智能方向&#xff09;&#xff1a;助力新人快速实战掌握技能、自主完成项目设计升级&#xff0c;提升自身的硬实力&#xff08;不仅限NLP、知识图谱、计算机视觉等领域&#xff09;&#xff1a;汇总有意义的项目设计集合&#xff0c;助力新人快速实…

SystemVerilog学习 (6)——验证平台

一、概述 测试平台&#xff08;Testbench&#xff09;是整个验证系统的总称。它包含了验证系统的各个组件、组件之间的互联关系&#xff0c;测试平台的配置与控制等&#xff0c; 从更系统的意义来讲&#xff0c;它还包括编译仿真的流程、结果分析报告和覆盖率检查等。 从狭义上…

【ArcGIS Pro二次开发】(76):面积平差工具

之前做过一个【三调土地利用现状分类面积汇总】的工具&#xff0c;在流程中使用了面积平差的方法。 考虑了在其它场合可能也需要进行面积平差&#xff0c;因此单独提取出来作为一个工具。 平差实现的方法如下图&#xff1a; 主要的计算过程如上图所示&#xff0c;算出总面积差…

ubuntu下C++调用matplotlibcpp进行画图(超详细)

目录 一、换源 二、安装必要的软件 三、下载matplotlibcpp 四、下载anaconda 1.anaconda下载 2.使用anaconda配置环境 五、下载CLion 1.下载解压CLion 2.替换jbr文件夹 3.安装CLion 4.激活CLion 5.CLion汉化 6.Clion配置 六、使用CLion运行 七、总结 我的环…

总结1057

考研倒计38天 极限冲刺day1 今日共计学习13h33m&#xff0c;为了能走出备考的低谷阶段&#xff0c;来一场与自我的较量。在尽可能保证效率的情况下&#xff0c;玩命干。考研这件事&#xff0c;从来不是因为看到了希望才去努力&#xff0c;而是玩命努力后才看到希望。

通过IP地理位置阻止网络攻击

随着网络技术的不断发展&#xff0c;网络安全问题日益引起人们的关注。网络攻击者往往隐藏在虚拟的网络世界中&#xff0c;难以追踪其真实身份和位置。然而&#xff0c;近年来技术专家们借助IP地址定位的方法来阻止网络被攻击&#xff0c;这种方法引起了广泛关注。本文将探讨通…

posix定时器的使用

POSIX定时器是基于POSIX标准定义的一组函数&#xff0c;用于实现在Linux系统中创建和管理定时器。POSIX定时器提供了一种相对较高的精度&#xff0c;可用于实现毫秒级别的定时功能。 POSIX定时器的主要函数包括&#xff1a; timer_create()&#xff1a;用于创建一个定时器对象…

图解分布式事务实现原理(一)

参考 本文参考https://zhuanlan.zhihu.com/p/648556608&#xff0c;在小徐的基础上做了个人的笔记。 分布式事务场景 事务核心特性 在聊分布式事务之前&#xff0c;我们先理清楚有关于 “事务” 的定义. 事务 Transaction&#xff0c;是一段特殊的执行程序&#xff0c;其需…

基于ChatGPT的文本生成艺术框架—WordArt Designer

WordArt Designer是一个基于gpt-3.5 turbo的艺术字生成框架&#xff0c;包含四个关键模块:LLM引擎、SemTypo、Styltypo和TextTypo模块。由gpt-3.5 turbo驱动的LLM引擎可以解释用户输入&#xff0c;从而将抽象概念转化为具体的设计。 SemTypo模块使用语义概念优化字体设计&…