Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@marbel82
Last active August 18, 2022 13:18
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marbel82/5bfa2010b534c681a3c618a98719c862 to your computer and use it in GitHub Desktop.
Save marbel82/5bfa2010b534c681a3c618a98719c862 to your computer and use it in GitHub Desktop.
When we iterate on the Enum values, why it is faster when we explicitly cast type?

This post refers to:


When we iterate on the Enum values, why it is faster when we explicitly cast type?

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment