Skip to content

Instantly share code, notes, and snippets.

@valyard
Created July 10, 2017 20:20
Show Gist options
  • Save valyard/e96060ee8e5a9d52d43901c7c6ad4288 to your computer and use it in GitHub Desktop.
Save valyard/e96060ee8e5a9d52d43901c7c6ad4288 to your computer and use it in GitHub Desktop.
"Slides" about C# Job System and ESC in Unity.
// ######################################################################
//
// We want you to write more efficient code
//
// ######################################################################
But, we teach the opposite...
You know, GameObjects and Components.
...
Otherwise, it feels not Unity way.
// ######################################################################
//
// To fully utilize CPU and all its cores you need:
//
// ######################################################################
1. Control over data and data layout.
2. Multi-threaded code which can execute in parallel.
3. Generate more efficient code.
// Answers:
1. Data-oriented design - we want to teach you how to structure your code properly.
2. C# Job System — safe and efficient way to write multi-threaded code.
3. Jobs compiler technology.
But... You will need to change the way you think about code.
// ######################################################################
//
// [DEMO]
//
// ######################################################################
// ######################################################################
//
// Data-oriented design
//
// ######################################################################
CPUs like their data sequential:
/[data] [data] [data] [data] [data] [data] [data] [data]
This layout will result in many cache misses:
/[data] ...... ...... ...... ...... [data] ...... ......
/...... ...... [data] ...... [data] ...... ...... ......
...... [data] ...... ...... [data] [data] ...... [data]
// ######################################################################
//
// Old school object-oriented code
// A MonoBehaviour on every GameObject.
// Update() is called on every one of them.
//
// ######################################################################
public class Rotator : MonoBehaviour
{
public float speed;
private void Update()
{
transform.rotation = transform.rotation *
Quaternion.AngleAxis(speed * Time.deltaTime, Vector3.up);
}
}
// ######################################################################
//
// Will have huge overhead when the number of GameObjects grows.
// http://blogs.unity3d.com/2015/12/23/1k-update-calls/
//
// ######################################################################
// ######################################################################
//
// Manager for all Rotators.
//
// ######################################################################
public class Rotator : MonoBehaviour
{
private RotatorManager m_Manager;
[SerializeField]
private float m_Speed;
private int m_Index = -1;
public float speed
{
get { return m_Speed; }
set
{
m_Speed = value;
if (m_Index != -1)
m_Manager.SetSpeed(m_Index, value);
}
}
private void OnEnable()
{
m_Manager = RotatorManager.Instance;
m_Index = m_Manager.Add(transform, m_Speed);
}
private void OnDisable()
{
m_Manager.Remove(this);
}
}
class RotatorManager : MonoBehaviour
{
List<Transform> m_Transforms;
List<float> m_Speeds;
public int Add(Transform transform, float speed)
{
m_Speeds.Add(speed);
m_Transforms.Add(transform);
return m_Transforms.Count - 1;
}
public void SetSpeed(int index, float speed)
{
m_Speeds[index] = speed;
}
...
private void Update()
{
float deltaTime = Time.deltaTime;
for (int i = 0; i != m_Transforms.Count; i++)
{
var transform = m_Transforms[i];
transform.rotation = transform.rotation *
Quaternion.AngleAxis (m_Speeds[i] * deltaTime, Vector3.up);
}
}
}
// ######################################################################
//
// Entity-Component-System code
//
// ######################################################################
// Data is in Components
// Logic is in Systems
public class RotationSpeedComponent : MonoBehaviour
{
public float speed;
}
public class RotatingSystem : ComponentSystem
{
// NOTE: InjectTuples scans all [InjectTuples] in the class
// and returns the union of objects that have both Transform and LightRotator
[InjectTuples]
public ComponentArray<Transform> m_Transforms;
[InjectTuples]
public ComponentArray<RotationSpeedComponent> m_Rotators;
override protected void OnUpdate()
{
base.OnUpdate ();
float dt = Time.deltaTime;
for (int i = 0; i != m_Transforms.Length;i++)
{
m_Transforms[i].rotation = m_Transforms[i].rotation *
Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up);
}
}
}
// ######################################################################
//
// Lightweight components
//
// ######################################################################
[Serializable]
public struct RotationSpeed : IComponentData
{
public float speed;
public RotationSpeed (float speed) { this.speed = speed; }
}
[UnityEngine.ExecuteInEditMode]
public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }
public class RotatingSystem : ComponentSystem
{
// NOTE: InjectTuples scans all [InjectTuples] in the class
// and returns the union of objects that have both Transform and LightRotator
[InjectTuples]
public ComponentArray<Transform> m_Transforms;
[InjectTuples]
public ComponentDataArray<RotationSpeed> m_Rotators;
override protected void OnUpdate()
{
base.OnUpdate ();
float dt = Time.deltaTime;
for (int i = 0; i != m_Transforms.Length;i++)
{
m_Transforms[i].rotation = m_Transforms[i].rotation *
Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up);
}
}
}
// ######################################################################
//
// Lightweight jobified components
//
// ######################################################################
public class SystemRotator : ComponentSystem
{
[InjectTuples]
public TransformAccessArray m_Transforms;
[InjectTuples]
public ComponentDataArray<RotationSpeed> m_Rotators;
protected override void OnUpdate()
{
base.OnUpdate ();
var job = new Job();
job.dt = Time.deltaTime;
job.rotators = m_Rotators;
AddDependency(job.Schedule(m_Transforms, GetDependency()));
}
struct Job : IJobParallelForTransform
{
public float dt;
[ReadOnly]
public ComponentDataArray<RotationSpeed> rotators;
public void Execute(int i, TransformAccess transform)
{
transform.rotation = transform.rotation *
Quaternion.AngleAxis(dt * rotators[i].speed, Vector3.up);
}
}
}
// ######################################################################
//
// What is a Job?
//
// ######################################################################
// Always a struct
struct CopyFloatsJob : IJob
{
// All the data must be declared here
// All data must be blittable
[ReadOnly] // Declare how we are going to use this data
public NativeArray<float> src;
public NativeArray<float> dst;
public void Execute()
{
for (int i = 0; i < src.Length; i++)
dst[i] = src[i];
}
}
// Fill job data
var job = new CopyFloatsJob()
{
src = new NativeArray<float>(500, Allocator.Temp),
dst = new NativeArray<float>(500, Allocator.Temp)
}
// Run the job
var jobHandle = job.Schedule();
// Need results ready immediately!
jobHandle.Complete();
// ######################################################################
//
// Native containers
//
// ######################################################################
// It's all about taking care of your memory.
NativeArray<T>
NativeList<T>
NativeSlice<T>
NativeHashMap<TKey, TValue>
NativeMultiHashMap<TKey, TValue>
var array = new NativeArray<int>(100, Allocator.Temp);
array[0] = 42;
array.Dispose();
// ######################################################################
//
// For loop on different threads
//
// ######################################################################
struct CopyFloatsJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float> src;
public NativeArray<float> dst;
public void Execute(int index)
{
dst[index] = src[index];
}
}
var job = new CopyFloatsJob()
{
src = new NativeArray<float>(500, Allocator.Temp),
dst = new NativeArray<float>(500, Allocator.Temp)
}
// (number of items to iterate, batch size)
var jobHandle = job.Schedule(500, 100);
jobHandle.Complete();
// IJobParallelFor for Transforms
struct Job : IJobParallelForTransform
{
public float dt;
[ReadOnly]
public ComponentDataArray<RotationSpeed> rotators;
public void Execute(int i, TransformAccess transform)
{
transform.rotation = transform.rotation *
Quaternion.AngleAxis(dt * rotators[i].speed, Vector3.up);
}
}
// ######################################################################
//
// Dependencies
//
// ######################################################################
var source = new NativeArray<float>(500, Allocator.Temp);
var tmp = new NativeArray<float>(500, Allocator.Temp);
// The first job
var job1 = new CopyFloatsJob() { src = source, dst = tmp };
var job1Handle = job1.Schedule(src.Length, 100);
var destination = new NativeArray<float>(500, Allocator.Temp);
// The second job
var job2 = new CopyFloatsJob() { src = tmp, dst = destination };
var job2Handle = job2.Schedule(src.Length, 100, job1Handle);
job2Handle.Complete();
// ######################################################################
//
// Safety
//
// ######################################################################
We want Unity to be a Sandbox.
If you make a mistake we want to tell you what you did wrong.
C# job system will give you an time error if you:
* Incorrectly set up dependencies,
* Access the same collection in parallel jobs,
* Deallocate a collection while it is being used,
* Forget to deallocate a collection after all jobs complete,
* And more...
We want it to be impossible to shoot yourself in the foot with multi-threading.
// ######################################################################
//
// Make it run faster!!!
//
// ######################################################################
C# job compiler.
Subset of .NET IL => highly optimized native code through LLVM.
* No virtual functions
* No reference types
* No GC
[ComputeJobOptimization]
struct MyJob : IJob {}
// Don't care about precision
[ComputeJobOptimization(Accuracy.Med, Support.Relaxed)]
struct MyJob : IJob {}
New math library modelled after HLSL.
float, float2, float3, float4
int, int2, int3, int4
math.min, math.lerp, math.saturate, math.rsqrt
C# job compiler uses it to generate SIMD code.
// ######################################################################
//
// When?
//
// ######################################################################
Stages:
1. C# Job System -> 2017.3
2. Component System -> just a project
3. Math Library -> just a project
4. C# Job Сompiler -> 2018.x
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment