Job System и Burst — параллелизм и SIMD
IJob, IJobParallelFor, Burst-компилятор, NativeArray — рабочий путь к высокой производительности.
Главный поток C# в Unity — один. Если в Update() считается путь для тысячи NPC, FPS просядет.
Job System + Burst — официальный путь распараллелить вычисления на все ядра CPU и получить
SIMD-оптимизацию через специальный AOT-компилятор.
Что это
- Job System — пакет (
com.unity.jobs, в Unity 6 — core). API для описания параллельных задач через структуры-IJob/IJobParallelFor. - Burst — пакет (
com.unity.burst). High-performance AOT-компилятор: берёт ваш код, превращает в SIMD-оптимизированный нативный код (через LLVM). Обычно ×5–×100 ускорение. - Unity.Collections —
NativeArray<T>,NativeList<T>и др. — структуры без GC, передаваемые между Job и main thread. - Unity.Mathematics —
float3,quaternion,math.*— типы, на которых Burst оптимально работает (вместо UnityEngine.Vector3/Quaternion).
Web Workers + SharedArrayBuffer + WASM SIMD. Те же 3 элемента: параллелизм + кросс-поточные данные + low-level оптимизация.
Job System — это threads. Burst — это компиляция в SIMD. NativeArray — shared memory без race conditions через safety system.
Базовый IJob
Задача — посчитать result = sum(a × b) для двух больших массивов:
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
[BurstCompile]
public struct DotProductJob : IJob
{
[ReadOnly] public NativeArray<float> A;
[ReadOnly] public NativeArray<float> B;
public NativeArray<float> Result; // [0] — итог
public void Execute() {
float sum = 0f;
for (int i = 0; i < A.Length; i++) {
sum += A[i] * B[i];
}
Result[0] = sum;
}
}
public class JobUser : MonoBehaviour
{
private void Start() {
var a = new NativeArray<float>(10_000_000, Allocator.TempJob);
var b = new NativeArray<float>(10_000_000, Allocator.TempJob);
var result = new NativeArray<float>(1, Allocator.TempJob);
// ... заполнить a, b значениями ...
var job = new DotProductJob { A = a, B = b, Result = result };
JobHandle handle = job.Schedule();
handle.Complete(); // блокируем main thread пока job не закончит
Debug.Log($"Dot product: {result[0]}");
a.Dispose();
b.Dispose();
result.Dispose();
}
}
Что делает Burst:
[BurstCompile]атрибут говорит компилятору “взять этот struct и сгенерировать оптимизированный нативный код”.- Цикл
forразвернётся в SIMD (4 или 8 float’ов за инструкцию). - Без Burst — обычный C# IL, скорость ~10× ниже.
Allocator.TempJob — для данных, живущих до 4 кадров (job обычно завершается раньше).
Allocator.Persistent — если NativeArray переживает множество job’ов (вы вручную освобождаете
через .Dispose()).
Allocator.Temp — для очень коротких аллокаций (1 кадр).
Безопасность: Unity ругается на leak, если забыли .Dispose().
IJobParallelFor — деление работы по ядрам
Если задача — “посчитать что-то для каждого элемента независимо”, используйте IJobParallelFor:
[BurstCompile]
public struct MoveBoidsJob : IJobParallelFor
{
public NativeArray<float3> Positions;
[ReadOnly] public NativeArray<float3> Velocities;
public float DeltaTime;
public void Execute(int i) {
Positions[i] += Velocities[i] * DeltaTime;
}
}
public class BoidsManager : MonoBehaviour
{
private NativeArray<float3> _positions;
private NativeArray<float3> _velocities;
private const int Count = 100_000;
private void Start() {
_positions = new NativeArray<float3>(Count, Allocator.Persistent);
_velocities = new NativeArray<float3>(Count, Allocator.Persistent);
// ... инициализация ...
}
private void Update() {
var job = new MoveBoidsJob {
Positions = _positions,
Velocities = _velocities,
DeltaTime = Time.deltaTime,
};
// innerloopBatchCount = сколько iterations отдать одному worker thread за раз
JobHandle handle = job.Schedule(Count, 1024);
handle.Complete();
}
private void OnDestroy() {
_positions.Dispose();
_velocities.Dispose();
}
}
100 тысяч boids обновляются за ~0.5 мс на 8-ядерном CPU с Burst — против ~50 мс наивного C#. Это уже не “оптимизация”, а другой класс производительности.
Цепочки job’ов
Job-ы можно стыковать через JobHandle:
var firstJob = new ComputeForcesJob { /* ... */ };
JobHandle firstHandle = firstJob.Schedule(count, 64);
var secondJob = new IntegrateVelocityJob { /* ... */ };
JobHandle secondHandle = secondJob.Schedule(count, 64, firstHandle);
// secondJob дождётся firstJob перед стартом
secondHandle.Complete(); // блокируем main thread в конце
Это и есть основа DOTS-парадигмы: множество мелких job’ов с зависимостями, диспетчер сам распараллеливает на пул worker threads.
Safety system и common ошибки
Unity Job System имеет встроенный safety system (в Editor, не в release). Он ловит:
- Race condition — две job’a пишут в один NativeArray одновременно.
- Read-after-write без зависимости — job читает массив, в который параллельно пишет другой job.
- Disposed array — попытка использовать NativeArray после
.Dispose().
Маркируйте [ReadOnly] поля, которые job только читает — это даёт компилятору больше свободы для
параллелизма.
Burst не может вызывать большинство UnityEngine API — Transform.position, GameObject.Find,
Debug.Log. Только pure-функции из Unity.Mathematics, Unity.Collections, чистый C#. Это
цена за оптимизацию: вы переносите логику в “чистый” вычислительный мир, потом скармливаете
результаты обратно в Transform-ы на main thread.
Когда стоит использовать
- Раzе вычисления: pathfinding для 100+ агентов, симуляция флока, расчёт LOD, batch processing ассетов.
- Custom mesh generation: процедурные landscape, marching cubes, voxel terrain.
- AI batch decision-making: ECS-стиль, где у вас 1000 NPC, и каждый “что делать” считается отдельно.
Когда НЕ нужно
- Mало вычислений (10–100 итераций) — overhead Schedule съест выигрыш.
- Логика, тесно завязанная на UnityEngine API — переписывать всё на NativeArray дорого.
- Простой прототип — Burst не нужен, пока FPS норм.
Сравнение с ECS / Entities
ECS (Entities package) — следующий шаг. Там данные изначально хранятся в Native arrays (Chunks), и system-ы автоматически работают через Job + Burst. Если вы пишете simulation-heavy проект (RTS, sandbox, MMO), DOTS даёт structural-advantage над MonoBehaviour-Job комбинацией. Но порог входа выше.