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

news2024/11/26 6:23:50

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.coloradmin.cn/o/1213617.html

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

相关文章

Python数据容器之(元组)

我们前面所了解的列表是可以修改的,但如果想要传递的信息,不被篡改,列表就不合适了。 元组同列表一样,都是可以封装多个、不同类型的元素在内。 但最大的不同点在于: 元组一旦定义完成,就不可修改 所以…

Windows 11 设置 wsl-ubuntu 使用桥接网络

Windows 11 设置 wsl-ubuntu 使用桥接网络 0. 背景1. Windows 11 下启用 Hyper-V2. 使用 Hyper-V 虚拟交换机管理器创建虚拟网络3. 创建 .wslconfig 文件4. 配置 wsl.conf 文件5. 配置 wsl-network.conf 文件6. 创建 00-wsl2.yaml7. 安装 net-tools 和 openssh-server 0. 背景 …

SSD(Single Shot MultiBox Detector)的复现

SSD 背景 这是一种 single stage 的检测模型,相比于R-CNN系列模型上要简单许多。其精度可以与Faster R-CNN相匹敌,而速度达到了惊人的59FPS,速度上完爆 Fster R-CNN。 速度快的根本原因在于移除了 region proposals 步骤以及后续的像素采样或…

LeetCode(15)分发糖果【数组/字符串】【困难】

目录 1.题目2.答案3.提交结果截图 链接: 135. 分发糖果 1.题目 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获…

【微服务专题】Spring启动过程源码解析

目录 前言阅读对象阅读导航前置知识笔记正文一、SpringBoot启动过程源码解析1.1 SpringBoot启动过程源码流程图1.2 流程解析补充1.2.1 SpringApplicationRunListeners:SpringBoot运行过程监听器 学习总结感谢 前言 这部分只是个人的自结,方便后面回来看…

RK3588平台开发系列讲解(摄像头篇)USB摄像头驱动分析

🚀返回专栏总目录 文章目录 一. USB摄像头基本知识1.1 内部逻辑结构1.2 描述符实例解析二. UVC驱动框架2.1、设备枚举过程2.2、数据传输过程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 USB摄像头驱动位于 drivers\media\usb\uvc\uvc_driver.c ,我们本篇重点看下…

正版软件|Soundop 专业音频编辑器,实现无缝的音频制作工作流程

关于Soundop Soundop 音频编辑器 直观而专业的音频编辑软件,用于录制、编辑、混合和掌握音频内容。 Soundop 是一款适用于 Windows 的专业音频编辑器,可在具有高级功能的直观灵活的工作区中录制、编辑和掌握音频并混音轨道。音频文件编辑器支持波形和频谱…

一道 python 数据分析的题目

python 数据分析的题目。 做题方法:使用 pandas 读取数据,然后分析。 知识点:pandas,正则表达式,py知识。 过程:不断使用 GPT,遇到有问题的地方自己分析,把分析的结果告诉 GPT&am…

PPT转PDF转换器:便捷的批量PPT转PDF转换软件

在数字化时代,文档转换已成为日常工作不可或缺的一环。特别是对于那些需要转发或发布演示文稿的人来说,如果希望共享给他人的PPT文件在演示过程中不被修改,那么将PPT文件转换为PDF格式已经成为一个常见的选择。大多数PDF阅读器程序都支持全屏…

总结1057

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

USB复合设备构建CDC+HID鼠标键盘套装

最近需要做一个小工具,要用到USB CDCHID设备。又重新研究了一下USB协议和STM32的USB驱动库,也踩了不少坑,因此把代码修改过程记录一下。 开发环境: ST-LINK v2 STM32H743开发板 PC windows 11 cubeMX v6.9.2 cubeIDE v1.13.2 cub…

BIO、NIO、AIO三者的区别及其应用场景(结合生活例子,简单易懂)

再解释三者之前我们需要先了解几个概念: 阻塞、非阻塞:是相较于线程来说的,如果是阻塞则线程无法往下执行,不阻塞,则线程可以继续往下 执行。同步、异步:是相较于IO来说的,同步需要等待IO操作完…

HTTP1.1协议详解

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

CocosCreator3.8神秘面纱 CocosCreator 项目结构说明及编辑器的简单使用

我们通过Dashboard 创建一个2d项目,来演示CocosCreator 的项目结构。 等待创建完成后,会得到以下项目工程: 一、assets文件夹 assets文件夹:为资源目录,用来存储所有的本地资源,如各种图片,脚本…

零小时零信任:数据标记如何加速实施

现在是零信任的零小时。 虽然这个概念已经存在多年,但现在联邦政府实施它的时间已经紧迫。 拜登政府备忘录被誉为以战斗速度安全交付关键任务数据的解决方案,要求联邦机构在 2024 财年年底前实现具体的零信任安全目标。 此外,国防部正在努…

从0开始学习JavaScript--JavaScript DOM操作与事件处理

在前端开发中,DOM(文档对象模型)是一个至关重要的概念,它为JavaScript提供了一种与HTML和XML文档交互的方法。本文将深入探讨DOM的概念与作用,以及JavaScript与DOM之间的密切关系。 DOM的概念与作用 DOM是什么&#…

【接口自动化测试】Postman(一) 介绍和安装

一.Postman介绍 Postman是一款非常流行的接口调试工具,它使用简单,而且功能也很强大。不仅测试人员会使用,开发人员也会 经常使用。 主要特点 1. 简单易用的图形用户界面 2. 可以保存接口请求的历史记录 3. 使用测试集Collections可以更…

编程的简单实例,编程零基础入门教程,中文编程开发语言工具下载

编程的简单实例,编程零基础入门教程,中文编程开发语言工具下载 给大家分享一款中文编程工具,零基础轻松学编程,不需英语基础,编程工具可下载。 这款工具不但可以连接部分硬件,而且可以开发大型的软件&…

websocket学习笔记【springboot+websocket聊天室demo】

文章目录 WebSocket是什么?为什么需要WebSocket?WebSocket和Http连接的区别WebSocket的工作原理基本交互过程: Java中的WebSocket支持WebSocket的优势springboot websocket themlef 一个聊天室demopom.xmlWebSocketConfigChatControllerWebController…

__builtin_expect(x,0)

As opposed to the C code, above we can see bar case precedes foo case. Since foo case is unlikely, and instructions of bar are pushed to the pipeline, thrashing the pipeline is unlikely. This is a good exploitation of a modern CPU