Imperative vs. LINQ Performance on WP7

Jesse Liberty had a nice post presenting the concepts around imperative, LINQ and fluent programming to populate a listbox. Check out the post as it’s a great example of some foundational things every .NET programmer should know.

I was more interested in what the IL code that would be generated from imperative vs. LINQ was like and what the performance numbers are and how they differ.

The code at the instruction level is interesting but not surprising. The imperative example with it’s creating lists and loops weighs in at about 60 instructions.

   1:  .method private hidebysig instance void ImperativeMethod() cil managed

   2:  {

   3:      .maxstack 3

   4:      .locals init (

   5:          [0] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> someData,

   6:          [1] class [mscorlib]System.Collections.Generic.List`1<int32> inLoop,

   7:          [2] int32 n,

   8:          [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000,

   9:          [4] bool CS$4$0001)

  10:      L_0000: nop 

  11:      L_0001: ldc.i4.1 

  12:      L_0002: ldc.i4.s 50

  13:      L_0004: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)

  14:      L_0009: stloc.0 

  15:      L_000a: newobj instance void [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()

  16:      L_000f: stloc.1 

  17:      L_0010: nop 

  18:      L_0011: ldloc.0 

  19:      L_0012: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator1&lt;!0&gt; [mscorlib]System.Collections.Generic.IEnumerable1<int32>::GetEnumerator()

  20:      L_0017: stloc.3 

  21:      L0018: br.s L003a

  22:      L_001a: ldloc.3 

  23:      L001b: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::getCurrent()

  24:      L_0020: stloc.2 

  25:      L_0021: nop 

  26:      L_0022: ldloc.2 

  27:      L_0023: ldc.i4.5 

  28:      L_0024: cgt 

  29:      L_0026: ldc.i4.0 

  30:      L_0027: ceq 

  31:      L_0029: stloc.s CS$4$0001

  32:      L_002b: ldloc.s CS$4$0001

  33:      L002d: brtrue.s L0039

  34:      L_002f: ldloc.1 

  35:      L_0030: ldloc.2 

  36:      L_0031: ldloc.2 

  37:      L_0032: mul 

  38:      L_0033: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)

  39:      L_0038: nop 

  40:      L_0039: nop 

  41:      L_003a: ldloc.3 

  42:      L_003b: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()

  43:      L_0040: stloc.s CS$4$0001

  44:      L_0042: ldloc.s CS$4$0001

  45:      L0044: brtrue.s L001a

  46:      L0046: leave.s L005a

  47:      L_0048: ldloc.3 

  48:      L_0049: ldnull 

  49:      L_004a: ceq 

  50:      L_004c: stloc.s CS$4$0001

  51:      L_004e: ldloc.s CS$4$0001

  52:      L0050: brtrue.s L0059

  53:      L_0052: ldloc.3 

  54:      L_0053: callvirt instance void [mscorlib]System.IDisposable::Dispose()

  55:      L_0058: nop 

  56:      L_0059: endfinally 

  57:      L_005a: nop 

  58:      L_005b: ldarg.0 

  59:      L_005c: ldfld class [System.Windows]System.Windows.Controls.ListBox PerfTest.MainPage::LB1

  60:      L_0061: ldloc.1 

  61:      L0062: callvirt instance void [System.Windows]System.Windows.Controls.ItemsControl::setItemsSource(class [mscorlib]System.Collections.IEnumerable)

  62:      L_0067: nop 

  63:      L_0068: ret 

  64:      .try L0018 to L0048 finally handler L0048 to L005a

  65:  }

  66:   

  67:   

Compare that to the IL generated for the LINQ version which has about half of the instructions and just gets the job done, no fluff.

   1:  .method private hidebysig instance void LINQMethod() cil managed

   2:  {

   3:      .maxstack 4

   4:      .locals init (

   5:          [0] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> someData,

   6:          [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> queryResult)

   7:      L_0000: nop 

   8:      L_0001: ldc.i4.1 

   9:      L_0002: ldc.i4.s 50

  10:      L_0004: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)

  11:      L_0009: stloc.0 

  12:      L_000a: ldloc.0 

  13:      L000b: ldsfld class [System.Core]System.Func`2<int32, bool> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate6

  14:      L0010: brtrue.s L0025

  15:      L_0012: ldnull 

  16:      L0013: ldftn bool PerfTest.MainPage::<LINQProgramming>b_4(int32)

  17:      L_0019: newobj instance void [System.Core]System.Func`2<int32, bool>::.ctor(object, native int)

  18:      L001e: stsfld class [System.Core]System.Func`2<int32, bool> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate6

  19:      L0023: br.s L0025

  20:      L0025: ldsfld class [System.Core]System.Func`2<int32, bool> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate6

  21:      L_002a: call class [mscorlib]System.Collections.Generic.IEnumerable1&lt;!!0&gt; [System.Core]System.Linq.Enumerable::Where&lt;int32&gt;(<SPAN class="kwrd">class</SPAN> [mscorlib]System.Collections.Generic.IEnumerable1<!!0>, class [System.Core]System.Func`2<!!0, bool>)

  22:      L002f: ldsfld class [System.Core]System.Func`2<int32, int32> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate7

  23:      L0034: brtrue.s L0049

  24:      L_0036: ldnull 

  25:      L0037: ldftn int32 PerfTest.MainPage::<LINQProgramming>b_5(int32)

  26:      L_003d: newobj instance void [System.Core]System.Func`2<int32, int32>::.ctor(object, native int)

  27:      L0042: stsfld class [System.Core]System.Func`2<int32, int32> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate7

  28:      L0047: br.s L0049

  29:      L0049: ldsfld class [System.Core]System.Func`2<int32, int32> PerfTest.MainPage::CS$<>9_CachedAnonymousMethodDelegate7

  30:      L_004e: call class [mscorlib]System.Collections.Generic.IEnumerable1&lt;!!1&gt; [System.Core]System.Linq.Enumerable::Select&lt;int32, int32&gt;(<SPAN class="kwrd">class</SPAN> [mscorlib]System.Collections.Generic.IEnumerable1<!!0>, class [System.Core]System.Func`2<!!0, !!1>)

  31:      L_0053: stloc.1 

  32:      L_0054: ldarg.0 

  33:      L_0055: ldfld class [System.Windows]System.Windows.Controls.ListBox PerfTest.MainPage::LB2

  34:      L_005a: ldloc.1 

  35:      L005b: callvirt instance void [System.Windows]System.Windows.Controls.ItemsControl::setItemsSource(class [mscorlib]System.Collections.IEnumerable)

  36:      L_0060: nop 

  37:      L_0061: ret 

  38:  }

Again, not surprising here but a good indicator that you should consider using LINQ where possible. In fact if you have ReSharper installed you’ll see a squiggly (technical term) in the imperative code that says “Hey Dude, I can convert this to LINQ if you want to be c00L!” (or something like that, it’s the 2010 geek version of Clippy).

What about the fluent version? As Jon correctly pointed out in the comments, when you compare the IL for the LINQ code and the IL for the fluent code it’s the same. LINQ and the fluent interface are just syntactical sugar so you decide what you’re most comfortable with. At the end of the day they’re both the same.

Now onto the numbers. Again I expected the imperative version to be better performing than the LINQ version (before I saw the IL that was generated). Call it womanly instinct. A gut feel. Whatever. Some of the numbers are interesting though.

For Jesse’s example of 50 items, the numbers were interesting. The imperative sample clocked in at 7ms while the LINQ version completed in 4. As the number of items went up, the elapsed time didn’t necessarily climb exponentially. At 500 items they were pretty much the same and the results were similar up to about 50,000 items. After that I tried 500,000 items where the gap widened but not by much (2.2 seconds for imperative, 2.3 for LINQ). It wasn’t until I tried 5,000,000 items where things were noticeable. Imperative filled the list in 20 seconds while LINQ took 8 seconds longer (although personally I wouldn’t suggest you put 5 million items in a list unless you want your users showing up at your door with torches and pitchforks).

Here’s the table with the full results.

  </STRONG><TD vAlign="top" width="71"><STRONG>50</STRONG></TD><STRONG>

  </STRONG><TD vAlign="top" width="71"><STRONG>500</STRONG></TD><STRONG>

  </STRONG><TD vAlign="top" width="71"><STRONG>5,000</STRONG></TD><STRONG>

  </STRONG><TD vAlign="top" width="71"><STRONG>50,000</STRONG></TD><STRONG>

  </STRONG><TD vAlign="top" width="71"><STRONG>500,000</STRONG></TD><STRONG>

  </STRONG><TD vAlign="top" width="71"><STRONG>5,000,000</STRONG></TD>
</TR>

<TR>
  <TD vAlign="top" width="71">Imperative</TD>

  <TD vAlign="top" width="71">7ms</TD>

  <TD vAlign="top" width="71">7ms</TD>

  <TD vAlign="top" width="71">38ms</TD>

  <TD vAlign="top" width="71">223ms</TD>

  <TD vAlign="top" width="71">2230ms</TD>

  <TD vAlign="top" width="71">20974ms</TD>
</TR>

<TR>
  <TD vAlign="top" width="71">LINQ/Fluent</TD>

  <TD vAlign="top" width="71">4ms</TD>

  <TD vAlign="top" width="71">6ms</TD>

  <TD vAlign="top" width="71">41ms</TD>

  <TD vAlign="top" width="71">240ms</TD>

  <TD vAlign="top" width="71">2310ms</TD>

  <TD vAlign="top" width="71">28731ms</TD>
</TR>

Method/Items

Like I said, at the end of the day it’s not a huge difference and you really don’t want your users waiting around for 30 seconds on a mobile device filling lists. In fact if Windows Phone 7 detects you’re taking more than 10 seconds to do any one thing, it considers the app hung and shuts it down. The results here are for Windows Phone 7 but frankly they're the same for desktop and web apps so feel free to apply it generally.

From a programming perspective, choose what you like. Some LINQ statements can get pretty hairy so I usually fall back with my simple mind and write it imperatively. If you really want to impress your friends, write it old school then let ReSharper do the hard work for!

Happy programming!

3 Comments

  • Interesting stats!

    Surprised by the 8 second difference on the high end. Will have to keep reasonably quite about LINQ to the boss. He doesn't like it and easily confused by the var keyword ;-)

  • I think it's funny how the imperative version looks like assembly code that could translate pretty close to machine code whereas the linq version looks like a high level language.

  • Oh, and also you're not counting the instructions in the two classes .NET builds for the two lambda functions.

    Sometimes it's difficult to read IL directly so another good way is looking at the C# generated by .NET Reflector. There are definitely some fascinating things generated by the compiler whenever you use IEnumerables (state machines) or closures (anonymous classes to hoist variables).

Comments have been disabled for this content.