UnityUnityメモ

【実践】Unityでスレッド処理を活用する方法!Job SystemとBurst Compilerで高速化

Unity

1. はじめに

Unityでゲームを作っていると、「もっと処理を速くしたい!」とか「重たい計算をスムーズにしたい!」と思うことがありませんか? 特に、物理演算やAIの処理、大量のオブジェクトを動かすといった計算負荷の高い処理を行う場合、メインスレッドだけに頼っているとパフォーマンスが落ちてしまいます。

そんな時に活躍するのが 「Job System」「Burst Compiler」 です! これらを活用することで、マルチスレッド処理を簡単に導入し、ゲームの動作を大幅に高速化できます。

この記事では、Unityのスレッド処理の基本から、Job SystemとBurst Compilerを組み合わせてパフォーマンスを向上させる方法 までを分かりやすく解説していきます。初心者の方でも理解しやすいように、実際のコード例を交えながら進めるので、ぜひ最後まで読んでみてください!




2. Unityのスレッド処理の基礎

Unityでは、通常 メインスレッド というひとつのスレッド上でほとんどの処理が実行されます。これは、ゲームオブジェクトの更新や物理演算、レンダリングなどを一つの流れで処理する仕組みです。しかし、複雑な計算や大量のオブジェクトを扱う場合、メインスレッドの負荷が高くなり、処理が遅くなる ことがあります。

シングルスレッド処理の問題点

Unityの標準的な動作では、すべての処理がメインスレッド上で直列(順番)に実行される ため、特定の処理が重くなると フレームレートが低下 し、ゲームの動きがカクついたり、遅延が発生したりすることがあります。

例えば、次のような処理を考えてみましょう。

void Update()
{
for (int i = 0; i < 1000000; i++)
{
float result = Mathf.Sqrt(i); // ループ内で大量の計算を実行
}
}

このような計算処理を Update 内で毎フレーム実行すると、メインスレッドが占有され、ゲームの動作が重くなる原因になります。

マルチスレッドの概念

マルチスレッドとは、複数のスレッド(処理の流れ)を並列して実行し、CPUのコアを最大限に活用する技術 です。これにより、処理の負担を分散し、ゲームのパフォーマンスを向上させることができます。

Unityでは、通常の ThreadTask を使ってマルチスレッド処理を行うことも可能ですが、GameObject や Unity API はメインスレッドでのみ動作する という制限があります。そのため、マルチスレッドで処理を分けるには慎重に設計する必要があります。

Unityにおけるマルチスレッドのアプローチ

Unityでは、スレッド処理を簡単に実装できるように 「Job System」 が用意されています。Job Systemは、メインスレッドの負荷を減らしつつ、安全に並列処理を実行する仕組みです。さらに 「Burst Compiler」 を組み合わせることで、ネイティブコードレベルで最適化され、より高速な処理が可能になります。




3. Job Systemとは?

Unityの Job System は、メインスレッドの負荷を軽減し、CPUのコアを活用して並列処理を実行する仕組み です。これにより、計算負荷の高い処理をマルチスレッド化し、ゲームのパフォーマンスを大幅に向上させることができます。

Job Systemの特徴

  1. マルチスレッドを簡単に扱える
    通常の ThreadTask を使うと、スレッドの管理が難しく、データ競合や同期の問題が発生しやすいですが、Job Systemでは安全に並列処理を管理 できます。
  2. C#のstructを活用し、GC(ガベージコレクション)を最小限に
    Job Systemは struct ベースで動作するため、GCの発生を抑えながら処理を並列化 できます。これにより、メモリ管理が最適化され、スムーズな動作が可能になります。
  3. Unity APIの制限
    Job System内では GameObjectやTransform、MonoBehaviourといったUnityのAPIを直接操作できません。そのため、計算処理やデータ操作を並列化し、メインスレッドで結果を反映する という設計が必要になります。

Job Systemの基本的な使い方

では、実際にJob Systemを使って簡単な並列処理を実装してみましょう。

Step 1: 必要な名前空間を追加

まず、Unity.Jobs を使用するために、スクリプトの冒頭に以下を追加します。

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

Step 2: Jobの定義

IJob インターフェースを実装し、並列処理を行うJobを作成します。

[BurstCompile] // Burst Compilerを適用して最適化
struct MyJob : IJob
{
public int number;
public NativeArray<int> result;

public void Execute()
{
result[0] = number * number; // シンプルな計算
}
}

Step 3: Jobの実行

作成した JobStart()Update() 内で実行してみましょう。

public class JobSystemExample : MonoBehaviour
{
private NativeArray<int> result;

void Start()
{
result = new NativeArray<int>(1, Allocator.TempJob);

MyJob job = new MyJob
{
number = 10,
result = result
};

JobHandle handle = job.Schedule(); // Jobをスケジュール
handle.Complete(); // 完了を待つ

Debug.Log("計算結果: " + result[0]); // 100が出力される

result.Dispose(); // メモリ解放
}
}

Step 4: Jobの非同期処理

JobHandle を使えば、メインスレッドをブロックせずに処理を非同期で進めることができます。

JobHandle handle = job.Schedule();
StartCoroutine(WaitForJob(handle));

Job Systemの活用例

Job Systemは、次のようなケースで特に役立ちます。

  • 物理演算の最適化
    • 例: 大量のオブジェクトのコリジョン計算
  • AIの並列処理
    • 例: NPCのパス計算
  • データの一括処理
    • 例: 大量の頂点データの変換

Job Systemを使えば、メインスレッドの負荷を大幅に軽減し、CPUのマルチコアを最大限活用できます。さらに Burst Compiler を併用することで、より効率的なコードを実装可能です。




4. Burst Compilerとは?

Burst Compiler は、UnityのC#コードをネイティブレベルで最適化し、高速化するコンパイラ です。特に、Job Systemと組み合わせることで、パフォーマンスを大幅に向上 させることができます。


Burst Compilerの特徴

C#コードをネイティブ最適化

通常のC#コードは IL(Intermediate Language) にコンパイルされ、.NETのランタイムで実行されます。しかし、Burst Compilerを使用すると、C++のようなネイティブコード にコンパイルされ、CPUに最適化されたバイナリとして実行されます。その結果、処理速度が大幅に向上 します。

SIMD(Single Instruction, Multiple Data)の活用

Burst Compilerは、SIMD命令(AVX、SSEなど)を活用して並列処理を最適化 します。これにより、例えば大量の数値計算を行う場合、1回の命令で複数のデータを処理 できるようになり、通常のC#よりも大幅に高速化できます。

ガベージコレクション(GC)負荷を最小化

Burst Compilerは、C#のクラス(参照型)ではなく、構造体(値型)を推奨 します。これにより、メモリの確保と解放を最小限に抑え、GC(ガベージコレクション)によるパフォーマンス低下を防ぐ ことができます。


Burst Compilerを有効にする方法

Burst Compilerを使うには、Burstパッケージをインストール し、ジョブや関数に [BurstCompile] 属性を追加 するだけです。

1. Burstパッケージをインストール

Unityの Package Manager を開き、Burst パッケージをインストールします。

  1. WindowPackage Manager を開く
  2. Unity Registry から Burst を検索
  3. Install をクリック

2. Burst Compilerを適用

以下のように、[BurstCompile] をジョブに追加するだけで、Burst Compilerによる最適化が適用されます。

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

[BurstCompile] // Burst Compilerを適用
struct MyBurstJob : IJob
{
public NativeArray<int> result;
public int number;

public void Execute()
{
result[0] = number * number; // 計算処理
}
}

public class BurstExample : MonoBehaviour
{
private NativeArray<int> result;

void Start()
{
result = new NativeArray<int>(1, Allocator.TempJob);

MyBurstJob job = new MyBurstJob
{
number = 10,
result = result
};

JobHandle handle = job.Schedule();
handle.Complete(); // ジョブの完了を待つ

Debug.Log("計算結果: " + result[0]); // 100が出力される

result.Dispose(); // メモリ解放
}
}

Burst Compilerの効果を比較

Burst Compilerを使うことで、どれくらいパフォーマンスが向上するのか比較してみましょう。

通常のC#(シングルスレッド)

void Calculate()
{
for (int i = 0; i < 1000000; i++)
{
float result = Mathf.Sqrt(i);
}
}

Burst Compiler + Job System

[BurstCompile]
struct SqrtJob : IJobParallelFor
{
public NativeArray<float> results;

public void Execute(int index)
{
results[index] = Mathf.Sqrt(index);
}
}

このように、計算処理をJob SystemとBurst Compilerで並列化 すると、通常のC#コードよりも 数倍~数十倍の速度向上 が期待できます。


Burst Compilerの注意点

  1. Unity APIを直接使用できない
    • GameObjectTransform などのUnity APIは、メインスレッドでのみ使用可能 なので、ジョブ内では操作できません。
  2. クラスではなく構造体(struct)を使用
    • Burstは struct(値型)を前提に動作するため、クラス(参照型)を使うと最適化が適用されない 可能性があります。
  3. ネイティブ配列(NativeArray)を活用
    • List<T>Array<T> ではなく、NativeArray<T> を使用することで、Burstによる高速化の恩恵を最大限に受ける ことができます。

Burst Compilerを使用すると、C#コードをネイティブレベルで最適化 し、パフォーマンスを大幅に向上できます。特に、Job Systemと組み合わせることで、並列処理と最適化を同時に実現 できるため、計算負荷の高いゲーム開発には必須の技術です。




5. 実践:Job SystemとBurst Compilerを組み合わせて最適化

ここまで、Job Systemの概要Burst Compilerの特徴 を紹介してきました。
このステップでは、Job SystemとBurst Compilerを組み合わせて実際にパフォーマンスを最適化する方法 を学びます!


🎯 目標

大量の計算処理を並列化し、メインスレッドの負担を減らしつつ、高速化する方法 を実践します。

ケース:10万個のオブジェクトの座標を更新

シンプルな例として、10万個のオブジェクトの座標を一括で計算し更新する処理 をJob System + Burst Compilerで最適化します。


1. 通常のC#スクリプトで処理する(非最適化)

まずは、Job Systemを使わずに Update() で直接処理するコードを見てみましょう。

using UnityEngine;

public class NormalUpdate : MonoBehaviour
{
private Vector3[] positions;

void Start()
{
positions = new Vector3[100000];
}

void Update()
{
for (int i = 0; i < positions.Length; i++)
{
positions[i] += new Vector3(0.1f, 0.1f, 0.1f);
}
}
}

🔴 問題点

  • すべての処理が メインスレッドで実行 される
  • Update() 内で10万回のループが回るため、CPU負荷が高く、フレームレートが低下 する

2. Job System + Burst Compilerで最適化

この処理を並列化し、CPUのコアを活用して高速化 してみます。

🔹 改善ポイント

Job System を使用し、メインスレッドの負荷を軽減
Burst Compiler を適用し、ネイティブレベルの最適化を実施
NativeArray を使用してメモリ効率を向上


🔹 最適化したコード

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public class JobSystemOptimization : MonoBehaviour
{
private NativeArray<Vector3> positions;
private JobHandle jobHandle;

void Start()
{
positions = new NativeArray<Vector3>(100000, Allocator.Persistent);
}

void Update()
{
// Jobを作成
PositionUpdateJob job = new PositionUpdateJob
{
positions = positions
};

// 並列処理を実行
jobHandle = job.Schedule(positions.Length, 64);
}

void LateUpdate()
{
// ジョブの完了を待つ
jobHandle.Complete();
}

void OnDestroy()
{
// メモリ解放
if (positions.IsCreated)
positions.Dispose();
}
}

// ✅ Burst最適化 + Job Systemを適用
[BurstCompile]
struct PositionUpdateJob : IJobParallelFor
{
public NativeArray<Vector3> positions;

public void Execute(int index)
{
positions[index] += new Vector3(0.1f, 0.1f, 0.1f);
}
}

3. 最適化の効果

⚡ Before(通常のC#)

🔴 Update() 内で10万回のループがメインスレッドで実行され、CPUの負荷が高い。
🔴 フレームレートが大幅に低下する可能性がある。

⚡ After(Job System + Burst Compiler)

CPUのコアを活用し並列処理を実行負荷を分散
Burst Compilerでネイティブコード化より高速に処理
メインスレッドが空く他の処理(レンダリングなど)がスムーズに動く

結果として、パフォーマンスが大幅に向上し、フレームレートの安定性も改善 されます。


Job SystemとBurst Compilerを組み合わせるとパフォーマンスが向上しますが、どれだけの効果が出ているかを測定することが重要 です。
Advanced FPS CounterProfiler Pro などのアセットを活用すれば、リアルタイムでFPSやCPU負荷を監視でき、最適化の効果をすぐに確認 できます。

4. まとめ

  • Job System を活用すると、メインスレッドの負荷を減らし、並列処理で高速化 できる
  • Burst Compiler を組み合わせると、ネイティブコードレベルで最適化され、さらに処理速度が向上
  • NativeArrayを使用 することで、メモリの無駄を削減 し、最適なデータ管理が可能

この技術を活用して、AIの並列処理や、物理演算の最適化、データ解析の高速化などにも応用 してみましょう!




6. 注意点とベストプラクティス

Unityの Job SystemBurst Compiler は、ゲームのパフォーマンスを大幅に向上させる強力なツールですが、適切に使用しないと 逆にパフォーマンスが低下したり、バグの原因になったり することがあります。
ここでは、よくある注意点とベストプラクティス を紹介します。


1. Job Systemの注意点

❌ UnityのAPI(GameObject, Transform)は使用不可

Job System内では、GameObjectやTransformを直接操作することはできません
これは、Unityのオブジェクトがメインスレッドでのみ動作する仕様になっているためです。

⚠️ NG例: Transformの位置を直接変更しようとする

struct MoveJob : IJob
{
public Transform objectTransform; // ❌ Job内では使用できない

public void Execute()
{
objectTransform.position += Vector3.one; // ❌ エラーになる
}
}

✅ OK例: Transformの変更はメインスレッドで行う

struct MoveJob : IJobParallelFor
{
public NativeArray<Vector3> positions;

public void Execute(int index)
{
positions[index] += Vector3.one; // ✅ Job内ではデータだけ変更する
}
}

// メインスレッドでTransformに適用
void LateUpdate()
{
jobHandle.Complete(); // ✅ Jobの完了を待つ
for (int i = 0; i < objects.Length; i++)
{
objects[i].transform.position = positions[i]; // ✅ メインスレッドで適用
}
}

2. NativeArrayの適切な管理

NativeArray を使うことで、GC(ガベージコレクション)を最小限に抑えられますが、解放を忘れるとメモリリークの原因 になります。

✅ NativeArrayの適切な管理

private NativeArray<Vector3> positions;

void Start()
{
positions = new NativeArray<Vector3>(100000, Allocator.Persistent);
}

void OnDestroy()
{
if (positions.IsCreated)
positions.Dispose(); // ✅ メモリ解放
}

🎯 ベストプラクティス

  • Temp → 短時間の使用(1フレーム以内)
  • TempJob → 一時的な使用(数フレーム)
  • Persistent → 長期間使用(OnDestroyでDisposeする)

3. Jobのスケジューリングを適切に

Job Systemでは、job.Schedule() を実行することでジョブがスケジュールされますが、
スレッドのオーバーヘッドを考慮しないと、逆にパフォーマンスが悪化する ことがあります。

⚠️ NG例: 小さすぎるJobを大量に作成

for (int i = 0; i < 100000; i++)
{
MyJob job = new MyJob { number = i };
job.Schedule(); // ❌ 毎回スケジュールすると負荷が増大
}

✅ OK例: BatchCount を指定して最適化

jobHandle = myJob.Schedule(positions.Length, 64); // ✅ 64単位で処理を並列化

🎯 ベストプラクティス

  • 小さいタスクはまとめて処理する
  • 適切なBatchCountを設定する(64や128など)
  • JobHandle.Complete()を適切なタイミングで呼ぶ



4. Burst Compilerの注意点

❌ クラス(参照型)は使用不可

Burst Compilerは、struct(値型) を前提として動作します。
そのため、クラス(class)を使うと最適化が適用されません

⚠️ NG例: クラスをJobで使用

[BurstCompile]
struct MyBurstJob : IJob
{
public MyClass data; // ❌ クラスは使えない

public void Execute()
{
data.value *= 2; // ❌ 最適化されない
}
}

✅ OK例: structを使用

[BurstCompile]
struct MyBurstJob : IJob
{
public int value;

public void Execute()
{
value *= 2; // ✅ structなら最適化される
}
}

5. Job SystemとBurst Compilerを組み合わせるベストプラクティス

GameObjectの操作はメインスレッドで行う

  • Job Systemでは データのみを処理し、結果をメインスレッドに渡す
  • Transformの変更などは LateUpdate() で適用する

NativeArrayを活用し、適切にDisposeする

  • List<T> ではなく、NativeArray<T> を使用
  • Persistent を使用する場合は、OnDestroyで必ずDisposeする

適切なスケジューリングをする

  • job.Schedule(配列サイズ, BatchCount) を使い、スレッドのオーバーヘッドを抑える
  • 小さなJobを大量に作ると逆効果になるので注意

Burst Compilerを最大限活用

  • [BurstCompile] をつける
  • クラスではなく構造体(struct)を使用
  • ポインタやメモリアクセスを最小限に

🔍 まとめ

⚠️ 注意点解決策(ベストプラクティス)
Job内で GameObject を直接操作できないデータを処理し、メインスレッドで適用
NativeArrayのメモリリークOnDestroyでDisposeを忘れずに
Jobのスケジュールが多すぎると逆効果BatchCountを適切に設定(64や128)
Burst Compilerでクラスが使えないstruct(値型)を使う

Job Systemを使うと、メインスレッドからデータの中身が見えなくなる ため、デバッグが難しくなることがあります。
そこで、Odin Inspector を使えば、NativeArrayの内容をエディター上で確認しながらデバッグできる ので、開発がスムーズになります。




7. まとめ

Unityの Job SystemBurst Compiler を活用すると、ゲームのパフォーマンスを大幅に向上させることができます。特に、計算負荷の高い処理を並列化 し、メインスレッドの負荷を軽減 することで、フレームレートを安定させることが可能です。


🎯 この記事で学んだこと

Job Systemとは?

  • Unityでマルチスレッド処理を簡単に実装できる仕組み
  • メインスレッドの負担を減らし、CPUのマルチコアを活用することで並列処理が可能
  • ただし、GameObjectやTransformを直接操作できない制限がある

Burst Compilerとは?

  • C#コードをネイティブコード化 し、実行速度を大幅に向上
  • SIMD(Single Instruction, Multiple Data) を活用し、並列計算を高速化
  • クラス(class)ではなく、構造体(struct)ベース で使用する必要がある

Job SystemとBurst Compilerの実践

  • 通常のC#コード vs Job System + Burst Compiler のパフォーマンス比較
  • 10万個のオブジェクトの座標計算を並列処理 し、フレームレートの安定化を実証
  • 適切なJobのスケジューリング(BatchCountの設定) でスレッドのオーバーヘッドを防ぐ

注意点とベストプラクティス

  • GameObjectの操作はメインスレッドで行う(Jobではデータのみ処理)
  • NativeArrayは適切にDisposeする(メモリリークを防ぐ)
  • Jobのスケジューリングを適切に設定し、並列処理の効果を最大化
  • Burst Compilerを適用し、最大限のパフォーマンスを引き出す



📌 Job SystemとBurst Compilerを活用すべき場面

💡 大規模な計算処理が必要な場合

  • AIのパス計算
  • 大量のオブジェクトの物理演算
  • シミュレーションや群れ行動(Boids)

💡 リアルタイムなデータ処理

  • マップのプロシージャル生成
  • リアルタイムデータ解析
  • 3Dメッシュの動的変形

💡 CPU負荷を分散してフレームレートを安定させたい場合

  • メインスレッドを開放し、レンダリングや入力処理をスムーズにする
  • 大量のエンティティを処理するゲーム(MMO、シミュレーション)

Job SystemとBurst Compilerを活用すればCPUの負荷は大幅に軽減できますが、さらにメッシュ描画やアニメーションの最適化も組み合わせると、より快適なゲーム開発が可能 です。
Mesh Baker を使ってメッシュを統合すれば、描画負荷を軽減 でき、
DOTween Pro を導入すれば、アニメーションのパフォーマンスを向上 できます。


🚀 次のステップ

小さなプロジェクトで試してみる
→ 小規模なスクリプトをJob SystemとBurst Compilerで最適化し、パフォーマンスの違いを比較する

実際のゲームプロジェクトに導入する
→ 例えば、物理エンジンやAIの計算処理を並列化し、パフォーマンスを測定する

最新のUnity技術をキャッチアップ
→ Unityのアップデートにより、Job SystemやBurst Compilerの機能がさらに拡張される可能性があるので、最新情報をチェックする


Job SystemとBurst Compilerを活用すれば、ゲームの処理速度を最適化し、スムーズな動作を実現 できます。ただし、適切な使い方をしないと逆効果になる可能性もあるため、ベストプラクティスを守りながら実装することが重要 です。

ぜひ、この技術を活用して、より快適なゲーム開発を目指しましょう! 🚀🔥




よくある質問(FAQ)

Q
Job Systemを使うと必ずパフォーマンスが向上しますか?
A

いいえ、場合によります。
Job Systemは、計算負荷の高い処理を並列化することでパフォーマンスを向上させる 仕組みですが、小規模な処理をJobにすると逆にオーバーヘッドが増えて遅くなる ことがあります。

🚀 ベストプラクティス:

  • 大規模なループ処理負荷の高い計算 に適用する
  • Schedule(配列サイズ, BatchCount) を適切に設定する
  • 小さなJobを大量に作らない(まとめて処理する)
Q
Job SystemとCoroutine(コルーチン)はどう違いますか?
A

Job Systemは並列処理、Coroutineは非同期処理です。

機能Job SystemCoroutine
処理の種類並列処理(マルチスレッド)非同期処理(メインスレッド)
メインスレッドの負荷軽減する(バックグラウンドで動作)軽減しない(メインスレッド内で処理)
使いどころ大量の計算処理一定時間待機やアニメーション管理
Unity API使用❌ 使用不可✅ 使用可能

🚀 ベストプラクティス:

  • 物理演算やAIの計算処理 → Job System
  • タイマー処理や遅延実行 → Coroutine
Q
Burst Compilerの最適化が正しく適用されているか確認するには?
A

Burst Inspectorを使うと確認できます!

  1. Jobs > Enable Burst Compilation を有効にする
  2. Jobs > Enable Burst Safety Checks を無効にする(デバッグ時)
  3. Burst Inspector (Window > Analysis > Burst Inspector) を開く
  4. コンパイルされたネイティブコードの最適化結果を確認

🚀 ベストプラクティス:

  • [BurstCompile] をつけたジョブの処理時間を測定し、最適化されているか確認
  • Burst Safety Checks を無効にしてリリースビルドを最適化
タイトルとURLをコピーしました