ECS优化缓存行--
ECS和传统游戏对象--
原型Archtype--
栈帧--
组件与共享组件--
ECS实例--
汇编指令--
SIMD--
Burst编译器--
Unity.mathematics数学库--
Job多线程计算--
系统生命周期--
ECS优化缓存行: 怎样提高缓冲区命中率,每个对象只加载xy坐标和旋转一共12b,那么一个缓存行能存五个对象,浪费64-21*5 = 4b
ECS和传统游戏对象: ECS:Entity(实体)、Component(组件)、System(系统)
传统:
1 2 3 4 5 6 7 8 public class Game : MonoBehaviour { public int x; private void Update() { x++; } }
ECS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public struct GameComponentData : IComponentData // 组件 //结构体只保存数据,不能写逻辑 { public int x; } public class MyGameSystem : ComponentSystem //在System里找关心的组件 { protected override void OnUpdate() { this.Entities.ForEach((ref GameComponentData data) => { data.x++; }); } }
原型Archtype: 即使不同的实体Entity,只要组件相同都会保存在原型ArcheType。
ArcheType是16KB的数组容器。
栈帧: 栈上会保留值类型数据和指向堆的指针。
组件与共享组件: 值类型组件和共享类型组件。
System可以找到它关心的组件去遍历。
Component System在Main Thread,Job Component System(JCS)可以在多线程。 Main Thread–JCS >>> Worker Thread–Job, Job, Job……
JobSystem应用: ECS实例: ArcheType Chunk容量16KB,包含Trees #1, Trees #2, Rocks #1, Big Enemies #1, Small Enemies #1, Small Enemies #2, Query出符合条件的实体组件,大敌人、小敌人统一Update(相比Rocks,Trees更感兴趣)
汇编指令: mov指令:内存中数据传到寄存器/寄存器数据传到另一寄存器
mov ax 8 数据->寄存器
mov ax bx 寄存器->寄存器
SIMD: 没听懂用来干嘛
SIMD指令优化总结:
避免代码出现分支预测(会打断SIMD的向量化指令),使用math.select和math.lerp代替分支预测
使用float4 bool4等代替float bool
使用m128自己组织128位数据
编译后尽量使用v开头指令,结尾尽量是ps指令而不是ss指令
Burst编译器: Burst只支持值类型数据的编译,不支持引用类型数据编译(因为C#的GC做的不好)。
Burst编译器是以LLVM为基础的后端编译技术。
怎么启动Burst编译器?在Job上面加上[BurstCompile],如果在Job外怎么工作呢?使用有一个限制是需要静态方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [BurstCompile] public class MyClass { [BurstCompile] public static float add(float a, float b) { return a * b; } [BurstCompile] { public static unsafe void dot(float3* a, float3* b, float* c) { *c = math.dot(*a, *b); } } }
Unity.mathematics数学库: 提供矢量类型(float4 float3…)可直接映射到硬件SIMD寄存器
Math类也提供了直接映射到硬件SIMD寄存器
原本CPU一个一个计算的有了SIMD可以一次性计算
Job多线程计算: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [BurstCompile] public struct MyJob1 : IJob { [ReadOnly] public int left; [ReadOnly] public int right; [WriteOnly] public NativeArray<int> @out; public Execute() { @out[0] = left * right; } } private void Start() { MyJob1 myJob = new MyJob1(); myJob.left = 2; myJob.right = 3; myJob.@out = new NativeArray<int>(1, Allocator.TempJob); myJob.Schedule().Complete(); //在一个子线程中计算并且等待完成 Debug.Log(myJob.@out[0]); // log 6; myJob.@out.Dispose(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // IJobFor [BurstCompile] public struct MyJob2 : IJobFor { public NativeArray<int> left; [ReadOnly] public NativeArra<int> right; public void Execute(int index) { left[index] = left[index] * right[index]; //输出线程ID和当前执行的索引 Debug.Log(System.Threading.Thread.CurrentThread.ManagedThreadId + "," + index); } } private void start() { MyJob2 myJob = new MyJob2(); myJob.left = new NativeArray<int>(100, Allocator.TempJob); myJob.right = new NativeArray<int>(100, Allocator.TempJob); myJob.Schedule(myJob.left.Length, new JobHandle()).Complete(); //实际上是在一个子线程里面开了个for循环,Schedule是在一个子线程中执行,可以保证顺序 myJob.left.Dispose(); myJob.right.Dispose(); }
Schedule和ScheduleParallel对比:
1 2 //ScheduleParallel可以在多个子线程中并行运行,不保证顺序 myJob.ScheduleParallel(myJob.left.Length, 64, new JobHandle()).Complete();
IJobFor和IJobParallelFor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [BurstCompile] public struct MyJob2 : IJobParallelFor { public NativeArray<int> left; [ReadOnly] public NativeArra<int> right; public void Execute(int index) { left[index] = left[index] * right[index]; //输出线程ID和当前执行的索引 Debug.Log(System.Threading.Thread.CurrentThread.ManagedThreadId + "," + index); } } private void start() { MyJob2 myJob = new MyJob2(); myJob.left = new NativeArray<int>(100, Allocator.TempJob); myJob.right = new NativeArray<int>(100, Allocator.TempJob); //因为接口是IJobParallelFor,这里的Schedule就完全是并行执行不保证顺序了(注意参数有些不同,多了个“64”) myJob.Schedule(myJob.left.Length, 64, new JobHandle()).Complete(); myJob.left.Dispose(); myJob.right.Dispose(); }
Complete是实现在主线程等待执行的结果
myJob.Schedule和myJob.Run对比:Schedule是在多核子线程中并行计算,Run是完全在主线程执行
Job的处理依赖关系:
我有Job1和Job2,怎么并行执行快一些?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 MyJob2 myJob1 = new MyJob2(); MyJob3 myJob2 = new MyJob3(); //同时并行执行 myJob1.Schedule(100, 64); myJob2.Schedule(100, 64); //Job1执行完毕后再并行执行Job2 //缺点是要在主线程等待Job1结束(因为用了Complete()) myJob1.Schedule(100, 64).Complete(); myJob2.Schedule(100, 64); //设置Job2依赖Job1,这样不需要在主线程等待 //JobHandle和依赖项:调用Schedule方法时会返回JobHandle,可以用Job1的JobHandle作为Job2的依赖项 JobHandle jobHandle = new JobHandle(); JobHandle scheduleJobDependencyJob = myJob1.Schedule(100, 64, jobHandle); myJob2.Schedule(100, 64, scheduleJobDependencyJob).Complete();
设计模式-组合模式: 系统生命周期: OnCreate(), OnStartRunning(), OnUpdate(), OnStopRunning(), OnDestory()