えんじにあ雑記!

開発していて学んだことをまとめていきます!

structをusing-statementで使用したらboxingが発生するのか

f:id:flat-M_M:20210912202602p:plain

structをusingステートメントで使用するとboxingされるのかを検証しました。

結論

兎にも角にも調査した結論を。

// これはboxingは発生せず、GC.Allocは0で済む
using (var val = new MyStruct()) {}

この記事の説明

CSharpにはusingステートメントという構文が備わっており、これによりIDisposableオブジェクトのDispose呼び出しを保証することができます。 このusingステートメントはあくまでシンタックスシュガーであり、コンパイル時にはtry-finallyに展開されます。

using ステートメント - C# リファレンス | Microsoft Docs

このusingステートメント内部でIDisposableを実装したstructを使用すると、boxingが発生するのではないか?という疑問が浮かんだので検証しました。

検証環境

  • Unity2020.3.18f1
  • .NET Standard2.0
  • IL2CPP

検証コード

  • IDisposableを実装したstruct
  • IDisposableを実装したstructをIDisposableとして渡す
  • IDisposableを実装したclass

の3パターンで検証しました。下二つのケースはそれぞれboxingとclassのインスタンス化が走るのでGC.Allocが実行されるのは大方予想がつきます。 今回のポイントは1つ目の「IDisposableを実装したstruct」をusingステートメント内で使用するとboxingが走るのかです。

public class StructExperiment : MonoBehaviour
{
    private const int TRY_COUNT = 100_000;
    
    private class Data : IDisposable
    { public void Dispose() { } }

    private struct Value : IDisposable
    { public void Dispose() { } }

    public void StructDispose()
    {
        for (var i = 0; i < TRY_COUNT; i++)
        {
            using (var val = new Value()) { }
        }
    }

    public void IDisposableDispose()
    {
        for (var i = 0; i < TRY_COUNT; i++)
        {
            Dispose(new Value());
        }
    }

    public void ClassDispose()
    {
        for (var i = 0; i < TRY_COUNT; i++)
        {
            using (var data = new Data()) {}
        }
    }

    private void Dispose(IDisposable disposable)
    {
        disposable.Dispose();
    }
}

検証結果

検証項目 結果
IDisposableを実装したstruct GC.Allocなし
IDisposableを実装したstructをIDisposableとして渡す GC.Alloc発生
IDisposableを実装したclass GC.Alloc発生

考察

IDisposableを実装したstructをusingステートメント内部で使うコードから生成されるILをILViewerで確認してみると下記の通りになっていました。

IL_0000: ldloca.s 0
IL_0002: initobj StructExperiment/Value
.try
{
    IL_0008: leave.s IL_0018
} // end .try
finally
{
    // sequence point: hidden
    IL_000a: ldloca.s 0
    IL_000c: constrained. StructExperiment/Value
    IL_0012: callvirt instance void [System.Private.CoreLib]System.IDisposable::Dispose()
    IL_0017: endfinally
} // end handler

ここでのポイントはcallvirt命令の実行直前にconstrainedを呼び出していることにあるかと思います。

The constrained opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr is a value type or a reference type.

公式ドキュメントから引用してきましたが、どうやらconstrained命令を実行することで通常のクラスのインスタンスへのポインタの代わりに構造体へのポインタを受け取ってcallvirtを実行することができるようです。 この工夫がなされているおかげで、構造体をIDisposableにboxingする必要なくDisposeメソッドを呼び出すことが実現できているということでした。

参考リンク

stackoverflow.com

haacked.com

docs.microsoft.com

docs.microsoft.com

stackoverflow.com