はじめに
今回はBurst Direct Call
を触って、簡単な計測をしてみたという記事になります。
公式のYoutube動画でBurst Direct Call
の紹介動画があったので、自分でも触ってみようかなと思った次第です。
結果としてあんまり良い結果が得られなかったのですが、備忘録として残しておきます。
概要
Burst 1.5 の新機能のうち特筆するべきものに、Direct Call(直接呼び出し)と呼ばれるものがあります。Burst では、HPC#コンパイラを使って Unity のジョブシステム上で実行されるタスクを高速化するために、まずジョブに注目しました。その後、関数ポインターを追加し、Burst のコードの一部をどこからでも管理・呼び出しできるようにしました。
2021 年は Burst 1.5 でコードを高速化しよう | Unity Blog
今まではBurst & Job System
の組み合わせで利用していたのですが、Burst Direct Call
を利用すればC# Job System
を使わずにBurst
の恩恵を受けることができます。
ただUnity 2019.4 or later
でないとサポートされていないよう。
Burst supports the ability to call out to Burst compiled methods directly from managed code (2019.4+ only).
Language Support | Burst | 1.6.6
あとBurst v1.5
まではMain Thread
からしか呼び出せませんでしたが、Burst v1.6
からはどのスレッドでも大丈夫だそうです。
Fixed the 1.5 restriction that Direct Call methods can only be called from the main thread, now they work when called from any thread.
環境
MacBook Pro (16-inch, 2019)
Unity2021.3.0f1
Burst v1.6.6
インストール
PackageManager
のUnity Registry
からインストールできます。
XorShift
今回は適当な乱数を生成して2048x2048
の画像に適応するサンプルを作成していきます。
擬似乱数を生成するにはLCG
やメルセンヌ・ツイスタ, XorShift
, PCG
など色々とアルゴリズムがあるそうですが今回はXor Shift
を使ってみたいと思います。
ja.wikipedia.org
private uint value = 2463534242; /// <summary> /// 32bitのXorShift /// </summary> private uint XorShift() { value ^= value << 13; value ^= value >> 17; value ^= value << 5; return value; }
またUnity.Mathematics.Random
というランダム用の構造体があるのですが、乱数生成の仕組みを知りたかったのもあり自作してみました。
(UnityEngine.Random
はメインスレッド以外で動作しなかったり、マネージドピープを利用していたりと辛いところがある)
実装
Class
とstatic
関数に[BurstCompile]
をつけます。
Language Support | Burst | 1.6.6
[BurstCompile] public sealed class BurstDirectCallTest : MonoBehaviour { private const int Size = 2048; [SerializeField] private RawImage image; private Texture2D _texture; private uint _xorShiftValue = 2463534242; private void Start() { // アルファのみのテクスチャを用意 _texture = new Texture2D(Size, Size, TextureFormat.Alpha8, false); image.texture = _texture; } private void Update() { // マネージドヒープではなく、ネイティブメモリで確保された配列を用意 var nativeArray = new NativeArray<byte>(Size * Size, Allocator.Temp); // ランダムなノイズを生成 for (var x = 0; x < Size; x++) for (var y = 0; y < Size; y++) nativeArray[x * Size + y] = GetPixel(); // 画像にピクセルを反映, GPUにアップロード _texture.LoadRawTextureData(nativeArray); _texture.Apply(); // GCの管理外なので、きっちり破棄するのを忘れず nativeArray.Dispose(); } private void OnDestroy() { image.texture = null; Destroy(_texture); } /// <summary> /// ピクセルの色を取得する /// </summary> private byte GetPixel() { // 0 ~ 255(alpha : 8bits)の擬似乱数を返す _xorShiftValue = XorShift(_xorShiftValue); return (byte) (_xorShiftValue % 255); } /// <summary> /// 32bitのXorShift /// </summary> [BurstCompile] private static uint XorShift(uint value) { value ^= value << 13; value ^= value >> 17; value ^= value << 5; return value; } }
このコードをみるとNativeArray
をキャッシュして使い回した方がいいんじゃ?みたいな感覚を抱きますが、これってマネージドヒープの考え方って事でいいんですかね。
Unity.Collections.Allocator.Temp
があるくらいですし、間違ってはいないとは思うのですが。
エディタ上での結果
試してみた結果以下のようになりました。またMono
と表記されているものは[BurstCompile]
を取り除いた以外は同じコードを使ってます。
- Safety Checks : Off
- Enable Compilation : On
- Synchronous Compilation : On
- Native Debug Mode Compilation : Off
- Show Timings : Off
ちゃんと平均FPSを測るべきではありますが、ざっと見た感じ以下ぐらいになっていました。
種類 | FPS |
---|---|
Mono | 19.1 |
Burst Direct Call | 6.1 |
まさかのMono
の方が早いという結果に。
エディター上で動かしたせいなのか、コードが間違っているのか、設定が間違っているのか。
一応コードはkeijiro
神とほぼ同じにしたつもりなのですが、もう何も分かりません。
アセンブリが読めたりできれば一番良かったのですが......。
StandaloneOSXで実験
種類 | FPS |
---|---|
Mono | 28.0 |
Burst Direct Call | 10.5 |
※ 計測値は正確な値ではなく、ざっと見積もった値です。
さいごに
動画ではMemoryMarshal.Cast
とSpan
を生成(ポインタと長さ)&渡すという手法を高速化のために紹介されていましたが、それをやる前でも結構な速度改善になっていたんですよね。
別にエディタだから失敗したみたいでもなさそうですし、暇があればもっと細かく調べてみたいと思います。