{"id":5758,"date":"2020-05-25T07:00:51","date_gmt":"2020-05-25T14:00:51","guid":{"rendered":"http:\/\/writeasync.net\/?p=5758"},"modified":"2020-05-24T09:51:52","modified_gmt":"2020-05-24T16:51:52","slug":"switch-expression-performance-part-1","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=5758","title":{"rendered":"Switch expression performance: part 1"},"content":{"rendered":"<p>Being the good <a href=\"https:\/\/en.wikipedia.org\/wiki\/Comparison_of_multi-paradigm_programming_languages\">multi-paradigm language<\/a> that it is, C# has adopted features from across the programming spectrum. One recent addition of the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Functional_programming\">functional programming<\/a> variety is <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/pattern-matching\">pattern matching<\/a>, first arriving in <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/whats-new\/csharp-7#pattern-matching\">C# 7.0<\/a> with enhancements in <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/whats-new\/csharp-8#more-patterns-in-more-places\">C# 8.0<\/a>. The latter defines a new syntax called the <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/language-reference\/operators\/switch-expression\">switch expression<\/a> which can make your code a lot less cluttered and <a href=\"https:\/\/stackoverflow.com\/questions\/56068105\/what-happens-if-my-c-sharp-switch-expression-is-non-exhaustive\">warn you when your cases are not exhaustive<\/a>. But of course, I need to ask &#8212; does this syntactic sugar come at a price?<\/p>\n<p>To answer this, we will run a head-to-head benchmark. We&#8217;ll compare these three implementations of an enum translation method:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n    public static class MyLevel\r\n    {\r\n        public static char Translate0(TraceLevel level)\r\n        {\r\n            if (level == TraceLevel.Verbose)\r\n            {\r\n                return 'V';\r\n            }\r\n\r\n            if (level == TraceLevel.Info)\r\n            {\r\n                return 'I';\r\n            }\r\n\r\n            if (level == TraceLevel.Warning)\r\n            {\r\n                return 'W';\r\n            }\r\n\r\n            if (level == TraceLevel.Error)\r\n            {\r\n                return 'E';\r\n            }\r\n\r\n            if (level == TraceLevel.Off)\r\n            {\r\n                return ' ';\r\n            }\r\n\r\n            throw new ArgumentOutOfRangeException(nameof(level));\r\n        }\r\n\r\n        public static char Translate1(TraceLevel level)\r\n        {\r\n            switch (level)\r\n            {\r\n                case TraceLevel.Verbose: return 'V';\r\n                case TraceLevel.Info: return 'I';\r\n                case TraceLevel.Warning: return 'W';\r\n                case TraceLevel.Error: return 'E';\r\n                case TraceLevel.Off: return ' ';\r\n                default: throw new ArgumentOutOfRangeException(nameof(level));\r\n            };\r\n        }\r\n\r\n        public static char Translate2(TraceLevel level)\r\n        {\r\n            return level switch\r\n            {\r\n                TraceLevel.Verbose =&gt; 'V',\r\n                TraceLevel.Info =&gt; 'I',\r\n                TraceLevel.Warning =&gt; 'W',\r\n                TraceLevel.Error =&gt; 'E',\r\n                TraceLevel.Off =&gt; ' ',\r\n                _ =&gt; throw new ArgumentOutOfRangeException(nameof(level)),\r\n            };\r\n        }\r\n    }\r\n<\/pre>\n<p>The first method uses a boring if\/else statement; we can think of it as the control group. The next method uses the classic switch statement. The final method is the same switch statement transformed into a switch expression &#8212; exactly what Visual Studio would do to apply the <a href=\"https:\/\/docs.microsoft.com\/en-us\/visualstudio\/ide\/reference\/convert-switch-statement-to-switch-expression?view=vs-2019\">&#8220;Convert switch statement to switch expression&#8221;<\/a> refactoring. Finally, we need a benchmark to compare the raw runtime performance:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n    &#x5B;SimpleJob(RuntimeMoniker.NetCoreApp31)]\r\n    &#x5B;MemoryDiagnoser]\r\n    public class SwitchBenchmark\r\n    {\r\n        &#x5B;Benchmark]\r\n        public int IfElse()\r\n        {\r\n            int sum = 0;\r\n            sum += MyLevel.Translate0(TraceLevel.Error);\r\n            sum += MyLevel.Translate0(TraceLevel.Warning);\r\n            sum += MyLevel.Translate0(TraceLevel.Info);\r\n            sum += MyLevel.Translate0(TraceLevel.Verbose);\r\n            sum += MyLevel.Translate0(TraceLevel.Off);\r\n            return sum;\r\n        }\r\n\r\n        &#x5B;Benchmark]\r\n        public int Switch1()\r\n        {\r\n            int sum = 0;\r\n            sum += MyLevel.Translate1(TraceLevel.Error);\r\n            sum += MyLevel.Translate1(TraceLevel.Warning);\r\n            sum += MyLevel.Translate1(TraceLevel.Info);\r\n            sum += MyLevel.Translate1(TraceLevel.Verbose);\r\n            sum += MyLevel.Translate1(TraceLevel.Off);\r\n            return sum;\r\n        }\r\n\r\n        &#x5B;Benchmark]\r\n        public int Switch2()\r\n        {\r\n            int sum = 0;\r\n            sum += MyLevel.Translate2(TraceLevel.Error);\r\n            sum += MyLevel.Translate2(TraceLevel.Warning);\r\n            sum += MyLevel.Translate2(TraceLevel.Info);\r\n            sum += MyLevel.Translate2(TraceLevel.Verbose);\r\n            sum += MyLevel.Translate2(TraceLevel.Off);\r\n            return sum;\r\n        }\r\n    }\r\n<\/pre>\n<p>The results:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n|  Method |     Mean |    Error |   StdDev |   Median | Gen 0 | Gen 1 | Gen 2 | Allocated |\r\n|-------- |---------:|---------:|---------:|---------:|------:|------:|------:|----------:|\r\n|  IfElse | 16.11 ns | 0.038 ns | 0.030 ns | 16.10 ns |     - |     - |     - |         - |\r\n| Switch1 | 14.76 ns | 0.329 ns | 0.756 ns | 14.31 ns |     - |     - |     - |         - |\r\n| Switch2 | 16.49 ns | 0.364 ns | 0.933 ns | 15.93 ns |     - |     - |     - |         - |\r\n<\/pre>\n<p>Now this is interesting. It appears that the classic switch statement is consistently faster! I completely expected it to do better than if\/else because of the possibility of a <a href=\"http:\/\/lazarenko.me\/switch\/\">jump table implementation for switch<\/a>, but I am a bit surprised that the switch expression is measurably slower. To see why this might be the case, we have to go spelunking in the IL code again:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n.method public hidebysig static \r\n    char Translate0 (\r\n        valuetype &#x5B;System.Diagnostics.TraceSource]System.Diagnostics.TraceLevel level\r\n    ) cil managed \r\n{\r\n    \/\/ Method begins at RVA 0x2050\r\n    \/\/ Code size 45 (0x2d)\r\n    .maxstack 8\r\n\r\n    \/\/ (no C# code)\r\n    IL_0000: ldarg.0\r\n    IL_0001: ldc.i4.4\r\n    IL_0002: bne.un.s IL_0007\r\n\r\n    \/\/ return 'V';\r\n    IL_0004: ldc.i4.s 86\r\n    \/\/ (no C# code)\r\n    IL_0006: ret\r\n\r\n    IL_0007: ldarg.0\r\n    IL_0008: ldc.i4.3\r\n    IL_0009: bne.un.s IL_000e\r\n\r\n    \/\/ return 'I';\r\n    IL_000b: ldc.i4.s 73\r\n    \/\/ (no C# code)\r\n    IL_000d: ret\r\n\r\n    IL_000e: ldarg.0\r\n    IL_000f: ldc.i4.2\r\n    IL_0010: bne.un.s IL_0015\r\n\r\n    \/\/ return 'W';\r\n    IL_0012: ldc.i4.s 87\r\n    \/\/ (no C# code)\r\n    IL_0014: ret\r\n\r\n    IL_0015: ldarg.0\r\n    IL_0016: ldc.i4.1\r\n    IL_0017: bne.un.s IL_001c\r\n\r\n    \/\/ return 'E';\r\n    IL_0019: ldc.i4.s 69\r\n    \/\/ (no C# code)\r\n    IL_001b: ret\r\n\r\n    IL_001c: ldarg.0\r\n    IL_001d: brtrue.s IL_0022\r\n\r\n    \/\/ return ' ';\r\n    IL_001f: ldc.i4.s 32\r\n    \/\/ (no C# code)\r\n    IL_0021: ret\r\n\r\n    \/\/ throw new ArgumentOutOfRangeException(&quot;level&quot;);\r\n    IL_0022: ldstr &quot;level&quot;\r\n    IL_0027: newobj instance void &#x5B;System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)\r\n    \/\/ (no C# code)\r\n    IL_002c: throw\r\n} \/\/ end of method MyLevel::Translate0\r\n\r\n.method public hidebysig static \r\n    char Translate1 (\r\n        valuetype &#x5B;System.Diagnostics.TraceSource]System.Diagnostics.TraceLevel level\r\n    ) cil managed \r\n{\r\n    \/\/ Method begins at RVA 0x207e\r\n    \/\/ Code size 54 (0x36)\r\n    .maxstack 8\r\n\r\n    \/\/ switch (level)\r\n    IL_0000: ldarg.0\r\n    \/\/ (no C# code)\r\n    IL_0001: switch (IL_0028, IL_0025, IL_0022, IL_001f, IL_001c)\r\n\r\n    IL_001a: br.s IL_002b\r\n\r\n    \/\/ return 'V';\r\n    IL_001c: ldc.i4.s 86\r\n    \/\/ (no C# code)\r\n    IL_001e: ret\r\n\r\n    \/\/ return 'I';\r\n    IL_001f: ldc.i4.s 73\r\n    \/\/ (no C# code)\r\n    IL_0021: ret\r\n\r\n    \/\/ return 'W';\r\n    IL_0022: ldc.i4.s 87\r\n    \/\/ (no C# code)\r\n    IL_0024: ret\r\n\r\n    \/\/ return 'E';\r\n    IL_0025: ldc.i4.s 69\r\n    \/\/ (no C# code)\r\n    IL_0027: ret\r\n\r\n    \/\/ return ' ';\r\n    IL_0028: ldc.i4.s 32\r\n    \/\/ (no C# code)\r\n    IL_002a: ret\r\n\r\n    \/\/ throw new ArgumentOutOfRangeException(&quot;level&quot;);\r\n    IL_002b: ldstr &quot;level&quot;\r\n    IL_0030: newobj instance void &#x5B;System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)\r\n    \/\/ (no C# code)\r\n    IL_0035: throw\r\n} \/\/ end of method MyLevel::Translate1\r\n\r\n.method public hidebysig static \r\n    char Translate2 (\r\n        valuetype &#x5B;System.Diagnostics.TraceSource]System.Diagnostics.TraceLevel level\r\n    ) cil managed \r\n{\r\n    \/\/ Method begins at RVA 0x20b8\r\n    \/\/ Code size 66 (0x42)\r\n    .maxstack 1\r\n    .locals init (\r\n        &#x5B;0] char\r\n    )\r\n\r\n    \/\/ switch (level)\r\n    IL_0000: ldarg.0\r\n    \/\/ (no C# code)\r\n    IL_0001: switch (IL_0030, IL_002b, IL_0026, IL_0021, IL_001c)\r\n\r\n    IL_001a: br.s IL_0035\r\n\r\n    \/\/ return 'V';\r\n    IL_001c: ldc.i4.s 86\r\n    IL_001e: stloc.0\r\n    \/\/ (no C# code)\r\n    IL_001f: br.s IL_0040\r\n\r\n    \/\/ return 'I';\r\n    IL_0021: ldc.i4.s 73\r\n    IL_0023: stloc.0\r\n    \/\/ (no C# code)\r\n    IL_0024: br.s IL_0040\r\n\r\n    \/\/ return 'W';\r\n    IL_0026: ldc.i4.s 87\r\n    IL_0028: stloc.0\r\n    \/\/ (no C# code)\r\n    IL_0029: br.s IL_0040\r\n\r\n    \/\/ return 'E';\r\n    IL_002b: ldc.i4.s 69\r\n    IL_002d: stloc.0\r\n    \/\/ (no C# code)\r\n    IL_002e: br.s IL_0040\r\n\r\n    \/\/ return ' ';\r\n    IL_0030: ldc.i4.s 32\r\n    IL_0032: stloc.0\r\n    \/\/ (no C# code)\r\n    IL_0033: br.s IL_0040\r\n\r\n    \/\/ throw new ArgumentOutOfRangeException(&quot;level&quot;);\r\n    IL_0035: ldstr &quot;level&quot;\r\n    IL_003a: newobj instance void &#x5B;System.Runtime]System.ArgumentOutOfRangeException::.ctor(string)\r\n    \/\/ (no C# code)\r\n    IL_003f: throw\r\n\r\n    IL_0040: ldloc.0\r\n    IL_0041: ret\r\n} \/\/ end of method MyLevel::Translate2\r\n<\/pre>\n<p>As we might expect, the if\/else implementation wins in terms of code size. However, it must sequentially evaluate all the conditions so it will on average need more cycles to produce a result. The classic switch statement as predicted uses <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.reflection.emit.opcodes.switch?view=netcore-3.1\">a jump table in IL<\/a> at the expense of a slightly larger code size. The switch expression has the largest code size, which partially explains its worse performance. Looking more closely, we can see that this implementation requires storing to a local value and branching in every case. By contrast, the classic switch statement does early returns and elides the local.<\/p>\n<p>Maybe a slightly smarter compiler\/optimizer could close this gap. However, for now, it seems that switch expressions have a slight overhead which could give classic switch statements an edge in the hottest of hot paths.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Being the good multi-paradigm language that it is, C# has adopted features from across the programming spectrum. One recent addition of the functional programming variety is pattern matching, first arriving in C# 7.0 with enhancements in C# 8.0. The latter&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[104],"tags":[],"class_list":["post-5758","post","type-post","status-publish","format-standard","hentry","category-performance"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5758","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5758"}],"version-history":[{"count":3,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5758\/revisions"}],"predecessor-version":[{"id":5761,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5758\/revisions\/5761"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5758"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5758"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5758"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}