どうもC#erの@MeilCliです。仕事ではAndroidエンジニアをしていますがC#erなのでアドベントカレンダーではC#について書きます
今回参加しているアドベントカレンダーはこちらです。3日目の記事になります
あと、同様なカレンダーがもう一つあります
問: どれが早いか
int? n = 0; if (n.HasValue) {}// ① if (n is int) {}// ② if (n is int and int) {}// ③ if (n is not null) {}// ④
※ Roslyn master(25 Nov 2020)時点
正解はこの記事の中盤に書いています
n.HasValueとはなんぞや
C#erではない人向けに解説すると、C#のnull許容型は2種類(null許容参照型・null許容値型)が存在します
null許容参照型のほうはC# 8.0でできた概念で、参照型の変数・値がnullになりえるものをnull許容参照型、nullになりえないものをnull非許容参照型とすることで近年のモダンな言語のnullセーフティーを取り入れようとするものです
null許容値型のほうはだいぶ昔からあり*1、通常は何らかの値が存在する構造体においてNullable構造体を介することで疑似的にnullという状態を表現できるようにするという機能です
そして本題のn.HasValue
についてですが、Nullable構造体が値を持っているかを返すプロパティとしてHasValue
があるというわけです
n is int and intとはなんぞや
n is int and int
という一見奇妙な式はC# 9.0で機能強化されたパターンマッチングによるものです
強化内容としては主にパターンマッチング中にand
, or
, not
が使えるようになるといった感じです
たとえば変数nが0から10の間というのを素直に表現すると0 <= n && n <= 10
ですが、機能強化されたパターンマッチングで表現するとn is >= 0 and <= 10
となります*2
パターンマッチングには型パターンがありn is int and > 0
でint?
に値があり*3それが0以上という表現ができるのですが、無意味にint
を判定させることもできるのでn is int and int
という書き方ができるわけです
答: どれが早いか
正解は①, ③, ④です。②のn is int
による判定が遅いという感じです
①と④だと考えた人は惜しかったですね。以前、C# 7.3の頃に同様な記事を書いてn is int
は遅いよ~と話していたのでMeilCliの個人ブログの熱烈な読者の方*4は同様にn is int and int
も遅いと考えたことでしょう(たぶん)
初見で①, ③, ④と答えれた人はすごいですね。C#のエキスパートもしくはMeilCliのTwitterのフォロワーですね
int? n = 0;
— C#ワカラナイ (@penguin_sharp) November 30, 2020
if (n is int) {}
if (n is int and int) {}
後者のほうが早そうということを発見
確: どれが早いか
さて、ほんとに②だけが遅いのか
手っ取り早くBenchmarkDotNetを使って計測してみましょう
namespace BenchmarkCode { [SimpleJob] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class NullableInt { public IEnumerable<int?> Source() { yield return 1; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool HasValue(int? n) { return n.HasValue; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsInt(int? n) { return n is int; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsIntAndInt(int? n) { return n is int and int; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsNotNull(int? n) { return n is not null; } } }
計測コードはこちらです。コードの簡略化のため直接returnしています。SharpLabで軽く確認した限りだとif文の中で記述した場合と大差がない状態になっていました
そして結果がこう*5
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 16 logical and 8 physical cores .NET Core SDK=5.0.100 [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
Method | n | Mean | Error | StdDev | Min | Max | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
HasValue | 1 | 0.6882 ns | 0.0093 ns | 0.0087 ns | 0.6732 ns | 0.7032 ns | - | - | - | - |
IsInt | 1 | 31.5686 ns | 0.1760 ns | 0.1646 ns | 31.2655 ns | 31.8268 ns | 0.0023 | - | - | 24 B |
IsIntAndInt | 1 | 0.6989 ns | 0.0074 ns | 0.0069 ns | 0.6891 ns | 0.7116 ns | - | - | - | - |
IsNotNull | 1 | 0.6731 ns | 0.0076 ns | 0.0071 ns | 0.6634 ns | 0.6852 ns | - | - | - | - |
明らかにn is int
の場合が遅いですね
解: どれが早いか
さて、なぜこのような差がついたかというとベンチマーク結果で明らかなのですが、n is int
の場合にボックス化が発生しているためです
bool HasValue(int? n) => n.HasValue; bool IsInt(int? n) => n is int; bool IsIntAndInt(int? n) => n is int and int; bool IsNotNull(int? n) => n is not null;
SharpLabで上記のコードをデコンパイルするとIsInt以外のケースはldarga.s
で引数のアドレスを取得してcall
でNullable構造体のHasValueの値を返すようになっています
.method assembly hidebysig static bool '<<Main>$>g__IsIntAndInt|0_2' ( valuetype [System.Private.CoreLib]System.Nullable`1<int32> n ) cil managed { // Method begins at RVA 0x2056 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarga.s n IL_0002: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue() IL_0007: ret } // end of method '<Program>$'::'<<Main>$>g__IsIntAndInt|0_2'
こんな感じ
.method assembly hidebysig static bool '<<Main>$>g__IsInt|0_1' ( valuetype [System.Private.CoreLib]System.Nullable`1<int32> n ) cil managed { // Method begins at RVA 0x205f // Code size 15 (0xf) .maxstack 8 IL_0000: ldarg.0 IL_0001: box valuetype [System.Private.CoreLib]System.Nullable`1<int32> IL_0006: isinst [System.Private.CoreLib]System.Int32 IL_000b: ldnull IL_000c: cgt.un IL_000e: ret } // end of method '<Program>$'::'<<Main>$>g__IsInt|0_1'
しかし、IsIntのケースではbox
でボックス化をしていることがわかります
ボックス化をするコストがかかることによってほかのケースより遅いというわけです
なぜこのようなことが起きるのか
C#コンパイラーであるRoslynのソースを読めば答えが見つかると思いますが、手元でいろいろなケースのCIL*6を確かめてるとnull許容値型のvalue is T
のケースのみ最適化のようなことがされず常にボックス化されるコードが吐き出されており、C# 7.0以降で追加されたパターンマッチングの機能の領域に入ると最適化されたコードが吐き出されてる印象がありました
そのためなのか一見n is int
より無駄が多そうなn is int and int
のほうが早いみたいな不可解な現象がおきてるようです
ちなみに
この記事のネタはアドベントカレンダーの記事を書いてて偶然発見したものをアドベントカレンダーの記事化したものです。本題のほうは16日に投稿すると思います(たぶん)
昨日の「dely #1 Advent Calendar 2020」はGENさんの「木も見て森も見るための Athena(Presto) 集計術」でした
明日はfunzinさんの「RenovateをiOSアプリ開発に導入してみた」です。お楽しみに!