5.2K Views
September 04, 17
スライド概要
017/8/30~9/1に開催されたCEDEC2017の講演スライドです。
講師:伊藤 周 (ユニティ・テクノロジーズ・ジャパン合同会社)
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
C#JobSystem を使った Unity流マルチスレッドプログラミング ユニティ・テクノロジーズ・ジャパン合同会社 エバンジェリスト 伊藤 周
諸注意 • • • • 今回紹介するC# Job Systemはまだ発展段階 リリースでは多少の差異が出る可能性がある C# Job Systemの概念を知ってほしい プログラマ以外は理解不能
アジェンダ • 従来のマルチスレッドプログラミング • C# Job System の概要 • Let’s read codes. (コードを読む) • Let’s make a mistake. (間違ってみる) • Let’s try “C# Job Compiler” (コンパイラを体験) • Let’s implement. (実装してみる) • まとめ
従来のマルチスレッドプログラミングの話
マルチスレッド プログラミングは 好きですか?
私は嫌いです
MTPのここが嫌だ その1 • レースコンディション対策が嫌だ Aが使うよ • 面倒臭い write A • コードが汚い、可読性が低い • 間違っても気づきにくい ? B write Aが使うよ Bが使うよ read A
MTPのここが嫌だ その2 • デッドロックが嫌だ • 面倒臭い A B • コードが汚い、可読性が低い • 間違うと無限ループ 待ち B 待ち A
MTPのここが嫌だ その3 • 難解なところが嫌だ • mutex:lockとか • アトミック変数とか
MTPのここが嫌だ その4 • デバッグが嫌だ • 正常に”動いてしまったり”する • 無限ループになったりする • 突然ハングアップしたりする • リリース後にバグが判明したりする
私には無理だ!
そんなあなたに C# Job System
C# Job System の概要
43 伊藤周の年齢
116
116倍 Boid シミュレーションを マルチスレッドで 8 core CPUで 動かした場合の速度倍
Demo
C# Job System の特徴 • • • • 簡潔に書ける GCフリー 安全 高速な新コンパイラ
特徴1 簡潔に書ける • Data Oriented Programming • データとビヘイビア(振舞い)の分離 • struct(構造体)コンポーネントの導入 • Job Component System の用意 • 簡潔に書けるようにマネージャーを用意
特徴2 GCフリー • GCをいかにさせないか • NativeArrayの導入 • 以下の感じで確保 要素数 ↓ アロケーターの種類 ↓ var src = new NativeArray<float>(500, Allocator.Temp); • 以下の感じで解放(自分で) src.Dispose();
特徴2 GCフリー • 他のNativeArrayファミリー struct NativeArray<Value> // 配列 struct NativeList<Value> // リスト。追加削除が容易 struct NativeSlice<Value> // 一部を切り取れる struct NativeHashmap<Key, Value> // Dictionary「 struct NativeMultiHashmap<Key, Value> //複数Dictionary
特徴3 安全 • • • • エラーで指摘してくれる 落ちることはない レースコンディション、デッドロックは起こり得ない “Sandbox”
特徴4 高速な新コンパイラ • C#→[Mono]→IL→[C# Job Compiler]→内部的な Domain Model →[最適化]→[LLVM]→実行形式 • 10倍〜20倍高速になる • 電池消費の軽減 • Why faster? • SIMD命令の有効利用 • 正確さとパフォーマンスのトレードオフ
Let’s read C# Job System codes!
コーディング基本まとめ • • • • • IJob~でジョブを定義 Execute にジョブの中身を書く Schedule でジョブを開始 Complete でジョブ終了確認 変数はNativeArray系を使い、自力でDispose
コーディング基本まとめ • IJob • 1つのスレッドでジョブを回す public void Execute() {} • IJobParallelFor • 複数のスレッドでジョブを回す public void Execute(int i) {} • IJobParallelForTransform • Transformにアクセスが可能 public void Execute(int i, TransformAccess transform){}
Let’s make a mistake!
エラーまとめ • マルチスレッドプログラミングは間違えやすい • ちょっとした見落としはしてしまう • Unityは落ちることなくエラーが教えてくれる • CTO Joachim「Unityは「Sandbox(=砂場)」である」 • 砂場では間違っていい。正解に導いてくれれれば。
Let’s try “C# Job コンパイラ”
C# Job Compiler • 一文付け足すだけ • [ComputeJobOptimizationAttribute(Accuracy .Med, Support.Relaxed)] • Accuracy は計算の精度 • 新しいmathライブラリ
新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
Let’s implement C# Job System.
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); } }
Job Component System実装まとめ • STEP1:データレイアウトの最適化 • GameObjectごとにするのはやめる • データをシーケンシャルにする • キャッシュ化する • forループでGetComponentとかしなくてよくなる
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); } }
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
{
:
(たくさんの実装)
}
:
Job Component System実装まとめ • STEP2: Job化 • List<Transform> → TransformAccessArray • IJobParallelForTransform継承したジョブ • Execute(int index, TransformAccess transform)の実装
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
{
:
(たくさんの実装)
}
:
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
{
:
(たくさんの実装)
}
:
Job Component System実装まとめ • STEP3: データからビヘイビアを分離する • ジョブで使用するデータを分離する • InjectTuplesの導入 • Tuples が付加した配列はindexが同期する • ComponentSystemから継承させる • マネージャーの仕事を任せる
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);
}
}
Job Component System実装まとめ • STEP4: データのstruct化 • MonoBehaviour継承 → IComponentData継承 • struct化 • ComponentSystemからの継承でお手軽マネー ジャー • ComponentArray → ComponentDataArray
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);
}
}
[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);
}
}
}
Job Component System実装まとめ • STEP5: ジョブ実装 と 依存性解決 • IJobParallelForTransformを継承したstruct • Execute で Transformが使える • ComponentSystem → JobComponentSystem • GetDependency()で依存性の自動解決
[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);
}
}
}
[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);
}
}
}
C# Job System 注意点 & まとめ
C# Job System 注意点 • データ構造はstructのみ (class はNG) • .NETやUnity のAPIはジョブ内では(基本的に)使えない • 何でもかんでも早くなるわけではない • 算術系が早くなる、と考えるのが正解 • 相互の距離の計算とか • 敵AIの思考ルーチンとか
リリース予定 • STEP1 C# Job system • Unity 2017.3 or 2018.X • STEP2 Component system • STEP3 math library • STEP4 C# Job Compiler
実際に何に使える? • 多数の敵・味方が出るゲームでの相互距離の計算 • それによるソート • RTS等で使う影響マップの生成 • 弾幕シューティング • etc…
C# Job System まとめ • マルチスレッドプログラミングが安全に書ける • 新しいComponent System で簡潔に書ける • コンパイラをかければさらに早くなる
Q&A