https://vishnubhagyanath.dev/blog/feed.xml

A trip through the performance of Enum.Parse() in .NET

2022-02-03

I came across a performance bottleneck when trying to develop an evaluation function for Rudim which led me to make an alternative to Enum.Parse

I had finished implementing Rudim's evaluation function and had moved on to implementing Negamax with Alpha-Beta pruning as my search algorithm. Upon completing the search algorithm, I noticed that it performed horribly - so horribly that it wasn't even evaluating moves even 2% at the speed at which it would have if it had no evaluation function.

Rudim was struggling to calculate 160,000 nodes - taking 7.5 seconds to do so, which was way too low if Rudim expected to play good chess. So I set out to try and improve this.

Finding the Culprit

I had never used a profiler before - so this was the first time and it was pretty interesting seeing it's capabilities. Using Visual Studio's built in profiler, I was able to find the culprit, a small code block that was taking 60% of CPU time. That was WAY too much for what it did.

This was the function.

private static int MirrorSquare(int square)
{
    Square originalSquare = (Square)square;
    return (int)Enum.Parse(typeof(MirroredSquare), originalSquare.ToString());
}

Such a tiny little function was hogging up 60% of the runtime of Rudim. To give a brief of what this function was intended for, I wanted a way to flip the board - e.g. square a1, if the board was just flipped would be sitting where square a8 actually was. So, what I did was I created another Enum called MirroredSquare and put this in there.

public enum Square
{
    a8, b8, c8, d8, e8, f8, g8, h8,
    a7, b7, c7, d7, e7, f7, g7, h7,
    a6, b6, c6, d6, e6, f6, g6, h6,
    a5, b5, c5, d5, e5, f5, g5, h5,
    a4, b4, c4, d4, e4, f4, g4, h4,
    a3, b3, c3, d3, e3, f3, g3, h3,
    a2, b2, c2, d2, e2, f2, g2, h2,
    a1, b1, c1, d1, e1, f1, g1, h1, NoSquare
};

public enum MirroredSquare
{
    a1, b1, c1, d1, e1, f1, g1, h1,
    a2, b2, c2, d2, e2, f2, g2, h2,
    a3, b3, c3, d3, e3, f3, g3, h3,
    a4, b4, c4, d4, e4, f4, g4, h4,
    a5, b5, c5, d5, e5, f5, g5, h5,
    a6, b6, c6, d6, e6, f6, g6, h6,
    a7, b7, c7, d7, e7, f7, g7, h7,
    a8, b8, c8, d8, e8, f8, g8, h8, NoSquare
};

Then the above implementation makes sense. It was (somewhat) clean, didn't involve too much code or magic calculations to find the mirror.

Changing the code

But now I needed a better approach, and had to try and make the simplest implementation of the above parsing.

Which led me to using a Dictionary. A map from what we want to parse - to what it should parse. This would definitely improve the performance of the code by miles. Most use cases of Enum.Parse() can be translated into a Dictionary, and if your application is seeing some performance bottlenecks, this will definitely help - you just need to find out how to model your solution.

Mine looked something like this.

private static int MirrorSquare(int square)
{
    return (int)SquareExtensions.MirroredSquare[(Square)square];
}

Now instead of just manually mapping all 64 squares to it's mirror in 64 lines of code, I did it programatically.

public static class SquareExtensions
{

    public static Dictionary<Square, Square> MirroredSquare;
    static SquareExtensions()
    {
        MirroredSquare = new Dictionary<Square, Square>();
        for (var i = 0; i < 8; ++i)
        {
            for (var j = 0; j < 8; ++j)
            {
                var square = (Square)(i * 8 + j);
                var mirror = (Square)((7 - i) * 8 + j);
                MirroredSquare[square] = mirror;
            }
        }
        MirroredSquare[Square.NoSquare] = Square.NoSquare;
    }
}

Implementing the Dictionary version brought the runtime for the same 160,000 nodes down to 1.7 seconds - a 400% improvement!

For a detailed comparison of a Dictionary based approach vs Enum.Parse() you can check out this blog post by Mariusz Wojcik.