Burst Compiler e Job in Unity

Quando si sviluppa un videogioco, è importante ottimizzare al massimo le performance, in particolare tutte quelle meccaniche di gioco che sono computazionalmente intensive. Immagina di dover sviluppare un gioco 3D dove ci sono migliaia di nemici, detti boids, che continuamente devono aggiornare la propria posizione e velocità, basandosi sia sulla posizione in quell’istante del player, ma anche rispetto agli altri boids per evitare le collisioni.
Figura 1: esempio di agenti (boids) che si muovono liberamente nel loro spazio, evitandosi l’uno con l’altro, mentre seguono lo stormo.
Ecco, un ottima strategia è quella di unire Burst Compiler, che consente di migliorare significativamente le performance del codice, e Job di Unity per sfruttare il multi-threading1.
Figura 2: esempio di multithreading
Cos’è Burst Compiler?
Burst Compiler è un compilatore Just-In-Time (JIT), ovvero compila il codice durante l’esecuzione del programma, anziché prima dell’esecuzione (come avviene nella compilazione tradizionale). Questa tecnica consente di adattare il codice alle specifiche condizioni di runtime, migliorando l’efficienza e le prestazioni complessive dell’applicazione.
Come funziona?
Burst funziona in combinazione con il sistema di Job di Unity, che permette di eseguire operazioni in parallelo sfruttando il multi-threading. Quando si scrive un Job in Unity, Burst può essere utilizzato per compilarlo, ottenendo così performance significativamente migliori.
Esempio di codice
Assicurati di avere installato Burst Compiler e Job dal Package Manager di Unity.
Inizializzazione degli Array:
- creiamo un array nativo di interi con una serie di valori. L’allocazione TempJob indica che l’array sarà utilizzato temporaneamente per un job e sarà automaticamente deallocato una volta completato. Infine, creiamo un altro array nativo per memorizzare le somme parziali. La lunghezza di questo array è la stessa di numbers.
NativeArray<int> numbers = new NativeArray<int>(new int[] { 1, 2, 3, 4, 5 }, Allocator.TempJob);
NativeArray<int> partialSums = new NativeArray<int>(numbers.Length, Allocator.TempJob);
Definiamo il Job:
- la struttura SumJob implementa l’interfaccia IJobParallelFor, che richiede l’implementazione del metodo Execute. Questo metodo è chiamato in parallelo per ogni indice dell’array. Ogni chiamata al metodo Execute copia il valore corrispondente da numbers a partialSums.
[BurstCompile]
struct SumJob : IJobParallelFor
{
[ReadOnly] public NativeArray<int> numbers;
public NativeArray<int> partialSums;
public void Execute(int index)
{
partialSums[index] = numbers[index];
}
}
- Schedulazione ed Esecuzione del Job: Creiamo un’istanza di SumJob e inizializziamo i suoi campi. Scheduliamo il job per eseguire in parallelo specificando il numero di iterazioni e il batch size. E attendiamo la fine dell’operazione con Complete.
SumJob sumJob = new SumJob
{
numbers = numbers,
partialSums = partialSums
};
JobHandle handle = sumJob.Schedule(numbers.Length, 1);
handle.Complete();
Calcolo del Risultato e Pulizia:
al completamento del Job, sommiamo i valori di partialSums per ottenere la somma totale; infine liberiamo la memoria utilizzata per allocare le risorse;
utilizzare IJobParallelFor consente di sfruttare il multi-threading, migliorando ulteriormente le performance per operazioni che possono essere parallelizzate. In questo esempio, ogni elemento dell’array viene processato indipendentemente, permettendo un’esecuzione parallela efficiente.
int totalSum = 0;
for (int i = 0; i < partialSums.Length; i++)
{
totalSum += partialSums[i];
}
Debug.Log("Sum: " + totalSum);
numbers.Dispose();
partialSums.Dispose();
Risultato finale:
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
public class BurstExample : MonoBehaviour
{
[BurstCompile]
struct SumJob : IJobParallelFor
{
[ReadOnly] public NativeArray<int> numbers;
public NativeArray<int> partialSums;
public void Execute(int index)
{
partialSums[index] = numbers[index];
}
}
void Start()
{
int length = 5;
NativeArray<int> numbers = new NativeArray<int>(new int[] { 1, 2, 3, 4, 5 }, Allocator.TempJob);
NativeArray<int> partialSums = new NativeArray<int>(length, Allocator.TempJob);
SumJob sumJob = new SumJob
{
numbers = numbers,
partialSums = partialSums
};
JobHandle handle = sumJob.Schedule(numbers.Length, 1);
handle.Complete();
int totalSum = 0;
for (int i = 0; i < partialSums.Length; i++)
{
totalSum += partialSums[i];
}
Debug.Log("Sum: " + totalSum);
numbers.Dispose();
partialSums.Dispose();
}
}
In conclusione:
l’utilizzo di Burst Compiler in combinazione con il sistema di job di Unity può portare a notevoli miglioramenti nelle performance del vostro gioco, specialmente quando numerose operazioni richiedono un alto consumo di risorse computazionali. Con una semplice configurazione e poche modifiche al codice, è possibile ottenere benefici significativi in termini di velocità di esecuzione e efficienza. C’è da dire che però Burst funziona solo con valori nativi e non è possibile chiamare metodi da altre classi. Per qualsiasi dubbio o curiosità rimandiamo alla documentazione di Unity.
Bibliografia:
https://docs.unity3d.com/Packages/com.unity.burst@0.2/manual/index.html
https://docs.unity3d.com/Manual/JobSystemOverview.html
https://www.undefinedgames.org/2019/10/22/unity-c-job-system-and-burst-compiler-dots-introduction/
https://itk.org/Doxygen50/html/ThreadingPage.html
https://github.com/topics/boids-algorithm
è una tecnica di programmazione che permette a un programma di fare più cose allo stesso tempo, suddividendo le attività in thread che possono essere eseguiti simultaneamente. ↩︎