Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

【GTMF2018TOKYO】ハードウェアの性能を活かす為の、Unityの新しい3つの機能

15,742 views

Published on

2018/7/13に開催されたGTMF2018TOKYOの講演資料です。
講師:山村 達彦(ユニティ・テクノロジーズ・ジャパン合同会社)

Published in: Technology
  • Be the first to comment

【GTMF2018TOKYO】ハードウェアの性能を活かす為の、Unityの新しい3つの機能

  1. 1. ハードウェアの性能を活かす為の、
 Unityの新しい3つの機能 ver 1.1 ユニティ・テクノロジーズ・ジャパン 山村 達彦
  2. 2. Unityの目指しているもの ゲーム開発の民主化 難しい問題の解決 成功を支援 今回得に解決するもの
  3. 3. スクリプト、基礎から考え直す ECS C# Job System Burstコンパイラ GameObject MonoBehaviour (簡単、様々な用途で使える。だが遅い) (よりHWに寄り添う) (パフォーマンスのために
  難しい事をする必要がある)
  4. 4. “ ” Performance By Default デフォルトで高性能なコードを
  5. 5. 3つの機能 ・C# Job System ・Entity Component System (ECS) ・Burst Compiler 今回はコレの話
  6. 6. Entity Component System オブジェクトを制御する :存在 :部品 :システム
  7. 7. コンポーネントは状態と処理を持つ GameObjectはコンポーネントのコンテナ GameObject GameObject • ゲーム世界内での物体 • 複数のComponentを持つTransform Component Component • 単体で動作 • フィールドを持つ • ロジックを持つ • 他のComponentを操作する
 (Transform等) Data Data Data Logic Logic
  8. 8. GameObject / Componentの場合 動作はオブジェクトごとに独立 動作を決める為に他のオブジェクトへの参照が必要 ハンドル タイヤ エンジン ハンドルの入力処理 曲がる動作の処理 加速する動作 Data Data Data DataData
  9. 9. GameObject Monobehaviourは他オブジェクトへの参照を持つ Transform Move Control GameManager Gun GameFlow 参照関係が複雑になると セットアップがややこしくなる n
  10. 10. GameObject / Componentの場合 依存関係が面倒な場合、Singletonが活躍 ハンドル タイヤ エンジン ドルの入力処理 がる動作の処理 加速する動作 神 Super Special Awesome ultra Game Manager
  11. 11. “ ” それで、ECSではどうなってんの?
  12. 12. ECSではオブジェクト毎にロジックを持たせず、 ComponentSystemが一括制御する Component A Data Data Data Component A Data Data Data Component A Data Data Data Component A ComponentSystem処理 処理 処理 毎フレーム呼ばれるイベントを持つ
  13. 13. ComponentSystemへのオブジェクト登録は自動 ComponentSystem Transform[] GameObject Transform Component GameObject Transform Component GameObject Transform Component GameObject 全体から効率的に自動検索。 Systemは誰がオブジェクトを
 生成しているか知らなくても良い Group
  14. 14. 取得するオブジェクト一覧は、 要求するComponentでフィルタリングする ComponentSystem Transform[] Move[] GameObject Move Transform GameObject Move Transform Player 必要なコンポーネントが 揃った状態で取得できる Group GameObject Transform Moveが足りない
  15. 15. Small Demo Rigidbody スピード キャラクター操作する
 System 入力を受け付ける
 System Object システムが他のシステムを 参照しつつ、オブジェクトを 操作する例 GameObjectEntity
  16. 16. Demo ComponentSystemを継承
  17. 17. Demo Systemが要求する Componentを登録
  18. 18. Demo[Inject]でオブジェクトへの参照を注入
  19. 19. Demo OnUpdateにオブジェクトを操作する処理を記述
  20. 20. 複数のシステムでEntityの動きを作る 入力
 システム 入力 座標 形状 耐久力 移動
 システム 当たり判定
 システム 死亡判定 コントロール 流れ作業のようにComponentDataを操作して、動きを作る
  21. 21. ECS上では、保有するComponentの組み合わせで動作が変わる タイヤ 人力 タイヤ 人力 荷物 荷物 タイヤ タイヤ エンジン エンジン エンジンで
 移動 荷物を
 運ぶ 人力で
 移動
  22. 22. Groupが要求できる組み合わせ ComponentSystem Group Entityに登録した • Component • ComponentData • SharedComponentData • … を含む 及び を含まない 要求できるもの なにこれ Entity / GameObject
  23. 23. Entityは超軽量なGameObject ComponentDataはロジックを持たないコンポーネント Entity • ゲーム世界でのモノ • 複数のComponentDataを持つPosition Wheel Engine ComponentData • Entityに含まれる • 値を持つ • 参照型を持たない • ロジックは持たない
  24. 24. GameObject Transform Component Data Data Logic Logic Position Wheel Engine EntityもGameObjectも役割は同じ「モノ」という概念 Entity
  25. 25. public struct Engine : IComponentData { public float power; } IComponentDataを継承 値のみ持つ 何もデータを持たない事もある public class Engine : MonoBehaviour { public float power; } MonoBehaviour ComponentData 定義の違い 構造体
  26. 26. var entity_manager = World.Active.GetOrCreateManager<EntityManager>(); var entity = entity_manager.CreateEntity(typeof(Engine)); EntityManagerで生成 var obj = new GameObject(“obj”, typeof(Engine)); MonoBehaviour ComponentData 生成の違い
  27. 27. ComponentDataArrayや EntityArrayで取得 struct Group{ GameObjectArray objs; ComponentArray<Monobehaviour> comps; } MonoBehaviourを要求 ComponentDataを要求 Systemが要求する時の違い struct Group{ EntityArray entities; ComponentArray<ComponentData> comps; }
  28. 28. “ ” GameObject&Componentから EntityとComponentDataに変えて 何かメリットでもあるの? Q先程のGameObjectとの組み合わせる場合はECSハイブリットと呼ぶ
  29. 29. “ ” ECSが本気モードになる A ハードのお勉強
  30. 30. メモリキャッシュ・CPUコア CPU CPU メモリ メモリからデータをロード
 するのは時間がかかる 1アクセスの間に 200サイクルも 処理ができる メモリのアクセス速度と
 CPUの性能は大きな差がある
  31. 31. メモリキャッシュ・CPUコア CPU CPU メモリ キャッシュを活用して ギャップを緩和 キャッシュから使えば 100倍早くなるかも L1
 キャッシュ L1
 キャッシュ L2キャッシュ 2〜3
 サイクル 20〜30
 サイクル
  32. 32. 一回でロードするのは64バイト 1byteしかデータを使用しない 場合でも64byte単位でロード いかに無駄なくデータを
 配置できるかがポイント メモリ CPU L1
 キャッシュ L2キャッシュ 64byte 64byte 64byte キャッシュライン
  33. 33. 「クラシック」の問題(1) データが散在していてキャッシュが使いにくい 必要なデータを集めるのに何度もロードが必要 Transform GameObject 1 Data Renderer Data Data Data Data Monobehavour Data Data TransformRigidbody Data DataData GameObject 2 Data Monobehavour Data GameObject 3 Data Transform Data Data Rigidbody Data Data Data
  34. 34. 「クラシック」の問題(2) float Speed; Transform transform … Vector3 up; Vector3 right; Vector3 forward; Vector3 position; Vector3 localPosition; Quatrainion rotation; Quatrainion localRotation; … MoveScript Transform 余計なデータが たくさん含まれる 余計なデータで キャッシュがすぐ埋まる 太字:必要なデータ 細字:不要なデータ
  35. 35. ECSのメモリレイアウト(見た目) Positoin Positoin Positoin Positoin Rotation Rotation Rotation Rotation Speed Speed Speed Speed Entity 0 Entity 1 Entity 2 Entity 3 EntityはComponentDataを持つコンテナのように見えるが…
  36. 36. ECSのメモリレイアウト(実際に近いイメージ) Positoin Positoin Positoin Positoin Rotation Rotation Rotation Rotation Speed Speed Speed Speed pos Array rot Array spd Array … … … ※実際にはもっと複雑で様々な最適化を含む Entityは唯のID、実態は構造体の配列 0 1 2 3 Entity2
  37. 37. ECSのシステムがアクセスするデータ Positoin Po Rotation Ro Speed Sp Pos Array Rot Array Spd Array 使用するデータのみ アクセスする 余計なデータで キャッシュがすぐ埋まらない
  38. 38. 連続するデータは先読み予測が効く POS POS POS POS POS POS POS POS POS POS POS POS 読んだ キャッシュライン 読んだ 多分次はココやろ 先に読んどいたる プリフェッチ
  39. 39. メモリはNative Containerで確保 •アンマネージドなメモリを提供 = GCは発生しない •利便性を損なわず効率アップ •IL2CPPではインデクサが1命令 •AOSやSOAのなどに対応 (get/setのオーバーヘッドなし) NativeArraySOANativeArray
  40. 40. NativeArray var a = new NativeArray<MyStruct>(32, Allocator.Persistent); 生成 解放 Allocator.Persistent Allocator.Temp Allocator.TempJob 永続的に使用可能 同じフレームで解放しないとエラー 4フレーム以内に解放しないとエラー
 ジョブ終了時に自動開放させるオプション有 a.Dispose();
  41. 41. オブジェクトの増減が低コスト POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS POS 最後の要素で塗り直すだけ ガベージコレクションは発生しない 要素を削除したときは POS
  42. 42. GameObject 1 Data Renderer Data Data Data Data Data Data Data Data Data Data Data Data Data Data Data Data 手続き的な手法を踏まずにオブジェクトをロード Entity Entity Entity Entity Entity Data Data Data Data レイアウトが確定しているので、 ロードしたデータをそのまま メモリに展開 Data Entity Entity Data Data Data Data Entity Entity ちょっと未来話 File
  43. 43. ECSまとめ 新しいオブジェクトを制御する機構 コンポーネント志向に近い形のオブジェクト制御 メモリレイアウトがCPUに優しい データ志向のレイアウト キャッシュ、フェッチを活用してアクセスを効率化 ECSで組めばHWを知らなくてもよしなにしてくれる
  44. 44. “ ” C# Job System 安全・簡単にマルチコアを活用
  45. 45. 最近のコアは増加傾向にある 1コア辺りのクロック数 コアの数 1989 2018 ※イメージ図 モバイルですら6〜8コア積んでる時代 iOS/Android
  46. 46. 一つのコアで複数の処理を実行 Core 0 Core 1 Work Work Work Work Work Work index[0] index[1] index[2] index[3] index[4] index[5] ZZZzzz 一つのコアが作業している間、他のコアは仕事が無い
  47. 47. 複数のコアで複数の処理を実行 複数のコアで分担して作業すれば早くおわる Core 0 Core 1 Work Work Work Work Work Work index[0] index[1] index[2] index[3] index[4] index[5] だからもっとコアを活用しよう!と言いたいが、
  48. 48. とはいえ、マルチスレッドは難しい 粒度 同期 競合 大きな区分で仕事を任せた方が色々と早いが、分担が難しい 同僚の仕事が終わらないと自分の仕事を始められない 使用中のデータを、誰かが書き換えて結果がおかしくなる デッドロック 互いに完了待ちしてしまい、仕事が停止する バグるとデバッグしんどいです
  49. 49. C# Job Systemでしんどくないマルチコア対応 ルールに従えば簡単・安心にマルチコア活用 ECSとも簡単に連携できる Burstを通すと、もっと高速化 並列処理で処理が完了するまでの時間を短縮
  50. 50. High Performance C# (C# Job Systemの制約) Class Type無し Boxing無し GC Allocation無し Exception無し 大丈夫! まだ (一応) C# All Basic Types Struct Enum Generic Properties Safe Sandbox 高速なC#を組む為のルール
  51. 51. メインスレッド以外もガンガン使うの楽しい
  52. 52. C# Job Systemの2つのAPI IJob Main Worker 0 Worker 1 Worker 2 IJobParallelFor 別のコアで処理 複数のコアで一括処理 Main Worker 0 Worker 1 Worker 2 処理 メインスレッドはジョブ発行するマン 処理が追いつかなければメインスレッドも使って一括処理
  53. 53. 例えばシューティングの 当たり判定
  54. 54. C# Job Systemの基本的な流れ(1/3) pos[0] pos[1] pos[2] pos[3] pos[4] pos[5] res[0] res[1] res[2] res[3] res[4] res[5] 3つのバッファーを用意する Jobの処理は全て確保したバッファーに対して行う NativeArray <float3> NativeArray <int> 座標データとか 座標的に接触したか NativeArray <int> 結局、接触したのか ans[0]
  55. 55. C# Job Systemの基本的な流れ(2/3) pos[0] pos[1] pos[2] pos[3] pos[4] pos[5] res[0] res[1] res[2] res[3] res[4] res[5] 複数のコアで担当する要素を処理し
 結果をバッファに格納する NativeArray <float3> NativeArray <int> 計算に利用するデータ 結果を格納する Core 1 で処理 Core 3 で処理Core 2 で処理
  56. 56. C# Job Systemの基本的な流れ(3/3) res[0] res[1] res[2] res[3] res[4] res[5] ans[0] 結果をまとめて最終的な判定を行う NativeArray <int> 最終的な結果 Core 1 で処理 NativeArray <int> 結果を格納する ココを見て接触したか判定
  57. 57. pos[0] pos[1] pos[2] pos[3] pos[4] pos[5] res[0] res[1] res[2] res[3] res[4] res[5] Core 1 で処理 Core 3 で処理Core 2 で処理 ans[0] Core 1 で処理 IJob IJobParallelFor 弾が特定の場所に接触したかを 全てのコアを使って並列処理 やってみる並列処理(1)
  58. 58. IJobParallelForを定義(1/3) struct 弾の判定ジョブ :         { public void Execute(int id) { } } IJobParallelForを継承した構造体を定義 継承 IJobParallelFor
  59. 59. IJobParallelForを定義(2/3) struct 弾の判定ジョブ : IJobParallelFor { public void Execute(int id) { } } Jobが使用するバッファを定義 入出力のバッファ public NativeArray<Vector3> pos; public NativeArray<int> res; C# Job Systemは 入出力を明確にする
  60. 60. IJobParallelForを定義(3/3) struct 弾の判定ジョブ : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { } } Jobの処理をExecuteに記述 idは供給される Jobが実行する処理 res[id] = (Vector3.Distance( Vector3.zero, pos[id]) < 1) ? 0 : 1;
  61. 61. IJobParallelForを使う(1/4) struct 弾の判定 : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { pos[id] = (Vector3.Distance( Vector3.zero, res[id]) < 1) ? 0 : 1; } } Jobを生成 void Update() { } 生成 var ajob = new 弾の判定ジョブ(){} ;
  62. 62. IJobParallelForを使う(2/4) struct 弾の判定 : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { pos[id] = (Vector3.Distance( Vector3.zero, res[id]) < 1) ? 0 : 1; } } Jobにバッファを登録 void Update() { var ajob = new 弾の判定ジョブ(){ } } データ参照 pos = positions, res = results1
  63. 63. IJobParallelForを使う(3/4) struct 弾の判定 : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { pos[id] = (Vector3.Distance( Vector3.zero, res[id]) < 1) ? 0 : 1; } } void Update() { var ajob = new 弾の判定ジョブ(){ pos = positions, res = results1 } } 実行指令 1ジョブでまとめて 実行する回数 毎フレームScheduleを呼ぶ var handle = ajob.Schedule(positions.Length, 8);
  64. 64. IJobParallelForを使う(4/4) struct 弾の判定 : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { pos[id] = (Vector3.Distance( Vector3.zero, res[id]) < 1) ? 0 : 1; } } void Update() { var ajob = new 弾の判定ジョブ(){ pos = positions, res = results1 } var handle = ajob.Schedule(positions.Length, 8); } 即時ジョブ起動 完了まで待つ 以下は必要に応じて JobHandle.ScheduleBatchedJobs(); handle.Complete();
  65. 65. 動かしてみる
  66. 66. メインスレッドの処理 (ジョブの発行) ワーカースレッドの処理 (当たり判定の計算)
  67. 67. pos[0] pos[1] pos[2] pos[3] pos[4] pos[5] res[0] res[1] res[2] res[3] res[4] res[5] Core 1 で処理 Core 3 で処理Core 2 で処理 ans[0] Core 1 で処理 IJob IJobParallelFor やってみる並列処理(2)
  68. 68. IJob struct 結果を受け取るジョブ :    { public void Execute() { } } 継承 IJob
  69. 69. IJob struct 結果を受け取るジョブ : IJob { public void Execute() { } } 入出力 public NativeArray<int> res; public NativeArray<int> ans;
  70. 70. IJob struct 結果を受け取るジョブ : IJob { public NativeArray<int> res; public NativeArray<int> ans; public void Execute() { } } ジョブの実行内容 var length = res.Length; for(var i=0; i<length; i++){ if( res[i] == 1){} }
  71. 71. IJob struct 結果を受け取るジョブ : IJob { public NativeArray<int> res; public NativeArray<int> ans; public void Execute() { var length = res.Length; for(var i=0; i<length; i++){ if( res[i] == 1){ return ;} } } } 結果を格納 ans[0] = 1; ans[0] = 0;
  72. 72. IJob struct BJob : IJob { public NativeArray<int> res; public NativeArray<int> result; public void Execute() { for (var i = 0; i < positions.Length; ++i) { var pos = positions[i]; if( pos.x > -0.5f && pos.x < 0.5f){ result[0] = 1; return; } } } } 要素への参照なので危険がつきまとう 読込専用なら兎も角、読み書きするジョブが複数同時に同じ要素を操作するとヤバイ [ReadOnly]や[WriteOnly]属性をつける データの読取専用 データの書込専用
  73. 73. IJob struct 結果を受け取るジョブ : IJob { public NativeArray<int> res; public NativeArray<int> ans; public void Execute() { var length = res.Length; for(var i=0; i<length; i++){ if( res[i] == 1){ans[0] = 1; return ;} } ans[0] = 0; } } 読取専用・書込専用の 属性を付ける [ReadOnly] [WriteOnly]
  74. 74. 問題が起こりそうならエラーを出してくれる InvalidOperationException: The previously scheduled job Job1:AJob writes to the NativeArray AJob.positions. You are trying to schedule a new job Job1:BJob, which writes to the same NativeArray (via BJob.positions). 要約:複数のジョブが同じNativeArrayに同時に書き込むかも
  75. 75. InvalidOperationException: The native container has been declared as [WriteOnly] in the job, but you are reading from it. 要約:書込専用のNativeArrayを読み込んじゃあかん 問題が起こりそうならエラーを出してくれる ありがたや
  76. 76. ジョブの依存関係 Scheduleの引数にJobHandleを入れる void Update() { var ajob = new 弾の判定ジョブ() { …}; var bjob = new 結果を受け取るジョブ() { … }; var handle = ajob.Schedule(positions.Length, 8); handle = bjob.Schedule(handle); } bJobはaJobが 完了したら実行
  77. 77. 動かしてみる
  78. 78. 集計処理 メインスレッド
  79. 79. ECSとのC# Job Systemの連携(1) public struct RigidbodyPositionJob :                              { } [Inject]無しにComponentData注入、若干効率的 Injectの代わりにジェネリックでComponentDataを要求 IJobProcessComponentDataを使う IJobProcessComponentData<Position, RigidbodyPosition>
  80. 80. ECSとのC# Job Systemの連携(2) public struct RigidbodyPositionJob : IJobProcessComponentData<Position, RigidbodyPosition> { } [Inject]無しにComponentData注入、若干効率的 [ReadOnly][WriteOnly]属性で、 競合しないように、ジョブの実行順を 自動調整してくれる機能付 IJobProcessComponentDataを使う public void Execute(ref Position pos, ref RigidbodyPosition rot) { // 処理 }
  81. 81. .NETのThreadと比較して、どう違うの? UnityのWorker Thread上で動作 コンテキストスイッチの増加を防ぐ オーバーヘッドが少ない ※IL2CPPでビルドした場合 使うための制約がキツイ ハイパフォーマンスなC#記述を強制 ケースバイケースで 使い分け推奨 .NET 4.xやC#7.2対応で 使えるようになった 複数フレームを跨ぐ処理に向かない そもそもコアを使い切る為のもの
  82. 82. 結局、マルチスレッドの難しい点は解決出来たか? • ジョブのまとめて実行する数を指定できる  →粒度をある程度コントロールする ・複数のジョブが同じ参照先に書き込み可能だとエラー  →競合の発生をエディターで防ぐ
  83. 83. 結局、マルチスレッドの難しい点は解決出来たか? ・ジョブはメインスレッドでのみCompleteできる  →デッドロックを起こさない ・ジョブはメインスレッドでのみScheduleできる  →Completeとの整合性を重視
  84. 84. “ ” 危険なのは自由すぎるマルチスレッド
  85. 85. “ ” Burst 厳しい条件下で最適化するコンパイラ
  86. 86. 第三の選択肢 IL2CPP Burstコンパイラ 使い勝手を維持しつつ 単純にC++へ変換 厳しい条件下で最適化 Mono IL 1 2 3 option option option
  87. 87. 厳しい前提条件の上で最適化 High Performance C#前提 Class Type無し Boxing無し GC Allocation無し Exception無し 連続したメモリレイアウト前提 データの入出力がハッキリしてる前提 制約を設ける事で 限界まで最適化 ECSやC# JobSystemで 組めばOK
  88. 88. struct 弾の判定 : IJobParallelFor { public NativeArray<Vector3>pos; public NativeArray<int> res; public void Execute(int id) { pos[id] = (Vector3.Distance( Vector3.zero, res[id]) < 1) ? 0 : 1; } } 使い方 IJob系のインターフェースに BurstCompile属性をつける [Unity.Burst.BurstCompile]
  89. 89. Burst OFF Burst ON だいたい0.2msだいたい 7ms (IL2CPPで4倍くらい高速化)
  90. 90. 様々な最適化 最適化しやすいMathライブラリも提供 CPU拡張命令を積極的に使う 計算の精度を下げる(オプション) 厳密な結果が必要ない箇所に使うオプション プロセッサ向けの最適化をUnityで行う avx2, sse4, neon, arrch その他、様々な最適化
  91. 91. Burstでコンパイルした後のアセンブリは確認出来る 最大効率でないなら、 レポートすれば修正が入るかも
  92. 92. PackageManagerで提供 新しいプロセッサが追加された際、
 Unityエンジンを自体をアップデート
 せず対応が可能 ECSパッケージには最初から含む
  93. 93. “ ” ECSでメモリレイアウト制御(難しい)をよしなにしてくれる C# Job Systemでマルチコア対応(難しい)をよしなにしてくれる BurstでCPU演算向け最適化(難しい)をよしなにしてくれる まとめ
  94. 94. “ ” 頑張って最適化せずとも 最初から最大効率で動作するコードになる まとめ
  95. 95. “ ” 空いた余剰スペックで もっと多くの要素をゲームにつぎ込めるように Thank You オブジェクト管理 消費電力 処理時間

×