動機
Unity で .Net3.5 から .Net4.x に変えただけですごく遅くなるコードが見つかった。プロファイリングをすると Regex クラスのコンストラクタの処理が重いということが分かったので調べたことをまとめる。
動作環境
Unity 2019.1.9f1
内容
次のコードをUnity上で .Net3.5 と .Net4.x それぞれで動作させてみる。
コードの内容はRegexクラスを生成して文字列を正規表現にかける処理をループで回しているだけ。
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);
}
}
結果
実行するたびに多少差はでるが、だいたい次のような結果になった。
time : 36
time : 613
.Net4.x が .Net3.5 に比べて20倍ほど遅いのがわかる。
(指定したパターン次第でこの差は変わる)
原因調査
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をみる
- releaseに同じ番号がなかったので近いバージョン番号のものにした。
- 5.11.0は最新のRegex.csをみる。
- releaseでバージョン番号が付けられている最新が mono-4.8.0.374 だったので 5.11.0 を探すのを諦めた。
実装比較
.Net3.5 の方をみると次のような実装になっていた。
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);
最新版の方をみる。
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 で速度はそこまで変わらないものにできるはずなので試す。
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);
}
}
改良後の結果
cached time : 31
cached time : 17
.Net4.x の処理速度が .Net3.5 より早くなることが確認できた。
まとめ
.Net4.x のときに Regex クラスを使うときは同じパターンを何度も使う場合はキャッシュして使うようにしたい。
.Net3.5 から .Net4.x に変更しただけで既存のコードで負荷がかかる箇所が変わっている可能性があるので注意したい。