I have grown the habit of doing a couple of things when writing code that I initially thought would improve performance, even if only by a teensy tiny bit.
Well, turns out most of that was pretty much useless. Not hurting anything, but not helping either.
How did I learn that? By having a look at the IL. And here’s a collection of things I found out, that I didn’t know about before.
The code was compiled in Visual Studio for the .NET Framework 4.8 with MSBuild 16.2.0 and the Optimize flag set to true.
The IL was read with the IL Viewer of JetBrains ReSharper.
i++ or ++i
My assumption always was, that the later is slightly faster, since the old value doesn’t need to be “remembered” for use in whatever context it’s used in. Turns out, not really.
These two lines result in the exact same IL:
++foo; foo++;
If there is a bit more going on, the code actually has a difference:
foo = ++foo;
IL_0002: ldloc.0 // foo
IL_0003: ldc.i4.1
IL_0004: add
IL_0005: dup
IL_0006: stloc.0 // foo
IL_0007: stloc.0 // foo
foo = foo++;
IL_000c: ldloc.0 // foo
IL_000d: dup
IL_000e: ldc.i4.1
IL_000f: add
IL_0010: stloc.0 // foo
IL_0011: stloc.0 // foo
The “remembering” part is simply the duplication moved further up, before the increment.
Same operations, different order.
The only real difference this makes, is that the first option has at most 2 elements on the stack, the second 3. Slight win for my preference? But generally not important. Especially since what ever else you’re doing is probably going to need a bigger stack anyway.
Variable declarations inside or outside of loops
Not sure where I got this from, but basically I used to always declare variables outside of a loop, even if I only need it inside. I think the idea in my head was, that declaring it would require resources at that point in the program, and declaring it inside a loop would cost me those resources every iteration.
Nope.
while (true) { string line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; }
string line; while (true) { line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; }
Both result in the same IL … until you add a second loop.
string line; while (true) { line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; } while (true) { line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; }
while (true) { string line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; } while (true) { string line = Console.ReadLine(); Console.WriteLine($"Input was: {line}"); if (line == "exit") break; }
Declaring line outside the loop means the exact same variable is used both times, or better, the exact same memory address. In the second example the compiler ignores that option and uses two different variables, one for each loop.
I suspect that at runtime the JIT compiler, which does a lot more optimization, sees the obvious potential for reuse and makes the IL difference meaningless.
I do tend to declare variables inside the loop nowadays, unless I explicitly want them kept “alive” beyond their scope.
for or foreach
Let’s assume you got an array and want to iterate through it. Do you use a for or foreach loop?
I used to think foreach adds overhead, because it treats the array as an IEnumerable and creates an Enumerator.
It doesn’t.
These two loops
for (int i = 0; i < bar.Length; ++i) sum += bar[i];
foreach (int i in bar) sum += i;
do look different in the IL, but they do the same basic thing.
The foreach loop uses a variable it increments each iteration to use as index for the array.
Once that index reaches the arrays length, the loop ends. Pretty much like the for loop. This only applies to arrays though, List for example has an indexer, but foreach will use the enumerator!
! is null or is object
In the new C# versions there is a neat language addition, but if var is not null is not an option for you, there is either
if (!(something is null))
or
if (something is object)
Both have the same effect. But does the later do more stuff? No. Same IL.
IL_0006: ldloc.0 // something
IL_0007: brfalse.s IL_000e
Same with these two.
var = !(something is null); var = something is object;
IL_0006: ldloc.0 // something
IL_0007: ldnull
IL_0008: cgt.un
IL_000a: stloc.1 // var
switch or else if
If you have several different branches depending on a single value, you often use a switch. But what if there are only two options? Would an if/else if have a better performance? Short answer: Maybe, but don’t bother. Because the compiler makes that choice for you already.
Actually, switch is quite often compiled into something closer to if/else if, you just get special switch IL when you have neatly arranged integer values. And even then there are some interesting constellations, like splitting your switch into several individual ones.
So when a switch makes your code more readable, or simply easier to expand in the future, just use a switch.
Repeat typeof or store Type
nameof is evaluated during compilation, and all that remains in the compiled code is a string constant. That means I can use it as often as I want, without worrying about performance.
How about typeof though? What does it do in the IL? If I reference the type several times, can I still reuse typeof or is it better to cache the value?
string typeFullName = typeof(A).FullName; bool typeIsEnum = typeof(A).IsEnum; bool typeIsSerializable = typeof(A).IsSerializable;
// [14 13 - 14 54]
IL_0000: ldtoken IlCode.A
IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_000a: callvirt instance string [mscorlib]System.Type::get_FullName()
// [15 13 - 15 48]
IL_000f: ldtoken IlCode.A
IL_0014: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0019: callvirt instance bool [mscorlib]System.Type::get_IsEnum()
IL_001e: stloc.0 // typeIsEnum
// [16 13 - 16 64]
IL_001f: ldtoken IlCode.A
IL_0024: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0029: callvirt instance bool [mscorlib]System.Type::get_IsSerializable()
IL_002e: stloc.1 // typeIsSerializable
As you can see, each typeof results in a method call to Type.GetTypeFromHandle. Storing the result seems the better option to me.
Type type = typeof(A); string typeFullName = type.FullName; bool typeIsEnum = type.IsEnum; bool typeIsSerializable = type.IsSerializable;
// [14 13 - 14 35]
IL_0000: ldtoken IlCode.A
IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
// [15 13 - 15 49]
IL_000a: dup
IL_000b: callvirt instance string [mscorlib]System.Type::get_FullName()
IL_0010: stloc.0 // typeFullName
// [16 13 - 16 43]
IL_0011: dup
IL_0012: callvirt instance bool [mscorlib]System.Type::get_IsEnum()
IL_0017: stloc.1 // typeIsEnum
// [17 13 - 17 59]
IL_0018: callvirt instance bool [mscorlib]System.Type::get_IsSerializable()
IL_001d: stloc.2 // typeIsSerializable
Good thing I did it this way already!
