C#開発における落とし穴~switch編~
ごきげんよう、皆様。
技術担当の大林です。
前回に引き続き、C#の開発における落とし穴、所謂うっかりやらかしたり勘違いしやすい部分について少し書こうと思います。
※当記事はVisualStudioでの開発を想定して執筆しております。
開発環境次第では当てはまらない事もある事を、あらかじめご了承ください。
さて、今回はswitch文について少し触れようと思います。
基本的に、数値の比較であればswitch文はコンパイラの方でジャンプテーブルを作成して上手いこと高速に判定してくれる処理になるようになっています。
ただ、ここで少し気になる事が出てきます。
そう、実はC#のswitch文では、通常の数値比較の他に、型比較も行う事が出来るのです。
型の判定やキャスト等、結構重い処理のはずなのですが、こういった型比較の方でも高速で判定出来るようになっているのでしょうか……?
気になったので、下記のようなコードを作成しました。
単純に、継承だけを行ったクラスを使って、速度を図ろうという試みです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class TestObjBase { } public class TestObjA : TestObjBase { } public class TestObjB : TestObjBase { } public class TestObjC : TestObjBase { } public class TestObjD : TestObjBase { } var list = new List(); //検証用にクラス作成 for (int i = 0; i < 1000000; i++) { list.Add(new TestObjA()); } TestSwitch(); void TestSwitch() { string testName = "TestSwitch"; //コスト検証 var sw = new Stopwatch(); sw.Start(); foreach (var obj in list) { switch (obj) { case TestObjA objA: break; case TestObjB objB: break; case TestObjC objC: break; case TestObjD objD: break; default: break; } } sw.Stop(); Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds)); } |
結果は
TestSwitch:6.758msec
さて、今度は後の方にあるcaseで比較している型に少し差し替えてみましょう。
1 2 3 4 5 6 7 8 9 10 11 | var list = new List(); //検証用にクラス作成 for (int i = 0; i < 1000000; i++) { // list.Add(new TestObjA()); list.Add(new TestObjD()); } TestIf(); TestSwitch(); |
結果は
TestSwitch:19.8408msec
目に見えて遅くなっていますね。
それもそのはず、型をキャスト可能かどうか判別するような複雑な処理においては、ジャンプテーブルを作成して高速化することなんて出来ないので、実質全てif~elseif~else文で判定しているのと同じことをやらないと行けないわけです。
ちなみにこちら、caseの順番を下のように書き換えてあげると、TestObjAで比較した時と同等の実行時間になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void TestSwitch() { string testName = "TestSwitch"; //コスト検証 var sw = new Stopwatch(); sw.Start(); foreach (var obj in list) { switch (obj) { case TestObjD objD: break; case TestObjB objB: break; case TestObjC objC: break; case TestObjA objA: break; default: break; } } sw.Stop(); Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds)); } |
おまけで、if文で同じような処理を記載したものも記載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | var list = new List(); //検証用にクラス作成 for (int i = 0; i < 1000000; i++) { list.Add(new TestObjD()); } TestIf(); TestSwitch(); void TestIf() { string testName = "TestIf"; //コスト検証 var sw = new Stopwatch(); sw.Start(); foreach (var obj in list) { if(obj is TestObjA objA) { } else if (obj is TestObjA objB) { } else if (obj is TestObjA objC) { } else if (obj is TestObjA objD) { } else { } } sw.Stop(); Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds)); } void TestSwitch() { string testName = "TestSwitch"; //コスト検証 var sw = new Stopwatch(); sw.Start(); foreach (var obj in list) { switch (obj) { case TestObjA objA: break; case TestObjB objB: break; case TestObjC objC: break; case TestObjD objD: break; default: break; } } sw.Stop(); Console.WriteLine(string.Format("{0}:{1}msec", testName, sw.Elapsed.TotalMilliseconds)); } |
結果は
TestIf:17.5013msec
TestSwitch:18.9551msec
誤差はあれど、同等の実行時間になりましたね。
ちなみにTestObjAの方で実行しても、同等の6~7msec程度の実行時間になります。
また、全て該当せずdefaultに行き着く場合、一番末尾のcaseと一致した時同様、最遅の結果になってしまいます。
やむを得ない場合は仕方ないですが、可能なら型判定用の整数値を持たせるなり、不要なクラス等を型比較の処理を通さないようにしたりと、上手く工夫する必要があります。
switch文=ジャンプテーブル使ってるから記述の順番気にしなくて良いやとか、とりあえずあれもこれもcaseに入れとこうみたいなノリで使ってしまうと、とても遅いコードを作ってしまう事になるので要注意です。
では!
見ていただき、ありがとうございました!