This post refers to:
- Simple test of Enum.GetValues() with casting type and without
- stackoverflow.com: How do I iterate over an enum?
- bartoszkp/EnumGetValuesTest.cs
I'm considering a single case here. The profit of the entire application will most likely be negligible.
Enum.GetValues returns all elements as Array
type. Array isn't strongly typed, and therefore, when we use it in foreach
statement, the compiler uses IEnumerator
to iterate through array elements.
First, it calls GetEnumerator()
, and then uses MoveNext()
and get_Current()
to iterate through the elements. Finally, it calls Dispose()
. All these operations take time unnecessarily because it is an array, and the access to the element can be direct. This is what happens if we explicitly cast type.
Let's take an example:
using System;
public class C {
enum Sizes
{
Small,
Medium,
Large
}
void Fun1()
{
foreach (Sizes s in Enum.GetValues(typeof(Sizes)))
{}
}
void Fun2()
{
foreach (Sizes s in (Sizes[])Enum.GetValues(typeof(Sizes)))
{}
}
}
And let's see how it will look after compiling to CIL code.
The following code was generated using sharplab.io (online C# compiler) in the Relase configuration. Same code, you'll see in ILSpy decompiler (from exe compiled in Release).
.class public auto ansi beforefieldinit C
extends [mscorlib]System.Object
{
// Nested Types
.class nested private auto ansi sealed Sizes
extends [mscorlib]System.Enum
{
// Fields
.field public specialname rtspecialname int32 value__
.field public static literal valuetype C/Sizes Small = int32(0)
.field public static literal valuetype C/Sizes Medium = int32(1)
.field public static literal valuetype C/Sizes Large = int32(2)
} // end of class Sizes
// Methods
//_________________________________________________________________________________
.method private hidebysig
instance void Fun1 () cil managed
{
// Method begins at RVA 0x2050
// Code size 63 (0x3f)
.maxstack 1
.locals init (
[0] class [mscorlib]System.Collections.IEnumerator,
[1] class [mscorlib]System.IDisposable
)
IL_0000: ldtoken C/Sizes
IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_000a: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
IL_000f: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator()
IL_0014: stloc.0
.try
{
IL_0015: br.s IL_0023
// loop start (head: IL_0023)
IL_0017: ldloc.0
IL_0018: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_001d: unbox.any C/Sizes
IL_0022: pop
IL_0023: ldloc.0
IL_0024: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_0029: brtrue.s IL_0017
// end loop
IL_002b: leave.s IL_003e
} // end .try
finally
{
IL_002d: ldloc.0
IL_002e: isinst [mscorlib]System.IDisposable
IL_0033: stloc.1
IL_0034: ldloc.1
IL_0035: brfalse.s IL_003d
IL_0037: ldloc.1
IL_0038: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003d: endfinally
} // end handler
IL_003e: ret
} // end of method C::Fun1
//_________________________________________________________________________________
.method private hidebysig
instance void Fun2 () cil managed
{
// Method begins at RVA 0x20ac
// Code size 40 (0x28)
.maxstack 2
.locals init (
[0] valuetype C/Sizes[],
[1] int32
)
IL_0000: ldtoken C/Sizes
IL_0005: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_000a: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
IL_000f: castclass valuetype C/Sizes[]
IL_0014: stloc.0
IL_0015: ldc.i4.0
IL_0016: stloc.1
IL_0017: br.s IL_0021
// loop start (head: IL_0021)
IL_0019: ldloc.0
IL_001a: ldloc.1
IL_001b: ldelem.i4
IL_001c: pop
IL_001d: ldloc.1
IL_001e: ldc.i4.1
IL_001f: add
IL_0020: stloc.1
IL_0021: ldloc.1
IL_0022: ldloc.0
IL_0023: ldlen
IL_0024: conv.i4
IL_0025: blt.s IL_0019
// end loop
IL_0027: ret
} // end of method C::Fun2
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x20e0
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method C::.ctor
} // end of class C
Comparing these two functions, you can see that using the IEnumerator requires more effort.
Let's now try to measure the execution time. I talked to @brogowski, @Scooletz and @norek and they advised me to use the BenchmarkDotNet tool.
Runtime environment:
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.431 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
Frequency=2343750 Hz, Resolution=426.6667 ns, Timer=TSC
[Host] : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2650.0
Clr : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2650.0
Job=Clr Runtime=Clr
...
Results:
Method | Enum elements | Mean | Error | StdDev | Faster than Fun1 |
---|---|---|---|---|---|
Fun1 | 3 | 1.685 us | 0.0336 us | 0.0622 us | |
Fun2 | 3 | 1.135 us | 0.0225 us | 0.0330 us | 1.48x |
Fun1 | 10 | 4.360 us | 0.0849 us | 0.1217 us | |
Fun2 | 10 | 2.769 us | 0.0536 us | 0.0716 us | 1.57x |
Fun1 | 100 | 39.420 us | 0.7470 us | 0.8892 us | |
Fun2 | 100 | 23.810 us | 0.4639 us | 0.6032 us | 1.66x |
Fun1 | 1000 | 397.100 us | 8.1280 us | 17.1440 us | |
Fun2 | 1000 | 234.500 us | 4.6840 us | 7.1530 us | 1.69x |
Values in columns "Mean", "Error" and "StdDev" are generated by BenchmarkDotNet. The average of the last column is 1.60x.
The results surprised me, because I thought that StopWatch really is inaccurate, but it turned out that the results are similar to my previous tests.
Summarizing. I'm not saying that you should always use cast type, because it probably will not speed up your application, but IMHO you can use it if you want. Jon Skeet also use it.
Related Links:
- stackoverflow.com: How do I iterate over an enum?
- Wikipedia: Common Intermediate Language (CIL)
- SharpLab is a .NET code playground that shows intermediate steps and results of code compilation (SharpLab source code).
- ILSpy - decompiler
- BenchmarkDotNet is a powerful .NET library for benchmarking.
- Enums.NET is a high-performance type-safe .NET enum utility library which provides many operations as convenient extension methods.
- bartoszkp/EnumGetValuesTest.cs
- stackoverflow.com: Enum.GetValues() Return Type
- stackoverflow.com: Enum in C# and foreach - Jon Skeet answeer
- stackoverflow.com: Benchmarking small code samples in C#, can this implementation be improved? - how to test the code with StopWatch
- stackoverflow.com: Calculate the execution time of a method
- stackoverflow.com: Can you loop through all enum values?