14

In Javascript for example, one is strongly encouraged to place function calls outside of loops for better performance:

var id = someIdType.ToString();
someList.Where(a => a.id == id) ...

How about C#? Same case or does the compiler/runtime employ internal optimization/caching?

someList.Where(a => a.id == someIdType.ToString()) ...

Probably a noob question and has been asked before, but can't find a reference.

Benjamin E.
  • 5,042
  • 5
  • 38
  • 65
  • What have you tried? Do you have a sample application that you could do some measurements on? – scheien Nov 06 '14 at 07:10
  • 3
    A very simple test using the sample you provided (looking up a string) within a list of 1000000 elements, the latter one is 4 times slower, ~118ms against ~32ms. Atleast on my workstation ;) – scheien Nov 06 '14 at 07:29
  • 2
    Before you do any manual optimization, it's important to know whether it's worth the effort - is this piece of code actually the bottleneck in performance for your application? If not, just pick the form that you think reads best and leave it alone. If it is the bottleneck, *then* try out different variants and pick the one that performs best. Don't try to learn thousands of "performance" rules and then rigidly code according to them. It's better to write clear code that you'll be able to go back and quickly understand in 6 months time than to try to write all code as if every ms counts. – Damien_The_Unbeliever Nov 06 '14 at 09:04

2 Answers2

14

C# code:

List<string> list = new List<string>();
list.Where(a => a == typeof(String).ToString());

Lambda expression in MSIL, Debug configuration:

.method private hidebysig static bool  '<Main>b__0'(string a) cil managed
{
  .custom instance void     [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       26 (0x1a)
  .maxstack  2
  .locals init ([0] bool CS$1$0000)
  IL_0000:  ldarg.0
  IL_0001:  ldtoken    [mscorlib]System.String
  IL_0006:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
  IL_000b:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0010:  call       bool [mscorlib]System.String::op_Equality(string,
                                                             string)
  IL_0015:  stloc.0
  IL_0016:  br.s       IL_0018
  IL_0018:  ldloc.0
  IL_0019:  ret
} // end of method Program::'<Main>b__0'

Lambda expression in MSIL, Release configuration:

.method private hidebysig static bool  '<Main>b__0'(string a) cil managed
{
  .custom instance void     [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       22 (0x16)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldtoken    [mscorlib]System.String
  IL_0006:  call       class [mscorlib]System.Type     [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
  IL_000b:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0010:  call       bool [mscorlib]System.String::op_Equality(string,
                                                             string)
  IL_0015:  ret
} // end of method Program::'<Main>b__0'

Both versions call typeof(String).ToString()), this lambda is called on every iteration. No optimization on IL level, JIT compilation will not add anything here. The reason is: function may have side effects.

Alex F
  • 42,307
  • 41
  • 144
  • 212
  • 5
    I don't think this is necessarily/particularly due to the fact that function may have side effects... For example, the member being accessed inside the lambda may be a field, and not a function... Even though field access does **not** have side effect, it will **not** be cached.. Hence to put is as "reason is: function may have side effects" is not exactly accurate.. So I'd simply say.. Compiler / Runtime doesn't cache it.. Why? Who knows? May be they never considered it.. May be they had 10 reasons, including side effecting functions.. may be they found the feature in general to be low ROI. – Vikas Gupta Nov 06 '14 at 08:01
  • @VikasGupta - probably it is better to formulate this as "one of the reasons". I tested this for public field, it is not optimized as well. – Alex F Nov 06 '14 at 08:16
  • Another good reason: function call (and even field access) may give different results from time to time. For example, `Random.Next()` mentioned by @Mad Sorcerer. Or result of multithreading, when object is changed in another thread. – Alex F Nov 06 '14 at 08:27
  • 1
    @AlexFarber Random.Next() is not a good example of this other reason, since this is a function that has side effects - And those side effects is the reason that the output changes. – Taemyr Nov 06 '14 at 10:53
  • 7
    Looking at MSIL doesn't help, code hoisting is an [optimization performed by the jitter](http://stackoverflow.com/a/4045073/17034) on the generated machine code. – Hans Passant Nov 06 '14 at 10:58
5

The lambda will be executed for each element of the list.. Hence the code someIdType.ToString() will execute for every element. I don't think compiler or runtime will cache it for you. (AFAIK someIdType will be captured in a closure, but not .ToString())

EDIT: The original question was only about "Does?", but not about "Why?", but still there are several comments and other answers which attempt to answer / demonstrate "Why?".

Given so much interest around "Why?" I am editing my answer to state my version of "Why?". i.e. if you look at C# specification, for any of the relevant scenarios, the specification talks about capturing the variable.. not capturing the expression. That is the reason why compiler behaves the way it does.. Because its not in the specification. Why is it not in the specification, is something C# Design team can answer.. Rest is speculation, parts or all of which may or may not have merit, should the feature of capturing expressions be considered.

Vikas Gupta
  • 4,455
  • 1
  • 20
  • 40
  • 3
    Compiler cannot and will not cache it, because in C# functions are not pure functions, i.e. they can have side-effects or return different values on same arguments (look at `Random.Next()`) – Diligent Key Presser Nov 06 '14 at 07:17
  • It is reasonable. Another question. What if called method has `Pure` attribute? Does it make sense? – serdar Nov 06 '14 at 07:18
  • @serdar AFAIK no, it does not enable such optimizations. – Diligent Key Presser Nov 06 '14 at 07:21
  • @MadSorcerer In fact if it enabled that optimization that would cause very big problems in the case we incorrectly put a pure attribute to a method which has side effects. – serdar Nov 06 '14 at 07:25
  • 1
    Even if it were possible to reliably determine which functions are "pure", you'd still have another problem: a pure function may still throw an exception, and if that pure function is called in a loop which should be executed not at all (let's say you're using `foreach` on an empty list), you should get no exception. If the call were simply moved out of the loop, you would get an exception. –  Nov 06 '14 at 08:44