Technology DOTS - chaolunner/CloudNotes GitHub Wiki

DATA-ORIENTED-TECH-STACK(DOTS)

多线程式数据导向型技术堆栈

HPC# 高性能C#

  • .Net Core比C++慢2倍

  • Mono比.Net Core慢3倍

  • IL2CPP比Mono快2-3倍,IL2CPP与.Net Core效率相当,但依然比C++慢2倍

  • Unity使用Burst编译后可以让C#代码运行的效率比C++更快

  • C# class类型数据的内存分配在堆上,无法主动释放,必须等到.Net垃圾回收才可真正清理

  • IL2CPP虽然将IL转成C++代码,实际还是模拟了.Net的垃圾回收机制,所以效率并非等同于C++

  • HPC# 就是NativeArray可代替数组T[] 数据类型包括(float,int,uint,short,bool...),enums,structs和其他类型的指针

  • NativeArray可以在C#层分配C++中的对象,可以主动释放不需要进行C#的垃圾回收

  • Job System中使用的就是NativeArray

    void Update()
    {
      Profiler.BeginSample("NativeArray");
      NativeArray<Vector3> velocity1 = new NativeArray<Vector3>(1000, Allocator.Persistent);
      for(int i = 0; i < velocity1.Length; i++)
      {
        velocity1[i] = Vector3.zero;
      }
      Profiler.EndSample();
      velocity1.Dispose();
    
      Profiler.BeginSample("Vector3[]");
      Vector3[] velocity2 = new Vector3[1000];
      for(int i = 0; i < velocity2.Length; i++)
      {
        velocity2[i] = Vector3.zero;
      }
      Profiler.EndSample();
    }
    

IJobParallelFor

  • IJob是一个一个开线程任务,因为数据是顺序执行的所以它可以保证正确性
  • 如果想让线程任务真正的并行,那么可以用IJobParallelFor
  • 一旦线程任务并行的话,就意味着数据执行的顺序不是线性的,每一个Job里的数据不能完全依赖上一个Job执行后的结果
  • [ReadOnly]声明数据是否只读,如果数据是只读的,意味着这个数据不需要加锁
  • 如果不声明默认数据是Read/Write的,数据一旦需要改写,那么Job就一定要等它
  • 然而这一切Unity都已经帮我们做好,我们不需要自己做加解锁逻辑

Burst编译器的原理

  • Burst编译器是以LLVM为基础的后端编译技术
  • 编译器的原理分为5个步骤:源代码->前端->优化器->后端->机器码
  • LLVM定义了个抽象语言IR,前端负责将源码(C#)编译成IR,优化器负责优化IR,后端负责将IR生成目标语言这里就是机器码
  • 正是因为抽象语言IR的存在,所以LLVM支持的语言很多,而且也方便扩展C#,ActionScript,Ads,D语言,Fortran,GLSL,Haskell,Java字节码,Objective-C,Swift,Python,Ruby,Rust,Scale等语言
  • LLVM代码是开源的,所以Unity很适合用它来做Burst的编译
  • 遗憾的是LLVM对C#的GC做的并不好,所以Burst只支持值类型数据编译,不支持引用类型数据编译

Unity.Mathematics数学库

  • Unity.Mathematics提供矢量类型(float4,float3...),它可以直接映射到硬件SIMD寄存器

  • Unity.Mathematics的Math类中也提供了直接映射到硬件SIMD寄存器

  • 这样原本CPU需要一个个计算的,有了SIMD可以一次性计算完毕

  • 需要注意的是Unity之前的Math类默认是不支持映射SIMD寄存器的

ECS 和 CPU Cache

CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,虽然一级缓存是与CPU同频运行的,但是由于容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,如果所需要的数据在二级缓存中也没有的话,会继续转向三级缓存(L3 Cache),如果还是没有则会从内存(主存)拿取一段连续的数据到缓存中来。

Unity ECS中,并不只是对Entity进行分组,而是连着Entity对应的Component一起进行分组,称作Archetype。每一种Component都存放在连续内存里,而这些Component对应的内存又被分割成固定长度被打包在一块固定大小的内存里,称作Chunk,满足一类Archetype的多个Chunk又被一个LinkedList连接起来存于满足条件的Archetype。

而且ECS下的Component里面只包含所需要用到的数据,不需要继承自MonoBehaviour,内存大为节省。同时由Component类型组合而成的Archetype,会在内存中高度紧密的排列。这就使得CPU的Cache命中率非常的高,效率大幅提升。

Reference

⚠️ **GitHub.com Fallback** ⚠️