【アドカレ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公式ブログには、以下のように記載されています。
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:サンプルコード前後の余分な記述を削除