C# 8.0で、Mutableな構造体にreadonly
関数メンバーを追加できるようになりました。
それにより、readonly
な構造体のインスタンス(変数、または参照)の防御的なコピーがなくなりますが、ILやJIT Asmがどう変わるかのメモです。
サンプルコード
-
readonly
のあるStruct.ReadOnlyToString()
を呼び出すProgram.R
-
readonly
のないStruct.ToString()
を呼び出すProgram.M
の差を見てみましょう。
using System;
using System.Runtime.CompilerServices;
public struct Struct
{
public long n;
public readonly Guid guid;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public readonly string ReadOnlyToString() => n.ToString();
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public string ToString() => n.ToString();
}
public static class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void R(in Struct s) =>
Console.WriteLine(s.ReadOnlyToString());
[MethodImpl(MethodImplOptions.NoInlining)]
public static void M(in Struct s) =>
Console.WriteLine(s.ToString());
public static void Main()
{
var s = default(Struct);
R(s);
M(s);
}
}
Program.R
およびProgram.M
の引数には in
を付けているので、readonly
の参照になります。
readonly
の変数でも同様になるはずです。
サンプルコードの注意事項
はじめに断って起きますが、わかりやすい結果になるようにサンプルコードは結構恣意的です。
例えば、構造体のサイズを変えたりするとJIT Asmの結果が結構変わります。
CoreCLRのToString()
には既にreadonly
が付いているので、直接呼び出すと思ったような結果にはなりません。
ILの差
readonlyのある関数メンバーの呼び出し
.method public hidebysig static
void R (
[in] valuetype Struct& s
) cil managed noinlining
{
.param [1]
.custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x205d
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance string Struct::ReadOnlyToString()
IL_0006: call void [System.Console]System.Console::WriteLine(string)
IL_000b: ret
} // end of method Program::R
readonlyのない関数メンバーの呼び出し
ldobj Struct
がありコピーしている感じがありますね。
.method public hidebysig static
void M (
[in] valuetype Struct& s
) cil managed noinlining
{
.param [1]
.custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x206c
// Code size 20 (0x14)
.maxstack 1
.locals init (
[0] valuetype Struct
)
IL_0000: ldarg.0
IL_0001: ldobj Struct
IL_0006: stloc.0
IL_0007: ldloca.s 0
IL_0009: call instance string Struct::ToString()
IL_000e: call void [System.Console]System.Console::WriteLine(string)
IL_0013: ret
} // end of method Program::M
JIT Asmの差
sharplabで見る
readonlyのある関数メンバーの呼び出し
非常にシンプルですね。
; Core CLR v4.700.20.26901 on amd64
Program.R(Struct ByRef)
L0000: sub rsp, 0x28
L0004: call Struct.ReadOnlyToString()
L0009: mov rcx, rax
L000c: call System.Console.WriteLine(System.String)
L0011: nop
L0012: add rsp, 0x28
L0016: ret
readonlyのない関数メンバーの呼び出し
コピーが発生してますね。
; Core CLR v4.700.20.26901 on amd64
Program.M(Struct ByRef)
L0000: sub rsp, 0x38
L0004: vzeroupper
L0007: xor eax, eax
L0009: mov [rsp+0x20], rax
L000e: mov [rsp+0x28], rax
L0013: mov [rsp+0x30], rax
L0018: vmovdqu xmm0, [rcx]
L001c: vmovdqu [rsp+0x20], xmm0
L0022: mov rax, [rcx+0x10]
L0026: mov [rsp+0x30], rax
L002b: lea rcx, [rsp+0x20]
L0030: call Struct.ToString()
L0035: mov rcx, rax
L0038: call System.Console.WriteLine(System.String)
L003d: nop
L003e: add rsp, 0x38
L0042: ret
結論
readonly
な参照の構造体に対しては、readonly
関数メンバーを使用すると効率的なアセンブリが生成されました。
余談ですが、in
を付けたreadonly
参照ではなく単にコピーにしても、今回のサンプルコードでは効率的なアセンブリになっていました。
Program.M(Struct)
L0000: sub rsp, 0x28
L0004: call Struct.ToString()
L0009: mov rcx, rax
L000c: call System.Console.WriteLine(System.String)
L0011: nop
L0012: add rsp, 0x28
L0016: ret
構造体でリソースを管理するような設計では値渡しにできないので、readonly
関数メンバーにする必要性はあるでしょう。
(もちろん、不意なメンバーの書き換えに対する予防という面もあります。)