RiderのIL Viewer(Low-Level C#)からコンパイル後どういう実装が行われているかを知る事でC#を更に理解していきたいと思います。
Low-Level C#とは?
簡単に説明すると、コンパイル時に生成されるILコードが分からなくてもILコードをC#に変換すれば理解しやすくて便利だよね。という機能です。
調査環境
- Unity : 2021.3.15f1 (Sillicon)
- パフォーマンスの確認 : Unity Profilerを使いEditor上で確認
- .NET Framework v4.7.1
- C# 9.0
文字列
+
演算子
内部ではstring.Concat
に変換されていました。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
var num = 1;
var msg = "Number is " + num;
}
// [変換後] Low-Level C#
private void Start()
{
string.Concat("Number is ", 1.ToString());
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
var num = 1;
var msg = "Number is " + num;
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
string.Concat("Number is ", 1.ToString());
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.method private hidebysig instance void
Start() cil managed
{
.maxstack 2
.locals init (
[0] int32 num
)
// [13 9 - 13 21]
IL_0000: ldc.i4.1
IL_0001: stloc.0 // num
// [14 9 - 14 38]
IL_0002: ldstr "Number is "
IL_0007: ldloca.s num
IL_0009: call instance string [mscorlib]System.Int32::ToString()
IL_000e: call string [mscorlib]System.String::Concat(string, string)
IL_0013: pop
// [15 5 - 15 6]
IL_0014: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
$
文字列補間
内部ではstring.Format
に変換されていました。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
var num = 1;
var msg = $"Number is {num}";
}
// [変換後] Low-Level C#
private void Start()
{
string.Format("Number is {0}", (object) 1);
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
var num = 1;
var msg = $"Number is {num}";
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
string.Format("Number is {0}", (object) 1);
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.method private hidebysig instance void
Start() cil managed
{
.maxstack 2
.locals init (
[0] int32 num
)
// [13 9 - 13 21]
IL_0000: ldc.i4.1
IL_0001: stloc.0 // num
// [14 9 - 14 38]
IL_0002: ldstr "Number is {0}"
IL_0007: ldloc.0 // num
IL_0008: box [mscorlib]System.Int32
IL_000d: call string [mscorlib]System.String::Format(string, object)
IL_0012: pop
// [15 5 - 15 6]
IL_0013: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
気になったのでパフォーマンスチェック
+
での連結と$
での文字列補間で内部の実装が違うので、変数の埋め込みと文字列の連結をテストしてみました。
テストコード
public sealed class StringTest : MonoBehaviour
{
void Update()
{
Profiler.BeginSample("string.Format");
for (var num = 1; num <= 100; num++)
{
string.Format("Number is {0}", num);
}
Profiler.EndSample();
Profiler.BeginSample("string.Concat");
for (var num = 1; num <= 100; num++)
{
string.Concat("Number is", num.ToString());
}
Profiler.EndSample();
Profiler.BeginSample("[coupling] string.Format");
{
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
stg = string.Format("{0}{1}", stg, "Number is 1");
}
}
Profiler.EndSample();
Profiler.BeginSample("[coupling] string.Concat");
{
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
stg = string.Concat(stg, "Number is 1");
}
}
Profiler.EndSample();
}
}
テスト結果
変数の埋め込みテスト
GC Alloc | Time ms | |
---|---|---|
string.Format |
6.5KB | 0.28ms |
string.Concat |
6.8KB | 0.19ms |
文字列の連結テスト
GC Alloc | Time ms | |
---|---|---|
string.Format |
340.7KB | 0.73ms |
string.Concat |
110.6KB | 0.13ms |
結論
変数の埋め込み
パフォーマンス的にはstring.Concat
が若干早いです。テスト結果から逸れますが、$
での文字列補間は、フォーマットが間違っていた場合コンパイルエラーとして検出してくれるのでフォーマットを使う場合は優先したいです。
文字列の連結
パフォーマンス的に、+
で繋げた方が良さそうです。
文字列の参考
// これはダメ
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
stg = $"{stg} Message";
}
// こっちの方が良い
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
stg += " Message";
}
// フォーマットを使うなら安全性を優先してこんな感じで書きたい
var stg = string.Empty;
for (var num = 1; num <= 1000; num++)
{
stg += $" Number is {num:#,0}";
}
// フォーマットを使わないならこれが最速
var stg = string.Empty;
for (var num = 1; num <= 1000; num++)
{
stg += "Number is " + num;
}
今回は、2つの方法を比較したかったので除外しましたが、頻繁に文字列の連結が必要ならStringBuilderを検討した方が良いです。
暗黙的変換
Vector2
の変数にVector3
を代入するとVector2
に自動的に変換してくれます。
内部では、Vector2
に定義されたimplicit operator
を呼んでいるようです。
public sealed class TestClass : MonoBehaviour
{
Vector2 _pos;
// [元のコード]
void Start()
{
_pos = transform.position; // Vector3 to Vector2
}
// [変換後] Low-Level C#
void Start()
{
this._pos = Vector2.op_Implicit(this.transform.position);
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
Vector2 _pos;
void Start()
{
_pos = transform.position;
}
}
public sealed class TestClass : MonoBehaviour
{
private Vector2 _pos;
private void Start()
{
this._pos = Vector2.op_Implicit(this.transform.position);
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.field private valuetype [UnityEngine]UnityEngine.Vector2 _pos
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [15 9 - 15 35]
IL_0000: ldarg.0 // this
IL_0001: ldarg.0 // this
IL_0002: call instance class [UnityEngine]UnityEngine.Transform [UnityEngine]UnityEngine.Component::get_transform()
IL_0007: callvirt instance valuetype [UnityEngine]UnityEngine.Vector3 [UnityEngine]UnityEngine.Transform::get_position()
IL_000c: call valuetype [UnityEngine]UnityEngine.Vector2 [UnityEngine]UnityEngine.Vector2::op_Implicit(valuetype [UnityEngine]UnityEngine.Vector3)
IL_0011: stfld valuetype [UnityEngine]UnityEngine.Vector2 TestClass::_pos
// [16 5 - 16 6]
IL_0016: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
Vector2.op_Implicit
の中身
implicit operatorのようです。内部を見てみるとnew Vector2
を実行している事が分かりました。
new
する度にほんの僅かですがコストがかかるので、例えば2Dゲームでもtransform.position
への代入はVector3
をメインに使った方が良さそうです。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Vector2(Vector3 v) => new Vector2(v.x, v.y);
Coroutine
Coroutine
を使うと内部で自動生成されたIEnumerator
を実装したクラスを使用している事が分かります。これによりCoroutine
を使う度にGC Allocが発生します。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
StartCoroutine(Process(1));
}
IEnumerator Process(float seconds)
{
yield return new WaitForSeconds(seconds);
Debug.Log("Completed");
}
// [変換後] Low-Level C#
[IteratorStateMachine(typeof (TestClass.<Process>d__1))]
private IEnumerator Process(float seconds)
{
// 自動生成されたクラスをインスタンス化している
TestClass.<Process>d__1 processD1 = new TestClass.<Process>d__1(0);
processD1.seconds = seconds;
return (IEnumerator) processD1;
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
StartCoroutine(Process(1));
}
IEnumerator Process(float seconds)
{
yield return new WaitForSeconds(seconds);
Debug.Log("Completed");
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
this.StartCoroutine(this.Process(1f));
}
[IteratorStateMachine(typeof (TestClass.<Process>d__1))]
private IEnumerator Process(float seconds)
{
TestClass.<Process>d__1 processD1 = new TestClass.<Process>d__1(0);
processD1.seconds = seconds;
return (IEnumerator) processD1;
}
public TestClass()
{
base..ctor();
}
[CompilerGenerated]
private sealed class <Process>d__1 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public float seconds;
[DebuggerHidden]
public <Process>d__1(int _param1)
{
base..ctor();
this.<>1__state = _param1;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
bool IEnumerator.MoveNext()
{
int num = this.<>1__state;
if (num != 0)
{
if (num != 1)
return false;
this.<>1__state = -1;
Debug.Log((object) "Completed");
return false;
}
this.<>1__state = -1;
this.<>2__current = (object) new WaitForSeconds(this.seconds);
this.<>1__state = 1;
return true;
}
object IEnumerator<object>.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
object IEnumerator.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.class nested private sealed auto ansi beforefieldinit
'<Process>d__1'
extends [mscorlib]System.Object
implements
class [mscorlib]System.Collections.Generic.IEnumerator`1<object>,
[mscorlib]System.Collections.IEnumerator,
[mscorlib]System.IDisposable
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field private int32 '<>1__state'
.field private object '<>2__current'
.field public float32 seconds
.method public hidebysig specialname rtspecialname instance void
.ctor(
int32 '<>1__state'
) cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0 // this
IL_0007: ldarg.1 // '<>1__state'
IL_0008: stfld int32 TestClass/'<Process>d__1'::'<>1__state'
IL_000d: ret
} // end of method '<Process>d__1'::.ctor
.method private final hidebysig virtual newslot instance void
System.IDisposable.Dispose() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
= (01 00 00 00 )
.override method instance void [mscorlib]System.IDisposable::Dispose()
.maxstack 8
IL_0000: ret
} // end of method '<Process>d__1'::System.IDisposable.Dispose
.method private final hidebysig virtual newslot instance bool
MoveNext() cil managed
{
.override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
.maxstack 2
.locals init (
[0] int32 V_0
)
IL_0000: ldarg.0 // this
IL_0001: ldfld int32 TestClass/'<Process>d__1'::'<>1__state'
IL_0006: stloc.0 // V_0
IL_0007: ldloc.0 // V_0
IL_0008: brfalse.s IL_0010
IL_000a: ldloc.0 // V_0
IL_000b: ldc.i4.1
IL_000c: beq.s IL_0031
IL_000e: ldc.i4.0
IL_000f: ret
IL_0010: ldarg.0 // this
IL_0011: ldc.i4.m1
IL_0012: stfld int32 TestClass/'<Process>d__1'::'<>1__state'
// [18 9 - 18 50]
IL_0017: ldarg.0 // this
IL_0018: ldarg.0 // this
IL_0019: ldfld float32 TestClass/'<Process>d__1'::seconds
IL_001e: newobj instance void [UnityEngine]UnityEngine.WaitForSeconds::.ctor(float32)
IL_0023: stfld object TestClass/'<Process>d__1'::'<>2__current'
IL_0028: ldarg.0 // this
IL_0029: ldc.i4.1
IL_002a: stfld int32 TestClass/'<Process>d__1'::'<>1__state'
IL_002f: ldc.i4.1
IL_0030: ret
IL_0031: ldarg.0 // this
IL_0032: ldc.i4.m1
IL_0033: stfld int32 TestClass/'<Process>d__1'::'<>1__state'
// [19 9 - 19 32]
IL_0038: ldstr "Completed"
IL_003d: call void [UnityEngine]UnityEngine.Debug::Log(object)
// [20 5 - 20 6]
IL_0042: ldc.i4.0
IL_0043: ret
} // end of method '<Process>d__1'::MoveNext
.method private final hidebysig virtual newslot specialname instance object
'System.Collections.Generic.IEnumerator<System.Object>.get_Current'() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
= (01 00 00 00 )
.override method instance !0/*T*/ class [mscorlib]System.Collections.Generic.IEnumerator`1<object>::get_Current()
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: ldfld object TestClass/'<Process>d__1'::'<>2__current'
IL_0006: ret
} // end of method '<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.get_Current'
.method private final hidebysig virtual newslot instance void
System.Collections.IEnumerator.Reset() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
= (01 00 00 00 )
.override method instance void [mscorlib]System.Collections.IEnumerator::Reset()
.maxstack 8
IL_0000: newobj instance void [mscorlib]System.NotSupportedException::.ctor()
IL_0005: throw
} // end of method '<Process>d__1'::System.Collections.IEnumerator.Reset
.method private final hidebysig virtual newslot specialname instance object
System.Collections.IEnumerator.get_Current() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
= (01 00 00 00 )
.override method instance object [mscorlib]System.Collections.IEnumerator::get_Current()
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: ldfld object TestClass/'<Process>d__1'::'<>2__current'
IL_0006: ret
} // end of method '<Process>d__1'::System.Collections.IEnumerator.get_Current
.property instance object 'System.Collections.Generic.IEnumerator<System.Object>.Current'()
{
.get instance object TestClass/'<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.get_Current'()
} // end of property '<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.Current'
.property instance object 'System.Collections.IEnumerator.Current'()
{
.get instance object TestClass/'<Process>d__1'::System.Collections.IEnumerator.get_Current()
} // end of property '<Process>d__1'::System.Collections.IEnumerator.Current
} // end of class '<Process>d__1'
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [13 9 - 13 36]
IL_0000: ldarg.0 // this
IL_0001: ldarg.0 // this
IL_0002: ldc.r4 1
IL_0007: call instance class [mscorlib]System.Collections.IEnumerator TestClass::Process(float32)
IL_000c: call instance class [UnityEngine]UnityEngine.Coroutine [UnityEngine]UnityEngine.MonoBehaviour::StartCoroutine(class [mscorlib]System.Collections.IEnumerator)
IL_0011: pop
// [14 5 - 14 6]
IL_0012: ret
} // end of method TestClass::Start
.method private hidebysig instance class [mscorlib]System.Collections.IEnumerator
Process(
float32 seconds
) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type)
= (
01 00 17 54 65 73 74 43 6c 61 73 73 2b 3c 50 72 // ...TestClass+<Pr
6f 63 65 73 73 3e 64 5f 5f 31 00 00 // ocess>d__1..
)
// type(class TestClass/'<Process>d__1')
.maxstack 8
IL_0000: ldc.i4.0
IL_0001: newobj instance void TestClass/'<Process>d__1'::.ctor(int32)
IL_0006: dup
IL_0007: ldarg.1 // seconds
IL_0008: stfld float32 TestClass/'<Process>d__1'::seconds
IL_000d: ret
} // end of method TestClass::Process
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
CoroutineのGC最適化
GC Allocの発生を抑えるコードを書いてみましたが、実用的では無いですし0 Allocで書けなかったので残念です。掲載するか迷いましたが、今後の自分の為に掲載します。
テスト結果
※ 下記の結果は2回目以降の数値です。
GC Alloc | Time ms | |
---|---|---|
IEnumerator |
84KB | 0.01ms |
WaitForSecondsEnumerator |
24KB | 0.00ms |
コードを見る
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
public sealed class StringTest : MonoBehaviour
{
WaitForSecondsEnumerator _waitForseconds = new WaitForSecondsEnumerator();
void Update()
{
Profiler.BeginSample("Process");
StartCoroutine(Process(1));
Profiler.EndSample();
Profiler.BeginSample("Process fast");
StartCoroutine(Process2(1));
Profiler.EndSample();
}
IEnumerator Process(float seconds)
{
yield return new WaitForSeconds(seconds);
Debug.Log("Completed");
}
IEnumerator Process2(float seconds)
{
return _waitForseconds.Start(seconds, () =>
{
Debug.Log("Completed");
});
}
class WaitForSecondsEnumerator : IEnumerator<object>
{
readonly IEnumerator _enumerator;
int _state;
float _seconds;
object _waitForSeconds;
Action _completedAction;
public WaitForSecondsEnumerator() => _enumerator = this;
public IEnumerator Start(float seconds, Action completedAction)
{
_completedAction = completedAction;
_state = 0;
if (Math.Abs(_seconds - seconds) > 0)
{
_seconds = seconds;
_waitForSeconds = new WaitForSeconds(seconds);
}
return _enumerator;
}
bool IEnumerator.MoveNext()
{
if (_state != 0)
{
if (_state != 1)
{
return false;
}
_state = -1;
_completedAction?.Invoke();
return false;
}
_state = 1;
return true;
}
object IEnumerator<object>.Current => _waitForSeconds;
object IEnumerator.Current => _waitForSeconds;
void IEnumerator.Reset() => throw new NotSupportedException();
void IDisposable.Dispose() {}
}
}
ボックス化
値型を参照型であるobject
型にキャストすると値型から参照型へ変換するボックス化が発生します。
身近なところで言うとDebug.Log
に数値などの値型を入れるとボックス化が発生します。それだけでは無く重い処理の為なるべくDebug.Log
はコードに残さない方が良いです。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
Debug.Log(1);
}
// [変換後] Low-Level C#
void Start()
{
// object型にキャストされている
Debug.Log((object) 1);
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
Debug.Log(1);
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
Debug.Log((object) 1);
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [13 9 - 13 22]
IL_0000: ldc.i4.1
IL_0001: box [mscorlib]System.Int32
IL_0006: call void [UnityEngine]UnityEngine.Debug::Log(object)
// [14 5 - 14 6]
IL_000b: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
Debug.Log
を残したい
値の確認やエラーチェックなど、何らかの理由でDebug.Log
を残したい事もあると思います。その場合、Development Buildのみ有効にするのをおすすめします。下記はすべてDevelopment Buildのみで機能します。
using System.Diagnostics;
using UnityEngine;
using UnityEngine.Assertions;
public sealed class TestClass : MonoBehaviour
{
[SerializeField] GameObject _obj;
void Start()
{
// 1) 問題があればメッセージを表示したい場合は、Assertを使う
// _objがnullの場合、エラーメッセージ表示
Assert.IsNotNull(_obj, "InspectorからObjが設定されていません");
// 2) Conditional属性を付与したメソッドを自作する
PrintLog("Message");
// 3) ディレクティブを使う
#if DEBUG
Debug.Log("Message");
#endif
}
[Conditional("DEBUG")]
void PrintLog(string message) => Debug.Log(message);
}
ラムダ式
ローカル変数をラムダ式内で使うと、GCゴミが発生します。
理由は内部でクラスが自動生成されていて、そのクラスを利用する為に発生します。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
var msg = "Completed";
Do(result =>
{
Debug.Log(result + " " + msg);
});
}
// [変換後] Low-Level C#
private void Start()
{
// 自動実装されたクラスがインスタンス化されている
TestClass.<>c__DisplayClass0_0 cDisplayClass00 = new TestClass.<>c__DisplayClass0_0();
cDisplayClass00.msg = "Completed";
this.Do(new Action<string>((object) cDisplayClass00, __methodptr(<Start>b__0)));
}
void Do(Action<string> completedAction)
{
completedAction("Result");
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
var msg = "Completed";
Do(result =>
{
Debug.Log(result + " " + msg);
});
}
void Do(Action<string> completedAction)
{
completedAction("Result");
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
TestClass.<>c__DisplayClass0_0 cDisplayClass00 = new TestClass.<>c__DisplayClass0_0();
cDisplayClass00.msg = "Completed";
this.Do(new Action<string>((object) cDisplayClass00, __methodptr(<Start>b__0)));
}
private void Do(Action<string> completedAction)
{
completedAction("Result");
}
public TestClass()
{
base..ctor();
}
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public string msg;
public <>c__DisplayClass0_0()
{
base..ctor();
}
internal void <Start>b__0(string result)
{
Debug.Log((object) string.Concat(result, " ", this.msg));
}
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.class nested private sealed auto ansi beforefieldinit
'<>c__DisplayClass0_0'
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field public string msg
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method '<>c__DisplayClass0_0'::.ctor
.method assembly hidebysig instance void
'<Start>b__0'(
string result
) cil managed
{
.maxstack 8
// [16 13 - 16 44]
IL_0000: ldarg.1 // result
IL_0001: ldstr " "
IL_0006: ldarg.0 // this
IL_0007: ldfld string TestClass/'<>c__DisplayClass0_0'::msg
IL_000c: call string [mscorlib]System.String::Concat(string, string, string)
IL_0011: call void [UnityEngine]UnityEngine.Debug::Log(object)
// [17 9 - 17 10]
IL_0016: ret
} // end of method '<>c__DisplayClass0_0'::'<Start>b__0'
} // end of class '<>c__DisplayClass0_0'
.method private hidebysig instance void
Start() cil managed
{
.maxstack 3
.locals init (
[0] class TestClass/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
)
IL_0000: newobj instance void TestClass/'<>c__DisplayClass0_0'::.ctor()
IL_0005: stloc.0 // 'CS$<>8__locals0'
// [13 9 - 13 31]
IL_0006: ldloc.0 // 'CS$<>8__locals0'
IL_0007: ldstr "Completed"
IL_000c: stfld string TestClass/'<>c__DisplayClass0_0'::msg
// [14 9 - 17 12]
IL_0011: ldarg.0 // this
IL_0012: ldloc.0 // 'CS$<>8__locals0'
IL_0013: ldftn instance void TestClass/'<>c__DisplayClass0_0'::'<Start>b__0'(string)
IL_0019: newobj instance void class [mscorlib]System.Action`1<string>::.ctor(object, native int)
IL_001e: call instance void TestClass::Do(class [mscorlib]System.Action`1<string>)
// [18 5 - 18 6]
IL_0023: ret
} // end of method TestClass::Start
.method private hidebysig instance void
Do(
class [mscorlib]System.Action`1<string> completedAction
) cil managed
{
.maxstack 8
// [22 9 - 22 35]
IL_0000: ldarg.1 // completedAction
IL_0001: ldstr "Result"
IL_0006: callvirt instance void class [mscorlib]System.Action`1<string>::Invoke(!0/*string*/)
// [23 5 - 23 6]
IL_000b: ret
} // end of method TestClass::Do
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
ラムダ式内にローカル変数を移動できるのであれば、自動生成されたクラスがキャッシュされるので2回目以降使い回してくれます。
public sealed class TestClass : MonoBehaviour
{
// [元のコード]
void Start()
{
Do(result =>
{
var msg = "Completed";
Debug.Log($"{result} {msg}");
});
}
// [変換後] Low-Level C#
private void Start()
{
// TestClass.<>c.<>9__0_0変数にキャッシュされる
this.Do(TestClass.<>c.<>9__0_0 ?? (TestClass.<>c.<>9__0_0 = new Action<string>((object) TestClass.<>c.<>9, __methodptr(<Start>b__0_0))));
}
void Do(Action<string> completedAction)
{
completedAction("Result");
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
Do(result =>
{
var msg = "Completed";
Debug.Log($"{result} {msg}");
});
}
void Do(Action<string> completedAction)
{
completedAction("Result");
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
this.Do(TestClass.<>c.<>9__0_0 ?? (TestClass.<>c.<>9__0_0 = new Action<string>((object) TestClass.<>c.<>9, __methodptr(<Start>b__0_0))));
}
private void Do(Action<string> completedAction)
{
completedAction("Result");
}
public TestClass()
{
base..ctor();
}
[CompilerGenerated]
[Serializable]
private sealed class <>c
{
public static readonly TestClass.<>c <>9;
public static Action<string> <>9__0_0;
static <>c()
{
TestClass.<>c.<>9 = new TestClass.<>c();
}
public <>c()
{
base..ctor();
}
internal void <Start>b__0_0(string result)
{
string str2 = "Completed";
Debug.Log((object) string.Concat(result, " ", str2));
}
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.class nested private sealed auto ansi serializable beforefieldinit
'<>c'
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field public static initonly class TestClass/'<>c' '<>9'
.field public static class [mscorlib]System.Action`1<string> '<>9__0_0'
.method private hidebysig static specialname rtspecialname void
.cctor() cil managed
{
.maxstack 8
IL_0000: newobj instance void TestClass/'<>c'::.ctor()
IL_0005: stsfld class TestClass/'<>c' TestClass/'<>c'::'<>9'
IL_000a: ret
} // end of method '<>c'::.cctor
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method '<>c'::.ctor
.method assembly hidebysig instance void
'<Start>b__0_0'(
string result
) cil managed
{
.maxstack 3
.locals init (
[0] string msg
)
// [15 13 - 15 35]
IL_0000: ldstr "Completed"
IL_0005: stloc.0 // msg
// [16 13 - 16 42]
IL_0006: ldarg.1 // result
IL_0007: ldstr " "
IL_000c: ldloc.0 // msg
IL_000d: call string [mscorlib]System.String::Concat(string, string, string)
IL_0012: call void [UnityEngine]UnityEngine.Debug::Log(object)
// [17 9 - 17 10]
IL_0017: ret
} // end of method '<>c'::'<Start>b__0_0'
} // end of class '<>c'
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [13 9 - 17 12]
IL_0000: ldarg.0 // this
IL_0001: ldsfld class [mscorlib]System.Action`1<string> TestClass/'<>c'::'<>9__0_0'
IL_0006: dup
IL_0007: brtrue.s IL_0020
IL_0009: pop
IL_000a: ldsfld class TestClass/'<>c' TestClass/'<>c'::'<>9'
IL_000f: ldftn instance void TestClass/'<>c'::'<Start>b__0_0'(string)
IL_0015: newobj instance void class [mscorlib]System.Action`1<string>::.ctor(object, native int)
IL_001a: dup
IL_001b: stsfld class [mscorlib]System.Action`1<string> TestClass/'<>c'::'<>9__0_0'
IL_0020: call instance void TestClass::Do(class [mscorlib]System.Action`1<string>)
// [18 5 - 18 6]
IL_0025: ret
} // end of method TestClass::Start
.method private hidebysig instance void
Do(
class [mscorlib]System.Action`1<string> completedAction
) cil managed
{
.maxstack 8
// [22 9 - 22 35]
IL_0000: ldarg.1 // completedAction
IL_0001: ldstr "Result"
IL_0006: callvirt instance void class [mscorlib]System.Action`1<string>::Invoke(!0/*string*/)
// [23 5 - 23 6]
IL_000b: ret
} // end of method TestClass::Do
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
結論
ローカル変数はできる限りラムダ式内に書くのがパフォーマンスが良い。
void Start()
{
var msg = "Completed"; // x
Do(result =>
{
// ○ ラムダ式内に書いた方がパフォーマンスが良い
var msg = "Completed";
Debug.Log($"{result} {msg}");
});
}
タプル
タプルは内部でValueTuple
に変換してくれるようです。
引数が少ないメソッドやシグネイチャの変更が発生しても問題無い場合は、積極的に使っていきたいです。ValueTuple
は値型なのでモバイルでパフォーマンスを優先したい箇所でも使えそうです。
public sealed class TestClass : MonoBehaviour
{
void Start()
{
var a = Get();
}
// [元のコード]
(int Number, string String) Get()
{
return (1, "1");
}
// [変換後] Low-Level C#
[return: TupleElementNames(new string[] {"Number", "String"})]
private ValueTuple<int, string> Get()
{
return new ValueTuple<int, string>(1, "1");
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
void Start()
{
var a = Get();
}
(int Number, string String) Get()
{
return (1, "1");
}
}
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
this.Get();
}
[return: TupleElementNames(new string[] {"Number", "String"})]
private ValueTuple<int, string> Get()
{
return new ValueTuple<int, string>(1, "1");
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [13 9 - 13 23]
IL_0000: ldarg.0 // this
IL_0001: call instance valuetype [mscorlib]System.ValueTuple`2<int32, string> TestClass::Get()
IL_0006: pop
// [14 5 - 14 6]
IL_0007: ret
} // end of method TestClass::Start
.method private hidebysig instance valuetype [mscorlib]System.ValueTuple`2<int32, string>
Get() cil managed
{
.param [0]
.custom instance void [mscorlib]System.Runtime.CompilerServices.TupleElementNamesAttribute::.ctor(string[])
= (
01 00 02 00 00 00 06 4e 75 6d 62 65 72 06 53 74 // .......Number.St
72 69 6e 67 00 00 // ring..
)
// string[2]
/*( string('Number') string('String') )*/
.maxstack 8
// [18 9 - 18 25]
IL_0000: ldc.i4.1
IL_0001: ldstr "1"
IL_0006: newobj instance void valuetype [mscorlib]System.ValueTuple`2<int32, string>::.ctor(!0/*int32*/, !1/*string*/)
IL_000b: ret
} // end of method TestClass::Get
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
Enum
Enum
は++
でインクリメントできます。中身を確認するとint
に置き換わっている事が分かりました。
public sealed class TestClass : MonoBehaviour
{
enum Enum {E1, E2, E3}
// [元のコード]
void Start()
{
var key = Enum.E1;
key++;
}
// [変換後] Low-Level C#
private void Start()
{
// ただのintの足し算に変換されている
int num = 0 + 1;
}
}
そして、数値が振られていない場合、自動的に0から始まる連番で初期化されるようです。
下記のようなイメージです。
// 元のコード
enum Enum {E1, E2, E3}
// ↓ コンパイル後、自動的に連番が振られる
enum Enum {E1 = 0, E2 = 1, E3 = 2}
内部を知れば下記の意味が理解できます。
"列挙型" は、基になる整数値型の一連の名前付き定数によって定義された値の型です。
(Microsoft リファレンスより)
コード全文
public sealed class TestClass : MonoBehaviour
{
enum Enum {E1, E2, E3}
void Start()
{
var key = Enum.E1;
key++;
}
}
※ 自動的に整数が振られるのはLow-Level C#では確認出来なかったのでILコードを確認ください。
public sealed class TestClass : MonoBehaviour
{
private void Start()
{
int num = 0 + 1;
}
public TestClass()
{
base..ctor();
}
private enum Enum {E1, E2, E3}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.class nested private sealed auto ansi
Enum
extends [mscorlib]System.Enum
{
.field public specialname rtspecialname int32 value__
.field public static literal valuetype TestClass/Enum E1 = int32(0) // 0x00000000
.field public static literal valuetype TestClass/Enum E2 = int32(1) // 0x00000001
.field public static literal valuetype TestClass/Enum E3 = int32(2) // 0x00000002
} // end of class Enum
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [15 9 - 15 27]
IL_0000: ldc.i4.0
// [16 9 - 16 15]
IL_0001: ldc.i4.1
IL_0002: add
IL_0003: pop
// [17 5 - 17 6]
IL_0004: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
Enum
をシリアライズして使う場合、正しく値を変更しないとおかしくなります。
変更前
public sealed class EnumTest : MonoBehaviour
{
[SerializeField] Enum _enum;
public enum Enum {E1, E2, E3}
}
変更後
E1
の前にE4
を追加すると、選択されている値がE4
に変わってしまいます。シリアライズするとenum
の名称は無視され値の整数しか見ない為にこの問題が発生します。
public sealed class EnumTest : MonoBehaviour
{
[SerializeField] Enum _enum;
public enum Enum {E4, E1, E2, E3}
}
どうしたら良いか?
下記のように定義するのがおすすめです。自動で振られる連番をあえて自分で振って、強い意志をアピールするのが効果的です。
/// <summary>
/// アイテムの状態
/// ※ 数値の変更禁止
/// </summary>
public enum ItemState
{
Unknown = 0,
Locked = 1,
Unlocked = 2
}
- セーブデータとして
enum
を保存する際も必ずシリアライズが必要になり同じ問題が発生するので注意が必要です。 - 個人的には外部に公開する場合や、インスペクタに表示する場合は整数を振るようにしてます。
GameObject
のnull
チェック
Unityの==
演算子は特殊なので内部でどうなっているのか調べてみました。
==でnullチェック
内部的には、Object.op_Equality
で比較している事が分かりました。
public sealed class TestClass : MonoBehaviour
{
GameObject _obj;
// [元のコード]
void Start()
{
if (_obj == null)
{
_obj = GetComponentInChildren<Transform>().gameObject;
}
}
// [変換後] Low-Level C#
void Start()
{
if (!Object.op_Equality((Object) this._obj, (Object) null))
return;
this._obj = this.GetComponentInChildren<Transform>().gameObject;
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
GameObject _obj;
void Start()
{
if (_obj == null)
{
_obj = GetComponentInChildren<Transform>().gameObject;
}
}
}
public sealed class TestClass : MonoBehaviour
{
private GameObject _obj;
private void Start()
{
if (!Object.op_Equality((Object) this._obj, (Object) null))
return;
this._obj = this.GetComponentInChildren<Transform>().gameObject;
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.field private class [UnityEngine]UnityEngine.GameObject _obj
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [15 9 - 15 26]
IL_0000: ldarg.0 // this
IL_0001: ldfld class [UnityEngine]UnityEngine.GameObject TestClass::_obj
IL_0006: ldnull
IL_0007: call bool [UnityEngine]UnityEngine.Object::op_Equality(class [UnityEngine]UnityEngine.Object, class [UnityEngine]UnityEngine.Object)
IL_000c: brfalse.s IL_001f
// [17 13 - 17 67]
IL_000e: ldarg.0 // this
IL_000f: ldarg.0 // this
IL_0010: call instance !!0/*class [UnityEngine]UnityEngine.Transform*/ [UnityEngine]UnityEngine.Component::GetComponentInChildren<class [UnityEngine]UnityEngine.Transform>()
IL_0015: callvirt instance class [UnityEngine]UnityEngine.GameObject [UnityEngine]UnityEngine.Component::get_gameObject()
IL_001a: stfld class [UnityEngine]UnityEngine.GameObject TestClass::_obj
// [19 5 - 19 6]
IL_001f: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
Object.op_Equality
の中身
operator ==のようです。 内部では、Object.IsNativeObjectAlive
でGameObject
がシーン上に存在するか確認する処理が実行されています。
// Object.op_Equality
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);
private static bool CompareBaseObjects(Object lhs, Object rhs)
{
bool flag1 = (object) lhs == null;
bool flag2 = (object) rhs == null;
if (flag2 & flag1)
return true;
if (flag2)
return !Object.IsNativeObjectAlive(lhs);
return flag1 ? !Object.IsNativeObjectAlive(rhs) : lhs.m_InstanceID == rhs.m_InstanceID;
}
null合体演算子での確認
null合体演算子でチェックした場合、先ほどのObject.op_Equality
が使われていません。
public sealed class TestClass : MonoBehaviour
{
GameObject _obj;
void Start()
{
_obj = _obj ?? GetComponentInChildren<Transform>().gameObject;
}
// [変換後] Low-Level C#
void Start()
{
GameObject gameObject = this._obj;
if (gameObject == null) // Object.op_Equalityで確認していない。
gameObject = this.GetComponentInChildren<Transform>().gameObject;
this._obj = gameObject;
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
GameObject _obj;
void Start()
{
_obj = _obj ?? GetComponentInChildren<Transform>().gameObject;
}
}
public sealed class TestClass : MonoBehaviour
{
private GameObject _obj;
private void Start()
{
GameObject gameObject = this._obj;
if (gameObject == null)
gameObject = this.GetComponentInChildren<Transform>().gameObject;
this._obj = gameObject;
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.field private class [UnityEngine]UnityEngine.GameObject _obj
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [15 9 - 15 71]
IL_0000: ldarg.0 // this
IL_0001: ldarg.0 // this
IL_0002: ldfld class [UnityEngine]UnityEngine.GameObject TestClass::_obj
IL_0007: dup
IL_0008: brtrue.s IL_0016
IL_000a: pop
IL_000b: ldarg.0 // this
IL_000c: call instance !!0/*class [UnityEngine]UnityEngine.Transform*/ [UnityEngine]UnityEngine.Component::GetComponentInChildren<class [UnityEngine]UnityEngine.Transform>()
IL_0011: callvirt instance class [UnityEngine]UnityEngine.GameObject [UnityEngine]UnityEngine.Component::get_gameObject()
IL_0016: stfld class [UnityEngine]UnityEngine.GameObject TestClass::_obj
// [16 5 - 16 6]
IL_001b: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
結論
null合体演算子は、GameObject
がシーン上に存在するか確認していない と言う事です。オブジェクトが削除されていても結果がtrue
になる可能性があります。UnityのGameObject
は必ず==
でnull
チェックする必要があります。Null条件演算子も同じです。
Null条件演算子
public sealed class TestClass : MonoBehaviour
{
MyScript _obj; // MyScriptにはMonoBehaviourが継承されています。
// [元のコード]
void Start()
{
var child = _obj?.GetChild();
}
// [変換後] Low-Level C#
private void Start()
{
MyScript myScript = this._obj;
if (myScript == null) // Object.op_Equalityで確認していない。
return;
myScript.GetChild();
}
}
コード全文
public sealed class TestClass : MonoBehaviour
{
MyScript _obj; // MyScriptにはMonoBehaviourが継承されています。
void Start()
{
var child = _obj?.GetChild();
}
}
public sealed class TestClass : MonoBehaviour
{
private MyScript _obj;
private void Start()
{
MyScript myScript = this._obj;
if (myScript == null)
return;
myScript.GetChild();
}
public TestClass()
{
base..ctor();
}
}
.class public sealed auto ansi beforefieldinit
TestClass
extends [UnityEngine]UnityEngine.MonoBehaviour
{
.field private class MyScript _obj
.method private hidebysig instance void
Start() cil managed
{
.maxstack 8
// [15 9 - 15 38]
IL_0000: ldarg.0 // this
IL_0001: ldfld class MyScript TestClass::_obj
IL_0006: dup
IL_0007: brtrue.s IL_000b
IL_0009: pop
IL_000a: ret
IL_000b: call instance class [UnityEngine]UnityEngine.GameObject MyScript::GetChild()
IL_0010: pop
// [16 5 - 16 6]
IL_0011: ret
} // end of method TestClass::Start
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method TestClass::.ctor
} // end of class TestClass
分かり易いように馴染み深いGameObject
で話を進めましたが、UnityEngine.Object
を継承するComponent
クラスはすべて==
でnull
チェックする必要があります。
あとがき
ILコードを読むのはしんどいですが、Low-Level C#であればいつも通りに読めるので解析しやすかったです。あと、今まで知っていた事でも読めるコードにすると理解しやすくて良いですね。今後もパフォーマンスを追い込む必要がある箇所や効率の良い書き方の発掘などで使っていきたいと思います。
最後まで読んでいただきありがとうございました。