Skip to content

Proposal: High performance code with readonly parameters within methods #12758

@devedse

Description

@devedse

Introduction

I've been building quite some algorithms in C# and doing this you often want to add some code where you can do things like logging progress or some other intermediate things that should only be executed if a certain flag is set to true. For example, let's take the following method:

        public static void DoSomething(int count, Action<int, int> progress)
        {
            for (int i = 0; i < count; i++)
            {
                if (progress != null)
                {
                    progress(i, count);
                }

                //Do something, e.g. sorting or w/e
            }
        }

If you would pass null as the value for progress it would still execute the if check. Which does for a lot of these low level algorithms actually impact the performance by quite some percentage.

In this case it would ofcourse be easy to just implement the method twice, one with the call to progress and one without, but as we'd all hate code duplication, let's see what else we could do.

Actual measurements

Do back up all my crazy claims let's look at a Maze generation algorithm that I've written. It's a simple implementation of the Backtrack algorithm:

        private InnerMap GoGenerateInternal(InnerMap map, IRandom random, Action<int, int, long, long> pixelChangedCallback)
        {
            if (pixelChangedCallback == null)
            {
                pixelChangedCallback = (vvv, yyy, zzz, www) => { };
            }

            long totSteps = (map.Width - 1L) / 2L * ((map.Height - 1L) / 2L);
            long currentStep = 1;

            int width = map.Width;
            int height = map.Height;
            int x = 1;
            int y = 1;

            var stackje = new Stack<MazePoint>();
            stackje.Push(new MazePoint(x, y));
            map[x, y] = true;

            pixelChangedCallback.Invoke(x, y, currentStep, totSteps);

            MazePoint[] targets = new MazePoint[4];

            while (stackje.Count != 0)
            {
                MazePoint cur = stackje.Peek();
                x = cur.X;
                y = cur.Y;

                int targetCount = 0;
                if (x - 2 > 0 && !map[x - 2, y])
                {
                    targets[targetCount].X = x - 2;
                    targets[targetCount].Y = y;
                    targetCount++;
                }
                if (x + 2 < width - 1 && !map[x + 2, y])
                {
                    targets[targetCount].X = x + 2;
                    targets[targetCount].Y = y;
                    targetCount++;
                }
                if (y - 2 > 0 && !map[x, y - 2])
                {
                    targets[targetCount].X = x;
                    targets[targetCount].Y = y - 2;
                    targetCount++;
                }
                if (y + 2 < height - 1 && !map[x, y + 2])
                {
                    targets[targetCount].X = x;
                    targets[targetCount].Y = y + 2;
                    targetCount++;
                }

                if (targetCount > 0)
                {
                    var target = targets[random.Next(targetCount)];
                    stackje.Push(target);
                    map[target.X, target.Y] = true;

                    currentStep++;

                    if (target.X < x)
                    {
                        map[x - 1, y] = true;
                        pixelChangedCallback.Invoke(x - 1, y, currentStep, totSteps);
                    }
                    else if (target.X > x)
                    {
                        map[x + 1, y] = true;
                        pixelChangedCallback.Invoke(x + 1, y, currentStep, totSteps);
                    }
                    else if (target.Y < y)
                    {
                        map[x, y - 1] = true;
                        pixelChangedCallback.Invoke(x, y - 1, currentStep, totSteps);
                    }
                    else if (target.Y > y)
                    {
                        map[x, y + 1] = true;

                        pixelChangedCallback.Invoke(x, y + 1, currentStep, totSteps);
                    }

                    pixelChangedCallback.Invoke(target.X, target.Y, currentStep, totSteps);
                }
                else
                {
                    stackje.Pop();
                }
            }

            return map;
        }

If the pixelChangedCallback is provided it will call this method 2 times per loop. However if it's not provided, it stubs it with an action that does nothing which is then being called.

If I completely remove the calls to the pixelChangedCallback we're looking at maze generation times of about 5.1 seconds. If I put a boolean around it the algorithm runs in 5.6 seconds and if I put the callback to null it takes about 5.7 seconds.

Implementing a boolean was done by doing a check once and then simply using the bool value everywhere else.

E.g.:

// At the start
bool isNullCallback = pixelChangedCallback == null;

// In the rest of the code
if (!isNullCallback)
{
    //Invoke...
}
Implementation Average Duration
No callback 5.1 seconds
if check around callback 5.6 seconds
callback set to () => {} 5.7 seconds

It's ofcourse also possible to implement the whole algorithm with just one giant if check around it, but in my oppinion it doesn't produce clean code.

Proposal

Instead of trying to fix this with the current capabilities of C# / CoreCLR I think we might be able to implement something in the Compiler / JIT that could solve this.

Readonly parameters (or something similar)

If we could mark parameters in a method as readonly, the compiler could start making assumptions based on them to optimize the code execution path.

For example, if that Action would've been made null, all checks on the object itself (so not Properties of the object) which are compared to other readonly/constants could be assumed as always false or true.

This would probably mean we'd have multiple instances of the same method to be compiled, but we could quite easily limit this to only null checks / constants / or booleans for example.

If we would do this, code could still look the same, but using the knowledge that's available in the compiler could speed up high performance code.

Example implemenatation

An example of this implementation could be something like this:

        private InnerMap GoGenerateInternal(InnerMap map, IRandom random, Action<int, int, long, long> readonly pixelChangedCallback)
        {
            ....

What do you guys think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions