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