tonari note

オンラインゲームエンジニアの雑記

Unityでパフォーマンス稼ぐための小ネタ その2

Unityでパフォーマンス稼ぐのって結構面倒くさかったりします。
たとえばパフォーマンスの敵と言われているものにガベージコレクションがあります。
時と場合によってGCが走らないようにする回避方法は変わったりしますが、今回はC#でパフォーマンスを稼ぐ一つの方法をご紹介します。

通信やその他ロードなどでバイナリファイルをリアルタイムに読み込むときに以下のようなコードを描いてたりしていると、GCが発生します。

using System;
using System.Linq;

class Program
{
	static void Main(string[] args)
	{
		var start = DateTime.Now;

		var source = Enumerable.Range(0, 100).ToArray();
		var binary = new byte[1024];

		var decode = new int[source.Length];

		start = DateTime.Now;
		var cache = new byte[sizeof(int)];
		for (var roop = 0; roop < 10000; ++roop)
		{
			for (var i = 0; i < source.Length; ++i)
			{
				cache = BitConverter.GetBytes(source[i]);
				binary[i * 4] = cache[0];
				binary[i * 4 + 1] = cache[1];
				binary[i * 4 + 2] = cache[2];
				binary[i * 4 + 3] = cache[3];
			}
			for (var i = 0; i < source.Length; ++i)
			{
				decode[i] = BitConverter.ToInt32(binary, i * 4);
			}
		}
		Console.WriteLine("経過時間 " + (DateTime.Now - start).ToString());
		Console.WriteLine("GC回数 " + GC.CollectionCount(0));
	}
}

実行結果
つらい。

CLRなんかに比べて当然Mono AOTだとGC回数は多くなるので、こういうのは避けたい。
というわけで以下のようにしてみます。

using System;
using System.Linq;

class Program
{
	static void Main(string[] args)
	{
		var start = DateTime.Now;

		var source = Enumerable.Range(0, 100).ToArray();
		var binary = new byte[1024];

		var decode = new int[source.Length];

		start = DateTime.Now;
		for (var roop = 0; roop < 10000; ++roop)
		{
			for (var i = 0; i < source.Length; ++i)
			{
				unsafe
				{
					fixed (byte* p = &binary[i * 4])
						*(int*)p = source[i];
				}
			}
			for (var i = 0; i < source.Length; ++i)
			{
				unsafe
				{
					fixed (byte* p = &binary[i * 4])
						decode[i] = *(int*)p;
				}
			}
		}
		Console.WriteLine("経過時間 " + (DateTime.Now - start).ToString());
		Console.WriteLine("GC回数 " + GC.CollectionCount(0));
	}
}

実行結果
結果はGC回数が大幅に減ってます。

あとAOTとポインタの相性がそこそこ良いっぽくて、BitConverter使うよりさらに速度を期待できます。
見てのとおりunsafeなのでunsafeで、それなりにルールもあるので使いどころは限られますが。