【C#】Try-Patternまだまだ使える説

C#
このエントリーをはてなブックマークに追加

Try-Patternについて言及したら、ふとTupleでメソッドの返り値を実質複数にするよりTry-Patternで参照(実質ポインター)を使った方が早いんじゃね?と思ってしまったのでベンチマーク取ってみました。

検証対象は

        [MethodImpl(MethodImplOptions.NoInlining)]
        public (bool, int) IsInt(object value)
        {
            if (value is int x)
            {
                return (true, x);
            }
            return (false, default);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public bool TryIsInt(object value, out int result)
        {
            if (value is int x)
            {
                result = x;
                return true;
            }
            result = default;
            return false;
        }

        [Benchmark]
        public int BenchmarkIsInt()
        {
            (bool hasResult, int result) = IsInt(default(int));
            if (hasResult)
            {
                return result;
            }
            return default;
        }

        [Benchmark]
        public int BenchmarkTryIsInt()
        {
            bool hasResult = TryIsInt(default(int), out int result);
            if (hasResult)
            {
                return result;
            }
            return default;
        }

こんな感じのありきたりなTupleでの複数値返しとTry-Patternで複数値返しのメソッドです。ただ、いい例が思いつかなかったのでobjectにbox化したものをunbox castしてますが、同条件なので影響はないでしょう。

ベンチマークはintに加え、16byteなstructと32byteなstructを対象にしてます。16と32byteなstructを用意したのは、構造体で値渡し(返し)するときはだいたい16byte付近が参照渡し(参照返し)とのパフォーマンスの境目だからです。

ベンチマークの結果としては、どの場合でもTry-Patternのほうが早いという結果に。16byteと32byteなstructを含むTupleでは合計16byte over(17 or 33byte)になるので、値を返すTupleのほうが遅くなるかなと思ってましたが、合計5byteとなるbool+intでもTupleのほうが遅いというのは予想外の結果です。

↓実際の結果と検証コード全体です。

いちおう先ほどのint版のコードをsharplabでCIL化してみました。

.class public auto ansi beforefieldinit TryVsTuple
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig 
        instance valuetype [mscorlib]System.ValueTuple`2<bool, int32> IsInt (
            object 'value'
        ) cil managed noinlining 
    {
        // Method begins at RVA 0x2050
        // Code size 33 (0x21)
        .maxstack 2
        .locals init (
            [0] int32,
            [1] object
        )

        IL_0000: ldarg.1
        IL_0001: dup
        IL_0002: stloc.1
        IL_0003: isinst [mscorlib]System.Int32
        IL_0008: brfalse.s IL_0019

        IL_000a: ldloc.1
        IL_000b: unbox.any [mscorlib]System.Int32
        IL_0010: stloc.0
        IL_0011: ldc.i4.1
        IL_0012: ldloc.0
        IL_0013: newobj instance void valuetype [mscorlib]System.ValueTuple`2<bool, int32>::.ctor(!0, !1)
        IL_0018: ret

        IL_0019: ldc.i4.0
        IL_001a: ldc.i4.0
        IL_001b: newobj instance void valuetype [mscorlib]System.ValueTuple`2<bool, int32>::.ctor(!0, !1)
        IL_0020: ret
    } // end of method TryVsTuple::IsInt

    .method public hidebysig 
        instance bool TryIsInt (
            object 'value',
            [out] int32& intValue
        ) cil managed noinlining 
    {
        // Method begins at RVA 0x2080
        // Code size 27 (0x1b)
        .maxstack 2
        .locals init (
            [0] int32,
            [1] object
        )

        IL_0000: ldarg.1
        IL_0001: dup
        IL_0002: stloc.1
        IL_0003: isinst [mscorlib]System.Int32
        IL_0008: brfalse.s IL_0016

        IL_000a: ldloc.1
        IL_000b: unbox.any [mscorlib]System.Int32
        IL_0010: stloc.0
        IL_0011: ldarg.2
        IL_0012: ldloc.0
        IL_0013: stind.i4
        IL_0014: ldc.i4.1
        IL_0015: ret

        IL_0016: ldarg.2
        IL_0017: ldc.i4.0
        IL_0018: stind.i4
        IL_0019: ldc.i4.0
        IL_001a: ret
    } // end of method TryVsTuple::TryIsInt

    .method public hidebysig 
        instance int32 BenchmarkIsInt () cil managed 
    {
        // Method begins at RVA 0x20a8
        // Code size 32 (0x20)
        .maxstack 2
        .locals init (
            [0] bool,
            [1] int32
        )

        IL_0000: ldarg.0
        IL_0001: ldc.i4.0
        IL_0002: box [mscorlib]System.Int32
        IL_0007: call instance valuetype [mscorlib]System.ValueTuple`2<bool, int32> TryVsTuple::IsInt(object)
        IL_000c: dup
        IL_000d: ldfld !0 valuetype [mscorlib]System.ValueTuple`2<bool, int32>::Item1
        IL_0012: stloc.0
        IL_0013: ldfld !1 valuetype [mscorlib]System.ValueTuple`2<bool, int32>::Item2
        IL_0018: stloc.1
        IL_0019: ldloc.0
        IL_001a: brfalse.s IL_001e

        IL_001c: ldloc.1
        IL_001d: ret

        IL_001e: ldc.i4.0
        IL_001f: ret
    } // end of method TryVsTuple::BenchmarkIsInt

    .method public hidebysig 
        instance int32 BenchmarkTryIsInt () cil managed 
    {
        // Method begins at RVA 0x20d4
        // Code size 20 (0x14)
        .maxstack 3
        .locals init (
            [0] int32
        )

        IL_0000: ldarg.0
        IL_0001: ldc.i4.0
        IL_0002: box [mscorlib]System.Int32
        IL_0007: ldloca.s 0
        IL_0009: call instance bool TryVsTuple::TryIsInt(object, int32&)
        IL_000e: brfalse.s IL_0012

        IL_0010: ldloc.0
        IL_0011: ret

        IL_0012: ldc.i4.0
        IL_0013: ret
    } // end of method TryVsTuple::BenchmarkTryIsInt

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20f4
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method TryVsTuple::.ctor

} // end of class TryVsTuple

CILを見る限りだと、ldfld でTupleのフィールドを見に行ってるのが速度差の原因かもしれませんが、Try-Patternまだまだ使える説がわかったので今回はこのあたりで。