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()