~2 мин чтения

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.CollectionsNativeArray<T>, NativeList<T> и др. — структуры без GC, передаваемые между Job и main thread.
  • Unity.Mathematicsfloat3, quaternion, math.* — типы, на которых Burst оптимально работает (вместо UnityEngine.Vector3/Quaternion).
Веб

Web Workers + SharedArrayBuffer + WASM SIMD. Те же 3 элемента: параллелизм + кросс-поточные данные + low-level оптимизация.

Unity

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:

  1. [BurstCompile] атрибут говорит компилятору “взять этот struct и сгенерировать оптимизированный нативный код”.
  2. Цикл for развернётся в SIMD (4 или 8 float’ов за инструкцию).
  3. Без Burst — обычный C# IL, скорость ~10× ниже.
Allocator стратегии

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

Burst не может вызывать большинство UnityEngine APITransform.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 комбинацией. Но порог входа выше.