Tanuki Log

雑記

【アドカレ2022】【C#】メモリとGC、最適化ことはじめ「unsafeとSpan」

※本記事は、Advent Callender 2022/12/25分の記事となります。


Unityとガベージコレクションについて

Unityでゲーム等を作成する場合に使用する言語として、C#が採用されています。

そこで避けて通れない存在として、ガベージ・コレクション(以下、GC)があります。

 

GC Collectが走ると、メインスレッドをブロックしてしまう性質があるため、ゲーム上でGCによるカクつき(ヒッチ)が発生します。

これを回避するためには、なるべく不要なメモリの確保(GC Allocation)を行わないようコーディングをする必要があります。

(GC Collectの発生を完全に回避することは実質不可能なので、できるだけ頻度を減らすという意味合いです。)

 

メモリ周りの知識については、下記が参考になると思います。

メモリ管理 - コンピュータの基礎知識 | ++C++; // 未確認飛行 C

 

GC Allocationを減らす手段としてのSpan

下記のUnity公式ブログには、以下のように記載されています。

 

blog.unity.com

 

Span の活用による、メモリの割り当てやコピーの削減。Unity は C++ のエンジンに C#スクリプトレイヤーを加えたもので、両者の間で多くのデータがやり取りされています。この場合、データコピーの往復や新しいマネージドオブジェクトの割り当てが必要になることが多いため、非効率になることがあります。 

このようなシナリオにおいて、状況を改善するために Span が C# 7.2 で導入され、.NET Standard 2.1 ではデフォルトで利用可能になっています。近年、Span のおかげで .NET Runtime のパフォーマンスが大幅に向上したことを耳にしたり、読んだりしたことがあるかもしれません(各バージョンでの改善の詳細については、以下のリンク先の文書でご確認ください:.NET Core 2.1.NET Core 3.0.NET 5.NET 6)。これは、多くの API について全体的なパフォーマンスを向上させながら、メモリ割り当てと、その結果として起きるガベージコレクションによる処理の一時停止を減らすのに役立つため、Unity 内部で活用したいと考えています。

 

Spanを活用しよう!という試みはあるものの、開発環境の関係でUnityのバージョンが古い(2021.2以前)場合はSpanが使えません(手段はあります)。

● Spanのメリットと使い所

メリットとしては、アンマネージドであるため、GC.Allocationが走りません。

また、Listに比べるとイテレーション速度が速いため、DB等から取得したListの中身が大きいサイズとなった際、愚直にfor文を回すよりもSpanの方が速い結果が得られる可能性があります。

GC.Allocationなしに文字列の参照を行いたい場合にも有効です。

 

● サンプルコードと導入

まずはサンプルコードを貼ります。

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace Common.Utility
{
class TestListObjects<T>
{
internal T[] Objects;
}

public static class ListExtensions
{
// 参照のみを行うために使う
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T[] AsObjects<T>(this List<T> list)
{
return Unsafe.As<TestListObjects<T>>(list).Objects;
}

// Span生成
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> AsSpan<T>(this List<T> list)
{
return new Span<T>(list.AsObjects());
}

// FindSpan(検索)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T FindSpan<T>(this List<T> list, Predicate<T> predicate)
{
var span = new Span<T>(list.AsObjects());
var spanCount = list.Count;

for (var i = 0; i < spanCount; i++)
{
if (predicate(span[i]))
{
return span[i];
}
}

return default;
}
}
}

 


var list = new List<int>();
for (var i = 0; i <= 10; i++)
{
list.Add(i);
}

// FindSpan
{
var result = list.FindSpan(i => i == 2);
Debug.LogFormat($"FindSpanResult: {result}", result != null ? result : "NotFound.");
}

 

サンプルはあくまで一例に過ぎませんが、

Listの中身が大きいサイズとなった際に有効です。

 

ただし、これだけではunsafeなコードを利用できず、Unity EditorのConsole上でエラーが表示されます。

まずは、
File > BuildSetting > PlayerSetting > Player > OtherSetting の

「Allow 'unsafe' Code」にチェックを入れます。

 

 

また、UnityProject内のAssets > Plugins内に、Unityのバージョンに対応したDLLファイル「System.Runtime.CompilerServices.Unsafe」を用意する必要があります。

Windows上ですと、

「C:\ProgramFiles\Unity\Hub\Editor\該当のUnityバージョン\Editor\Data\MonoBleedingEdge\lib\mono\msbuild\Current\bin\Roslyn\System.Runtime.CompilerServices.Unsafe.dll」

に対象のDLLファイルが存在します。

こちらをUnityProjectにインポートすると、エラーが解消し、アンマネージドなスクリプトを書いても動作します。

● 最後に

今回はunsafeを前提とするSpanの活用を紹介しましたが、

・そもそもボトルネックがどこにあるのかという目的を見失わず、特定のためにProfilerで計測を実施する

(例:ある処理で無意味にString型の生成を複数回行っていないか(StringBuilderやキャッシュで使い回すなどで改善が見込めます))

・マネージドな世界で高速化できるに越したことはない

(最適な手段を選ぶという意味での「最適化」)

という観点を忘れないよう、コーディングを心がけることが大切だと考えます。

適切にGC.Allocationを抑制するという目的に沿って使用することで、良い結果が得られると思われます。

 

参考:

Span<T>構造体 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

 

更新情報:

2022/12/26:サンプルコード前後の余分な記述を削除