[Unity] .Net4.xのときにRegexクラスを使うときは独自にキャッシュすることも考えたい

## 動機
Unity で .Net3.5 から .Net4.x に変えただけですごく遅くなるコードが見つかった。プロファイリングをすると Regex クラスのコンストラクタの処理が重いということが分かったので調べたことをまとめる。

## 動作環境
Unity 2019.1.9f1

## 内容
次のコードをUnity上で .Net3.5 と .Net4.x それぞれで動作させてみる。　
コードの内容はRegexクラスを生成して文字列を正規表現にかける処理をループで回しているだけ。

```csharp
using System.Text.RegularExpressions;
using UnityEngine;

public class MyRegex : MonoBehaviour
{
    void Start()
    {
        var str = "hogehoge";
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        for (var i=0; i < 10000; i++)
        {
            var regex = new Regex("^[A-Za-z0-9]+$");
            regex.Match(str);
        }

        sw.Stop();
        Debug.Log("time : " + sw.ElapsedMilliseconds);
    }
}
```

### 結果
実行するたびに多少差はでるが、だいたい次のような結果になった。


```:.Net3.5
time : 36
```

```:.Net4.x
time : 613
```

.Net4.x が .Net3.5 に比べて20倍ほど遅いのがわかる。
(指定したパターン次第でこの差は変わる)

## 原因調査
[Unity-Technologies/mono](https://github.com/Unity-Technologies/mono/)でコードをみるため、まずmonoのバージョンを確認する。

```
% /Applications/Unity/Hub/Editor/2019.1.9f1/Unity.app/Contents/Mono/bin/mono --version
Mono JIT compiler version 2.6.5 (tarball Mon Mar 11 12:18:44 GMT 2019)
Copyright (C) 2002-2010 Novell, Inc and Contributors. www.mono-project.com
	TLS:           normal
	GC:            Included Boehm (with typed GC)
	SIGSEGV:       normal
	Notification:  Thread + polling
	Architecture:  amd64
	Disabled:      none
```

```
% /Applications/Unity/Hub/Editor/2019.1.9f1/Unity.app/Contents/MonoBleedingEdge/bin/mono --version
Mono JIT compiler version 5.11.0 ((HEAD/2e8093bfb03 Thu Jun 13 15:51:36 GMT 2019)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
	TLS:           normal
	SIGSEGV:       altstack
	Notification:  kqueue
	Architecture:  amd64
	Disabled:      shared_perfcounters
	Misc:          softdebug 
	Interpreter:   yes
	LLVM:          supported, not enabled.
	GC:            sgen (concurrent by default)
```

monoの2.6.5が .Net3.5 で、5.11.0が.Net4.xで使われているmonoになるのかな？

バージョンがわかったのでそれぞれのRegex.csファイルをみる。

- 2.6.5は[mono-2.6.4](https://github.com/Unity-Technologies/mono/releases/tag/mono-2.6.4)をみる
  - releaseに同じ番号がなかったので近いバージョン番号のものにした。
- 5.11.0は[最新のRegex.cs](https://github.com/Unity-Technologies/mono/blob/07adebd8bf94f2c4310421460940fd6c873b24d5/mcs/class/referencesource/System/regex/system/text/regularexpressions/Regex.cs)をみる。
  - releaseでバージョン番号が付けられている最新が mono-4.8.0.374 だったので 5.11.0 を探すのを諦めた。

### 実装比較
.Net3.5 の方をみると次のような実装になっていた。

```csharp:Regex.cs(一部抜粋)
		public Regex (string pattern) : this (pattern, RegexOptions.None)
		{
		}

		public Regex (string pattern, RegexOptions options)
		{
			if (pattern == null)
				throw new ArgumentNullException ("pattern");
			validate_options (options);
			this.pattern = pattern;
			this.roptions = options;
			Init ();
		}
:
: 省略
:

#if !TARGET_JVM
		private void Init ()
		{
			this.machineFactory = cache.Lookup (this.pattern, this.roptions);
```

最新版の方をみる。

```csharp:Regex.cs(一部抜粋)

        public Regex(String pattern)
            : this(pattern, RegexOptions.None, DefaultMatchTimeout, false) {
        }
:
: 省略
:
        private Regex(String pattern, RegexOptions options, TimeSpan matchTimeout, bool useCache) {
:
: 省略
:
                if (useCache)
                    cached = CacheCode(key);
```

- .Net3.5のほうはコンストラクタ内でキャッシュ機構があり、同じパターンに対してはキャッシュが効く。
- 最新版の方はキャッシュ機構があるにはあるが、使われていなかった。
  - private のコンストラクタに bool useCache で指定ができるがこちらを true にするpublicなコンストラクタがなかった

.Net3.5では同じパターンに対してはキャッシュ機構が働いて初期化処理が軽かったが.Net4.xはキャッシュ機構がないため処理が重くなっていた。

## コードの改良
独自にキャッシュをすれば .Net3.5 と .Net4.x で速度はそこまで変わらないものにできるはずなので試す。

```csharp:MyCachedRegex.cs
using System.Text.RegularExpressions;
using UnityEngine;

public class MyCachedRegex : MonoBehaviour
{
    Regex regex = new System.Text.RegularExpressions.Regex("^[A-Za-z0-9]+$");

    void Start()
    {
        var str = "hogehoge";
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        for (var i = 0; i < 10000; i++)
        {
            regex.Match(str);
        }
        sw.Stop();
        Debug.Log("cached time : " + sw.ElapsedMilliseconds);
    }
}
```

### 改良後の結果


```.Net3.5
cached time : 31
```

```.Net4.x
cached time : 17
```

.Net4.x の処理速度が .Net3.5 より早くなることが確認できた。

## まとめ
.Net4.x のときに Regex クラスを使うときは同じパターンを何度も使う場合はキャッシュして使うようにしたい。

.Net3.5 から .Net4.x に変更しただけで既存のコードで負荷がかかる箇所が変わっている可能性があるので注意したい。