21

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?

Ender Look
  • 105
  • 5
Serge
  • 861
  • 2
  • 6
  • 12
  • 4
    1) In my experience such primitive methods will be inlined without the attribute. I mainly found the attribute useful with non trivial methods that should still be inlined. 2) There is no guarantee that a method decorated with the attribute will be inlined either. It's merely a hint to the JITter. – CodesInChaos Jun 23 '14 at 15:32
  • I don't know much about the new inlining attribute, but putting one here is almost certainly not going to make any difference in performance. All you are doing is returning a reference to an array, and the JIT will almost certainly already be making the correct choice here. – Robert Harvey Jun 23 '14 at 15:33
  • 17
    3) Inlining too much means the code becomes larger, and might not fit into caches anymore. Cache misses can be a significant performance hit. 4) I recommend not using the attribute until a benchmark showed that it improves performance. – CodesInChaos Jun 23 '14 at 15:33
  • 5
    Quit worrying. The more you try to outsmart the compiler, the more it will find ways to outsmart you. Find something else to worry about. – david.pfx Jun 24 '14 at 04:03
  • 2
    For my two cents, I've seen great gains in release mode, especially when calling a larger function in a tight loop. – jjxtra Aug 11 '17 at 14:23

4 Answers4

24

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,415
  • 3
  • 20
  • 26
  • 4
    I think the “noise” is the most important point here. Keep your code tidy and trust your compiler to do the right thing until proven otherwise. Everything else is a dangerous premature optimization. – 5gon12eder Aug 17 '15 at 19:45
  • So *NoInlining* will make my program smaller! – Den Jan 21 '16 at 17:41
  • 1
    If compilers are so smart, then why would trying to outsmart the compiler backfire? – Little Endian Apr 27 '16 at 15:57
  • 32
    Compilers are not *smart*. Compilers do not do "the right thing". Do not attribute intelligence where it is not. In fact, C# compiler / JITer is excessively dumb. For example, it won't inline anything past 32 bytes IL or cases involving `struct`s as parameters - where in quite many cases it should and could. In addition to missing hundreds of *obvious* optimizations - including, but not limited to - avoiding unnecessary bounds checks and allocations among other things. – JBeurer Dec 06 '16 at 11:19
  • @Den It depends. If a small piece of code is inlined, and then further optimisations are done, the nett result can be smaller size, and `NoInlining` will prevent that, with the nett result of larger size. – Jon Hanna Jan 06 '17 at 21:45
  • @JBeurer I don't know what version of C# you are using. Bounds checking, loop unrolling, and variable enregestering have been around from the beginning. Maybe you are looking at a build with 'Enable Optimizations' off? Realize that the C# compiler makes a small number of optimizations. The JIT is the main workhorse for optimization. I'm curious why you think those optimizations do not exist in the compiler/JIT? Have you looked in the disassembled x86/x64 code? I can see it if you examine a build with 'Enable Optimizations' turned on. – Dave Black Aug 30 '18 at 21:19
  • 10
    @DaveBlack Bounds check elusion in C# happens in very small list of very basic cases, usually on the most basic sequentual for loops performed, and even then many simple loops fail being optimized. Multi-dimensional array loops do not get bounds check elimination, loops iterated in descending order doesn't, loops on newly allocated arrays don't. Very many simple cases where you'd expect compiler to do it's job. But it doesn't. Because it's anything, but smart. https://blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/array-bounds-check-elimination-in-the-clr/ – JBeurer Sep 03 '18 at 07:10
  • @JBeurerj that was 9 years ago! I feel pretty confident that they've made significant progress since then. As you read in the article, there's a lot to figuring out how to do the optimizations and there's a cost for whichever option you choose. My experience is that anytime you try to outsmart the JIT compiler you either have little to no effect and in the worst case actually slow down your code! (Trying to manually hoist an array length check). The bottom line is the best technique is to write the best code you can and profile your code frequntly. Address issues only once identified. – Dave Black Sep 07 '18 at 03:53
  • 7
    Compilers are not "smart beasts". They simply apply a bunch of heuristics and make trade-offs to try and find a balance for the majority of scenarios that were anticipated by the compiler writers. I suggest reading: https://docs.microsoft.com/en-us/previous-versions/dotnet/articles/ms973858(v=msdn.10)#highperfmanagedapps_topic10 – cdiggins Jan 06 '19 at 23:42
11

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);
        }
    }
}
Chris Phillips
  • 166
  • 1
  • 4
8

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.

Eugene Podskal
  • 497
  • 2
  • 7
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
  • 1
  • 3