【CEDEC2017】C#JobSystem を使った Unity流マルチスレッドプログラミング

3.9K Views

September 04, 17

スライド概要

017/8/30~9/1に開催されたCEDEC2017の講演スライドです。
講師:伊藤 周 (ユニティ・テクノロジーズ・ジャパン合同会社)

profile-image

リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

C#JobSystem を使った Unity流マルチスレッドプログラミング ユニティ・テクノロジーズ・ジャパン合同会社 エバンジェリスト 伊藤 周

2.

諸注意 • • • • 今回紹介するC# Job Systemはまだ発展段階 リリースでは多少の差異が出る可能性がある C# Job Systemの概念を知ってほしい プログラマ以外は理解不能

3.

アジェンダ • 従来のマルチスレッドプログラミング • C# Job System の概要 • Let’s read codes. (コードを読む) • Let’s make a mistake. (間違ってみる) • Let’s try “C# Job Compiler” (コンパイラを体験) • Let’s implement. (実装してみる) • まとめ

4.

従来のマルチスレッドプログラミングの話

5.

マルチスレッド プログラミングは 好きですか?

6.

私は嫌いです

7.

MTPのここが嫌だ その1 • レースコンディション対策が嫌だ Aが使うよ • 面倒臭い write A • コードが汚い、可読性が低い • 間違っても気づきにくい ? B write Aが使うよ Bが使うよ read A

8.

MTPのここが嫌だ その2 • デッドロックが嫌だ • 面倒臭い A B • コードが汚い、可読性が低い • 間違うと無限ループ 待ち B 待ち A

9.

MTPのここが嫌だ その3 • 難解なところが嫌だ • mutex:lockとか • アトミック変数とか

10.

MTPのここが嫌だ その4 • デバッグが嫌だ • 正常に”動いてしまったり”する • 無限ループになったりする • 突然ハングアップしたりする • リリース後にバグが判明したりする

11.

私には無理だ!

12.

そんなあなたに C# Job System

13.

C# Job System の概要

14.

43 伊藤周の年齢

15.

116

18.

116倍 Boid シミュレーションを マルチスレッドで 8 core CPUで 動かした場合の速度倍

19.

Demo

20.

C# Job System の特徴 • • • • 簡潔に書ける GCフリー 安全 高速な新コンパイラ

21.

特徴1 簡潔に書ける • Data Oriented Programming • データとビヘイビア(振舞い)の分離 • struct(構造体)コンポーネントの導入 • Job Component System の用意 • 簡潔に書けるようにマネージャーを用意

22.

特徴2 GCフリー • GCをいかにさせないか • NativeArrayの導入 • 以下の感じで確保 要素数 ↓ アロケーターの種類 ↓ var src = new NativeArray<float>(500, Allocator.Temp); • 以下の感じで解放(自分で) src.Dispose();

23.

特徴2 GCフリー • 他のNativeArrayファミリー struct NativeArray<Value> // 配列 struct NativeList<Value> // リスト。追加削除が容易 struct NativeSlice<Value> // 一部を切り取れる struct NativeHashmap<Key, Value> // Dictionary「 struct NativeMultiHashmap<Key, Value> //複数Dictionary

24.

特徴3 安全 • • • • エラーで指摘してくれる 落ちることはない レースコンディション、デッドロックは起こり得ない “Sandbox”

25.

特徴4 高速な新コンパイラ • C#→[Mono]→IL→[C# Job Compiler]→内部的な Domain Model →[最適化]→[LLVM]→実行形式 • 10倍〜20倍高速になる • 電池消費の軽減 • Why faster? • SIMD命令の有効利用 • 正確さとパフォーマンスのトレードオフ

26.

Let’s read C# Job System codes!

27.

コーディング基本まとめ • • • • • IJob~でジョブを定義 Execute にジョブの中身を書く Schedule でジョブを開始 Complete でジョブ終了確認 変数はNativeArray系を使い、自力でDispose

28.

コーディング基本まとめ • IJob • 1つのスレッドでジョブを回す public void Execute() {} • IJobParallelFor • 複数のスレッドでジョブを回す public void Execute(int i) {} • IJobParallelForTransform • Transformにアクセスが可能 public void Execute(int i, TransformAccess transform){}

29.

Let’s make a mistake!

30.

エラーまとめ • マルチスレッドプログラミングは間違えやすい • ちょっとした見落としはしてしまう • Unityは落ちることなくエラーが教えてくれる • CTO Joachim「Unityは「Sandbox(=砂場)」である」 • 砂場では間違っていい。正解に導いてくれれれば。

31.

Let’s try “C# Job コンパイラ”

32.

C# Job Compiler • 一文付け足すだけ • [ComputeJobOptimizationAttribute(Accuracy .Med, Support.Relaxed)] • Accuracy は計算の精度 • 新しいmathライブラリ

33.

新mathライブラリ • • • • • • • • • float1, float2, float3, float4, half1, half2, half3, half4 int1, int2, int3, int4 math.abs math.min math.max math.pow math.lerp math.clamp • • • • • • • • math.saturate math.select // 条件分岐 math.rcp // 逆数 math.sign math.rsqrt // sqrtの逆数 math.any math.all math.sincos

34.

Let’s implement C# Job System.

35.

public class RotatorOldUpdate : MonoBehaviour { [SerializeField] float m_Speed; public float speed { get { return m_Speed; } set { m_Speed = value; } } void Update () { transform.rotation = transform.rotation * Quaternion.AngleAxis (m_Speed * Time.deltaTime, Vector3.up); } }

36.

Job Component System実装まとめ • STEP1:データレイアウトの最適化 • GameObjectごとにするのはやめる • データをシーケンシャルにする • キャッシュ化する • forループでGetComponentとかしなくてよくなる

37.

public class RotatorOldUpdate : MonoBehaviour { [SerializeField] float m_Speed; public float speed { get { return m_Speed; } set { m_Speed = value; } } void Update () { transform.rotation = transform.rotation * Quaternion.AngleAxis (m_Speed * Time.deltaTime, Vector3.up); } }

38.
[beta]
class RotatorManagerMainThread : ScriptBehaviourManager
{
List<Transform>
m_Transforms;
NativeList<float>
m_Speeds;
:
protected override void OnUpdate()
{
base.OnUpdate ();
float deltaTime = Time.deltaTime;
NativeArray<float> speeds = m_Speeds;
for (int i = 0; i != m_Transforms.Count; i++)
{
var transform = m_Transforms [i];
transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[i] * deltaTime, Vector3.up);
}
}
:
:
}
public class RotatorWithManagerMainThread : ScriptBehaviour
{
:
(たくさんの実装)
}

:

39.

Job Component System実装まとめ • STEP2: Job化 • List<Transform> → TransformAccessArray • IJobParallelForTransform継承したジョブ • Execute(int index, TransformAccess transform)の実装

40.
[beta]
class RotatorManagerMainThread : ScriptBehaviourManager
{
List<Transform>
m_Transforms;
NativeList<float>
m_Speeds;
:
protected override void OnUpdate()
{
base.OnUpdate ();
float deltaTime = Time.deltaTime;
NativeArray<float> speeds = m_Speeds;
for (int i = 0; i != m_Transforms.Count; i++)
{
var transform = m_Transforms [i];
transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[i] * deltaTime, Vector3.up);
}
}
:
:
}
public class RotatorWithManagerMainThread : ScriptBehaviour
{
:
(たくさんの実装)
}

:

41.
[beta]
class RotatorManager : ScriptBehaviourManager
{
TransformAccessArray
m_Transforms;
NativeList<float>
m_Speeds;
JobHandle
m_Job;
:
protected override void OnUpdate()
{
base.OnUpdate ();
m_Job.Complete ();
var jobData = new RotatorJob();
jobData.speeds = m_Speeds;
jobData.deltaTime = Time.deltaTime;
m_Job = jobData.Schedule (m_Transforms);
}

}

struct RotatorJob : IJobParallelForTransform
{
[ReadOnly]
public NativeArray<float>
speeds;
public float
deltaTime;
public void Execute(int index, TransformAccess transform)
{
transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[index] * deltaTime, Vector3.up);
}
}

public class RotatorWithManager : ScriptBehaviour
{
:
(たくさんの実装)
}

:

42.

Job Component System実装まとめ • STEP3: データからビヘイビアを分離する • ジョブで使用するデータを分離する • InjectTuplesの導入 • Tuples が付加した配列はindexが同期する • ComponentSystemから継承させる • マネージャーの仕事を任せる

43.
[beta]
public class RotationSpeedComponent : ScriptBehaviour
{
public float speed;
}
public class RotatingSystem : ComponentSystem
{
[InjectTuples]
public ComponentArray<Transform>
[InjectTuples]
public ComponentArray<RotationSpeedComponent>

}

m_Transforms;
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);
}
}

44.

Job Component System実装まとめ • STEP4: データのstruct化 • MonoBehaviour継承 → IComponentData継承 • struct化 • ComponentSystemからの継承でお手軽マネー ジャー • ComponentArray → ComponentDataArray

45.
[beta]
public class RotationSpeedComponent : ScriptBehaviour
{
public float speed;
}
public class RotatingSystem : ComponentSystem
{
[InjectTuples]
public ComponentArray<Transform>
[InjectTuples]
public ComponentArray<RotationSpeedComponent>

}

m_Transforms;
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);
}
}

46.
[beta]
[Serializable]
public struct RotationSpeed : IComponentData
{
public float speed;
public RotationSpeed (float speed) { this.speed = speed; }
}
public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }
public class RotatingDataSystem : ComponentSystem
{
[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);
}
}
}

47.

Job Component System実装まとめ • STEP5: ジョブ実装 と 依存性解決 • IJobParallelForTransformを継承したstruct • Execute で Transformが使える • ComponentSystem → JobComponentSystem • GetDependency()で依存性の自動解決

48.
[beta]
[Serializable]
public struct RotationSpeed : IComponentData
{
public float speed;
public RotationSpeed (float speed) { this.speed = speed; }
}
public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }
public class RotatingDataSystem : ComponentSystem
{
[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);
}
}
}

49.
[beta]
[Serializable]
public struct RotationSpeed : IComponentData
{
public float speed;
public RotationSpeed (float speed) { this.speed = speed; }
}
public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }
public class SystemRotator : JobComponentSystem
{
[InjectTuples]
public TransformAccessArray
m_Transforms;
[InjectTuples]
public ComponentDataArray<RotationSpeed> m_Rotators;
override protected 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);
}
}
}

50.

C# Job System 注意点 & まとめ

51.

C# Job System 注意点 • データ構造はstructのみ (class はNG) • .NETやUnity のAPIはジョブ内では(基本的に)使えない • 何でもかんでも早くなるわけではない • 算術系が早くなる、と考えるのが正解 • 相互の距離の計算とか • 敵AIの思考ルーチンとか

52.

リリース予定 • STEP1 C# Job system • Unity 2017.3 or 2018.X • STEP2 Component system • STEP3 math library • STEP4 C# Job Compiler

53.

実際に何に使える? • 多数の敵・味方が出るゲームでの相互距離の計算 • それによるソート • RTS等で使う影響マップの生成 • 弾幕シューティング • etc…

54.

C# Job System まとめ • マルチスレッドプログラミングが安全に書ける • 新しいComponent System で簡潔に書ける • コンパイラをかければさらに早くなる

55.

Q&A