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

[C# 8.0で、Mutableな構造体に`readonly`関数メンバーを追加できるようになりました。](https://ufcpp.net/study/csharp/resource/readonlyness/#readonly-member)
それにより、`readonly`な構造体のインスタンス(変数、または参照)の防御的なコピーがなくなりますが、ILやJIT Asmがどう変わるかのメモです。

## サンプルコード

- `readonly`のある`Struct.ReadOnlyToString()`を呼び出す`Program.R`
- `readonly`のない`Struct.ToString()`を呼び出す`Program.M`

の差を見てみましょう。

```csharp
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で見る](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBLANgHwAEAmARgFgAoQgBgAJDSA6AJQFcA7DbAWxiYDCEHgAc8MKAGUJAN2xgYAZwDcVQgGY6ijFDZgMdSTr0YqAbyp0rDTbggcA5nQ6rK1m3VgBDACb3cAJ50AOJs2D50DmE+rpbWANoAsjAYABYQPgCSorgAFMlpGdkiuADyItz2ikwAchCZHLjYHM1O+HQF6Vk55ZUc1XW9vNgAXl59AJQAunFWGp4wvv5BjPQsiz6ljQEAKhBGUK25E3QAvAB8zkx7B0cTsW4JnUU5+SldxWUV2FW19Y3NVp0drPbolIa/QbfHijcY/DjTWYeVZ0G46O5nS4ca77dGOY6uOgAXyoamI6nMSKS7xeJTehTBXz6A3+TRajkRjzmmkYSAYKDoLFyzUMxn0WhOFyR7kYAE5ctV1r4toE0Yd8RN7qSuXRqQzPvSPj1vpDWYCOTMdfNefyOsKOKLdOLFJLLtLrHKFTjbhqtTqkSRiEiLDr3DIvFAtGc6D4YAAzLxsXAYXIHEx+9zuIUuwmZqyJBUZ6wkyhEoA)

### 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](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8gOgCUBXAOwwEt8YLAMIR8AB14AbGFADKMgG68wMXAG4axAMwNcGKBzAYGs/YYw0A3jQa2mOyRC4BzBlw3U79hrGwATJ0kATwYAcQ5ePwZnCL8PGzsAbQBZGAwACwg/AElxSQAKVIys3LFJAHkxPidcFgA5CGyuSV4uVtdCBiLMnLzK6q5ahv7+XgAvbAGASgBdBNttHxh/QJDmRjZlv3LmoIAVCFModvyphgBeAD43FgOjk6n4zyTukrzCtJ7SiqreGvrGs1Wu0GJ1Xr0yiN/sNfvhxpM/lxZvNvOsGHd9A8LtcuLdDpiXKcPAwAL40TSkLRWFEpT5vMofYoQn4DIaAlptFzI54LHTMJBMFAMNj5VomMxGXRnK4orzMACc+Vqm38O2CGOOhKmj3JPIYtKZ30ZXz6v2h7OBXLmesW/MFXVFXHFBkluGl11ldgVSrx9y1Or1KLIpBR1j1XgU2CguguDD8MAAZtgOJIMPkjuYA14vCK3cTs7Zkkqs3YydQSUA=)で見る

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