LoginSignup
1

More than 3 years have passed since last update.

[C#] 構造体に readonly関数メンバーを使用した時のIL/JIT Asmの違い

Last updated at Posted at 2020-07-01

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の差

sharplabで見る

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関数メンバーにする必要性はあるでしょう。
(もちろん、不意なメンバーの書き換えに対する予防という面もあります。)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1