26

I bet I could answer that myself if I knew more about tools to analyze how C#/JIT behaves but since I don't, please bear with me asking.

I have simple code like this :

    private SqlMetaData[] meta;

    private SqlMetaData[] Meta
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get
        {
            return this.meta;
        }
    }

As you can see I put AggressiveInlining because I feel like it should be inlined.
I think. There's no guarantee the JIT would inline it otherwise. Am I wrong?

Could doing this kind of thing hurt the performance/stability/anything?

Serge
  • 901

4 Answers4

26

Compilers are smart beasts. Usually, they'll automatically squeeze out as much performance as they can from anywhere they can.

Trying to outsmart the compiler doesn't usually make a big difference, and has a lot of chances to backfire. For instance, inlining makes your program bigger as it duplicates the code everywhere. If your function is used in a lot of places throughout the code, it might actually be detrimential as pointed out @CodesInChaos. If it is obvious the function should be inlined, you can bet the compiler will do so.

In case of hesitation, you can still do both and compare if there is any performance gain, that's the only certain way to now. But my bet is the difference will be neglegible, the source code will just be "noisier".

dagnelies
  • 5,493
13

EDIT: I realize my answer didn't exactly answer the question, while there is no real downside, from my timing results there is no real upside either. The difference between a inline property getter is 0.002 seconds over 500 million iterations. My test case may also not be 100% accurate since its using a struct because there are some caveats to the jitter and inlining with structs.

As always, the only way to really know is to write a test and figure it out. Here are my results with the following configuration:

Windows 7 Home  
8GB ram  
64bit os  
i5-2300 2.8ghz  

Empty project with the following settings:

.NET 4.5  
Release mode  
Start without debugger attached - CRUCIAL  
Unchecked "Prefer 32-bit" under project build settings  

Results

struct get property                               : 0.3097832 seconds
struct inline get property                        : 0.3079076 seconds
struct method call with params                    : 1.0925033 seconds
struct inline method call with params             : 1.0930666 seconds
struct method call without params                 : 1.5211852 seconds
struct intline method call without params         : 1.2235001 seconds

Tested with this code:

class Program
{
    const int SAMPLES = 5;
    const int ITERATIONS = 100000;
    const int DATASIZE = 1000;

    static Random random = new Random();
    static Stopwatch timer = new Stopwatch();
    static Dictionary<string, TimeSpan> timings = new Dictionary<string, TimeSpan>();

    class SimpleTimer : IDisposable
    {
        private string name;
        public SimpleTimer(string name)
        {
            this.name = name;
            timer.Restart();
        }

        public void Dispose()
        {
            timer.Stop();
            TimeSpan ts = TimeSpan.Zero;
            if (timings.ContainsKey(name))
                ts = timings[name];

            ts += timer.Elapsed;
            timings[name] = ts;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct
    {
        private int x;
        public int X { get { return x; } set { x = value; } }
    }


    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct2
    {
        private int x;

        public int X
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get { return x; }
            set { x = value; }
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct3
    {
        private int x;
        private int y;

        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct4
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct5
    {
        private int x;
        private int y;

        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct6
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    static void RunTests()
    {
        for (var i = 0; i < SAMPLES; ++i)
        {
            Console.Write("Sample {0} ... ", i);
            RunTest1();
            RunTest2();
            RunTest3();
            RunTest4();
            RunTest5();
            RunTest6();
            Console.WriteLine(" complate");
        }
    }

    static int RunTest1()
    {
        var data = new TestStruct[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static int RunTest2()
    {
        var data = new TestStruct2[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct inline get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static void RunTest3()
    {
        var data = new TestStruct3[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest4()
    {
        var data = new TestStruct4[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct inline method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest5()
    {
        var data = new TestStruct5[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void RunTest6()
    {
        var data = new TestStruct6[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct intline method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void Main(string[] args)
    {
        RunTests();
        DumpResults();
        Console.Read();
    }

    static void DumpResults()
    {
        foreach (var kvp in timings)
        {
            Console.WriteLine("{0,-50}: {1} seconds", kvp.Key, kvp.Value.TotalSeconds);
        }
    }
}
9

You are right - there is no way to guarantee that the method would be inlined - MSDN MethodImplOptions Enumeration, SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut.

Programmers are more intelligent than a compiler, but we work on a higher level and our optimizations are products of one man's work - our own. Jitter sees what's going during the execution. It can analyze both the execution flow and the code according to the knowledge put into it by its designers. You can know your program better, but they know better the CLR. And who will be more correct in his optimizations? We don't know for sure.

That's why you should test any optimization you make. Even if it is very simple. And take into account that the environment may change and your optimization or disoptimization can have quite an unexpected result.

6

Compilers do a lot of optimizations. Inlining is one of them, whether the programmer wanted or not. For example, MethodImplOptions does not have an "inline" option. Because inlining is automatically done by the compiler if needed.

Many other optimizations are especially done if enabled from the build options, or "release" mode will do this. But these optimizations are kind of "worked for you, great! Not worked, leave it" optimizations and usually give better performance.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

is just a flag for the compiler that an inlining operation is really wanted here. More info here and here

To answer your question;

There's no guarantee the JIT would inline it otherwise. Am I wrong?

True. No guarantee; Neither C# has a "force inlining" option.

Could doing this kind of thing hurt the performance/stability/anything?

In this case no, as it is said in Writing High-Performance Managed Applications : A Primer

Property get and set methods are generally good candidates for inlining, since all they do is typically initialize private data members.

myuce
  • 169