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まだまだ使える説がわかったので今回はこのあたりで。