C# delegates blow my mind

Started by
18 comments, last by -MadHatter 15 years, 11 months ago
I noticed that C# does not have an inline keyword for functions. Seeing this, I wanted to see how fast a function call was, and if it really made much of a difference putting the code inline VS putting it in a function call. I did a test executing the same code with 4 different approaches. The code executed in each approach is 9 operations (add and subtract). The 4 approaches are: 1 - putting the code all inline 2 - putting the code in a function and calling the function 3 - using a delegate to call the function 4 - putting each one of the 9 operations in their own function, and using a multicast delegate to call the 9 functions My goal was to see how many times the code could be executed in a specified length of time. I specify how long the approaches should run for and how many times they should be ran (the same values are used for all four approaches). I then take the average number of times the code was executed for each approach and compare them. I was expecting approach 1 to be the fastest, approaches 2 and 3 to be about the same, but much slower than approach 1, and approach 4 to be very slow. To my amazement appraoches 1, 2 and 3 all perform about the same, and approach 4 suffers maybe a 1% performance hit, if that. This was not the results that I was expecting. I have done the test many times, specifying different amounts of time the approaches should run for (1 - 60 seconds), and the number of times they should be ran (1 - 20, then take the average), and I get consistent results. Below is my code; I want to make sure that there is nothing I am overlooking. I use a high resolution Stopwatch to control how long each approach runs for, and I randomly pick the order that each of the 4 approaches is called in. Also, for the function calls I pass a class object as the single parameter, which is the object to update, and I make sure I'm not running any other applications when I do the test. If you can spot a potential problem in my code (or something that I am not considering) let me know. I just found this very interesting and thought I would share it.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;

namespace Delegate_Speed_Test
{
    public partial class Form1 : Form
    {
        // Class to hold data to update
        class CParticle
        {
            public float f1;
            public float f2;
            public float f3;
            public int i4;
            public int i5;
            public int i6;
            public string s7;
            public string s8;
            public long l9;

            public CParticle()
            {
                f1 = f2 = f3 = 0.0f;
                i4 = i5 = i6 = 0;
                s7 = s8 = "";
                l9 = 0;
            }
        }

        // Define the Delegate function structure
        delegate void UpdateDelegate(CParticle cParticle);

        // Variables used to calculate the average number of times executed
        long mlNumberOfTimesRanInline = 0;
        long mlNumberOfTimesRanFunction = 0;
        long mlNumberOfTimesRanDelegate = 0;
        long mlNumberOfTimesRanMulticastDelegate = 0;
        
        Random mcRandom = new Random();
                

        public Form1()
        {
            InitializeComponent();

            UpdateShownTimeNeeded();
        }

        // Show how long the test will take to run using the given duration and number of times to run
        private void UpdateShownTimeNeeded()
        {
            float fTimeInSeconds = (float)(numericNumberOfTimesToRun.Value * numericLengthOfTimeToRun.Value * 4);
            float fTimeInMinutes = fTimeInSeconds / 60.0f;

            labelTimeNeededToRunInSeconds.Text = fTimeInSeconds.ToString();
            labelTimeNeededToRunInMinutes.Text = fTimeInMinutes.ToString();
        }

        // Start the test
        private void buttonStart_Click(object sender, EventArgs e)
        {
            // Reset the number of times each has ran
            mlNumberOfTimesRanInline = 0;
            mlNumberOfTimesRanFunction = 0;
            mlNumberOfTimesRanDelegate = 0;
            mlNumberOfTimesRanMulticastDelegate = 0;

            int iIndex = 0;
            for (iIndex = 0; iIndex < numericNumberOfTimesToRun.Value; iIndex++)
            {
                // Call the functions in a random order
                switch ((int)mcRandom.Next(0, 10))
                {
                    default:
                    case 0:
                        Inline();
                        Function();
                        Delegate();
                        MulticastDelegate();
                    break;

                    case 1:
                        Function();
                        Delegate();
                        MulticastDelegate();
                        Inline();
                    break;

                    case 2:
                        Delegate();
                        MulticastDelegate();
                        Inline();
                        Function();
                    break;

                    case 3:
                        MulticastDelegate();
                        Inline();
                        Function();
                        Delegate();
                    break;

                    case 4:
                        MulticastDelegate();
                        Delegate();
                        Function();
                        Inline();
                    break;

                    case 5:
                        Inline();
                        MulticastDelegate();
                        Delegate();
                        Function();
                    break;

                    case 6:
                        Function();
                        Inline();
                        MulticastDelegate();
                        Delegate();
                    break;

                    case 7:
                        Delegate();
                        Function();
                        Inline();
                        MulticastDelegate();
                        break;

                    case 8:
                        Inline();
                        Delegate();
                        Function();
                        MulticastDelegate();
                    break;

                    case 9:
                        MulticastDelegate();
                        Function();
                        Delegate();
                        Inline();
                    break;
                }
            }

            // Display Inline Info
            double dAverageTimesRanInline = (double)mlNumberOfTimesRanInline / (double)numericNumberOfTimesToRun.Value;
            labelNumberOfTimesInline.Text = dAverageTimesRanInline.ToString("#.###");
            labelNumberOfTimesInlinePercent.Text = "100.0%";

            // Display Function Info
            double dAverageTimesRanFunction = (double)mlNumberOfTimesRanFunction / (double)numericNumberOfTimesToRun.Value;
            labelNumberOfTimesFunction.Text = dAverageTimesRanFunction.ToString("#.###");
            float fPercent = (float)((dAverageTimesRanFunction / dAverageTimesRanInline) * 100.0f);
            labelNumberOfTimesFunctionPercent.Text = fPercent.ToString() + "%";

            // Display Delegate Info
            double dAverageTimesRanDelegate = (double)mlNumberOfTimesRanDelegate / (double)numericNumberOfTimesToRun.Value;
            labelNumberOfTimesDelegate.Text = dAverageTimesRanDelegate.ToString("#.###");
            fPercent = (float)((dAverageTimesRanDelegate / dAverageTimesRanInline) * 100.0f);
            labelNumberOfTimesDelegatePercent.Text = fPercent.ToString() + "%";

            // Display MulticastDelegate Info
            double dAverageTimesRanMulitcastDelegate = (double)mlNumberOfTimesRanMulticastDelegate / (double)numericNumberOfTimesToRun.Value;
            labelNumberOfTimesMulticastDelegate.Text = dAverageTimesRanMulitcastDelegate.ToString("#.###");
            fPercent = (float)((dAverageTimesRanMulitcastDelegate / dAverageTimesRanInline) * 100.0f);
            labelNumberOfTimesMulticastDelegatePercent.Text = fPercent.ToString() + "%";
        }

        // Approach 1 - Code inline
        private void Inline()
        {
            long lNumberOfTimesExecuted = 0;
            long lAmountOfTimeToRunFor = (long)(numericLengthOfTimeToRun.Value * 1000);

            CParticle cParticle = new CParticle();
            Stopwatch cStopwatch = new Stopwatch();
            cStopwatch.Start();

            while (cStopwatch.ElapsedMilliseconds < lAmountOfTimeToRunFor)
            {
                lNumberOfTimesExecuted++;

                cParticle.f1 += 1.5f;
                cParticle.f2 = 15.567f;
                cParticle.f3 -= 0.0001f;
                cParticle.i4 += 3;
                cParticle.i5 = 23345;
                cParticle.i6 -= 7;
                cParticle.s7 = "Hello";
                cParticle.s8 += "A";
                cParticle.l9 += 123;
            }

            mlNumberOfTimesRanInline += lNumberOfTimesExecuted;
        }

        // Approach 2 - Code in a function call
        private void Function()
        {
            long lNumberOfTimesExecuted = 0;
            long lAmountOfTimeToRunFor = (long)(numericLengthOfTimeToRun.Value * 1000);

            CParticle cParticle = new CParticle();
            Stopwatch cStopwatch = new Stopwatch();
            cStopwatch.Start();

            while (cStopwatch.ElapsedMilliseconds < lAmountOfTimeToRunFor)
            {
                lNumberOfTimesExecuted++;

                Update(cParticle);
            }

            mlNumberOfTimesRanFunction += lNumberOfTimesExecuted;
        }

        // Approach 3 - Code in a function call, called from a delegate
        private void Delegate()
        {
            long lNumberOfTimesExecuted = 0;
            long lAmountOfTimeToRunFor = (long)(numericLengthOfTimeToRun.Value * 1000);
            
            UpdateDelegate MyDelegate = new UpdateDelegate(Update);

            CParticle cParticle = new CParticle();
            Stopwatch cStopwatch = new Stopwatch();
            cStopwatch.Start();

            while (cStopwatch.ElapsedMilliseconds < lAmountOfTimeToRunFor)
            {
                lNumberOfTimesExecuted++;

                MyDelegate(cParticle);
            }

            mlNumberOfTimesRanDelegate += lNumberOfTimesExecuted;
        }

        // Approach 4 - Code in several function calls, each called from a multicast delegate
        private void MulticastDelegate()
        {
            long lNumberOfTimesExecuted = 0;
            long lAmountOfTimeToRunFor = (long)(numericLengthOfTimeToRun.Value * 1000);
            
            UpdateDelegate MyDelegate = null;
            MyDelegate += new UpdateDelegate(Update1);
            MyDelegate += new UpdateDelegate(Update2);
            MyDelegate += new UpdateDelegate(Update3);
            MyDelegate += new UpdateDelegate(Update4);
            MyDelegate += new UpdateDelegate(Update5);
            MyDelegate += new UpdateDelegate(Update6);
            MyDelegate += new UpdateDelegate(Update7);
            MyDelegate += new UpdateDelegate(Update8);
            MyDelegate += new UpdateDelegate(Update9);

            CParticle cParticle = new CParticle();
            Stopwatch cStopwatch = new Stopwatch();
            cStopwatch.Start();

            while (cStopwatch.ElapsedMilliseconds < lAmountOfTimeToRunFor)
            {
                lNumberOfTimesExecuted++;

                MyDelegate(cParticle);
            }

            mlNumberOfTimesRanMulticastDelegate += lNumberOfTimesExecuted;
        }

        // Function containing code to run
        private void Update(CParticle cParticle)
        {
            cParticle.f1 += 1.5f;
            cParticle.f2 = 15.567f;
            cParticle.f3 -= 0.0001f;
            cParticle.i4 += 3;
            cParticle.i5 = 23345;
            cParticle.i6 -= 7;
            cParticle.s7 = "Hello";
            cParticle.s8 += "A";
            cParticle.l9 += 123;
        }

        // Functions containing code to run (spread across several functions)
        private void Update1(CParticle cParticle)
        {
            cParticle.f1 += 1.5f;
        }

        private void Update2(CParticle cParticle)
        {
            cParticle.f2 = 15.567f;
        }

        private void Update3(CParticle cParticle)
        {
            cParticle.f3 -= 0.0001f;
        }

        private void Update4(CParticle cParticle)
        {
            cParticle.i4 += 3;
        }

        private void Update5(CParticle cParticle)
        {
            cParticle.i5 = 23345;
        }

        private void Update6(CParticle cParticle)
        {
            cParticle.i6 -= 7;
        }

        private void Update7(CParticle cParticle)
        {
            cParticle.s7 = "Hello";
        }

        private void Update8(CParticle cParticle)
        {
            cParticle.s8 += "A";
        }

        private void Update9(CParticle cParticle)
        {
            cParticle.l9 += 123;
        }


        // If the user changed how long each approach should run for
        private void numericLengthOfTimeToRun_ValueChanged(object sender, EventArgs e)
        {
            UpdateShownTimeNeeded();
        }

        // If the user changed how many times each approach should be run
        private void numericNumberOfTimesToRun_ValueChanged(object sender, EventArgs e)
        {
            UpdateShownTimeNeeded();
        }
    }
}


-Dan- Can't never could do anything | DansKingdom.com | Dynamic Particle System Framework for XNA
Advertisement
Quote:Original post by deadlydog
I noticed that C# does not have an inline keyword for functions.

Most C++ compilers ignore the "inline" keyword when determining whether or not to inline a function. Why should C# be any different?

The real issue, though, is JIT compiling, which is ideal for dynamic dispatch situations in which the same function is always chosen. JIT engines optimize for this situation (as do C++ compilers, in some circumstances), meaning that the difference between a delegate call and explicitly inlined code is usually just a pointer comparison.

EDIT: Actually, now that I look at it, the .NET runtime doesn't seem to do this.

[Edited by - Sneftel on May 14, 2008 11:40:17 AM]
You should perhaps read my blog posts on this very subject. Inlining in .Net is actually quite different than perhaps what you're used to.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Quote:Original post by Sneftel
Quote:Original post by deadlydog
I noticed that C# does not have an inline keyword for functions.

Most C++ compilers ignore the "inline" keyword when determining whether or not to inline a function. Why should C# be any different?

Yes, I know. the "inline" keyword is more of a hint to the compiler, rather than a strict rule it must follow. I was just blown away by how fast the function calls are; for example, in my test both the inline approach and the function call approach can execute the code 22000 times per second, even though the function call approach is making 22000 more function calls than the inline approach. So it seems that calling a function takes virtually no time at all. However, as I pointed out above, this is when passing a single parameter to the function, and it's passed by reference. If you were passing 10 parameters by value to the function, that may slow things down a bit.......hmmmm, I think I'll try that out.
-Dan- Can't never could do anything | DansKingdom.com | Dynamic Particle System Framework for XNA
Quote:Original post by deadlydog
So it seems that calling a function takes virtually no time at all.

This is definitely true when the function call is in a correctly predicted branch. Modern processors see the branch coming up and prefetch the branched-to instructions, meaning that execution continues similarly to if there was no branch at all.
reference types are passed by reference. value types are passed by value. anything declared as a struct is a value type, while things declared class are reference types.

default behavior for reference type passing is "sort of" by reference but not exactly. if you assign your parameter value to a new instance, it creates a new reference and the value of the variable passed into the method remains unchanged. modifying members of the reference type parameter will work like a by reference call.

the following method is how you really pass by reference in C# for both reference and value types.

public void Foo(ref MyObject mo) {    // ...}
Quote:Original post by -MadHatter
reference types are passed by reference. value types are passed by value. anything declared as a struct is a value type, while things declared class are reference types.

You seem to be confusing reference types with reference passing. C# passes both value and reference types by value, unless the ref keyword is given. The remainder of your post shows that you understand the practical upshot of C#'s system, but you should get your terminology straight. There's nothing "sort of" about C#'s evaluation strategy. ref variables are passed by reference; everything else, value type or reference type, is passed by value.
Quote:Original post by Sneftel
Quote:Original post by -MadHatter
reference types are passed by reference. value types are passed by value. anything declared as a struct is a value type, while things declared class are reference types.

You seem to be confusing reference types with reference passing. C# passes both value and reference types by value, unless the ref keyword is given. The remainder of your post shows that you understand the practical upshot of C#'s system, but you should get your terminology straight. There's nothing "sort of" about C#'s evaluation strategy. ref variables are passed by reference; everything else, value type or reference type, is passed by value.

So does this mean that it's faster to pass my class object with the ref keyword (since you say this is the only way to make it actually pass the reference (i.e. pointer in c++ speak) of the object, not a copy of it)?
Thanks
-Dan- Can't never could do anything | DansKingdom.com | Dynamic Particle System Framework for XNA
Quote:Original post by Sneftel
Quote:Original post by -MadHatter
reference types are passed by reference. value types are passed by value. anything declared as a struct is a value type, while things declared class are reference types.

You seem to be confusing reference types with reference passing. C# passes both value and reference types by value, unless the ref keyword is given. The remainder of your post shows that you understand the practical upshot of C#'s system, but you should get your terminology straight. There's nothing "sort of" about C#'s evaluation strategy. ref variables are passed by reference; everything else, value type or reference type, is passed by value.

Heh, that's one thing that's so much easier to explain using C++ :D.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Quote:Original post by deadlydog
since you say this is the only way to make it actually pass the reference (i.e. pointer in c++ speak) of the object, not a copy of it

He didn't say that at all. If you pass a reference type, the reference gets passed by value, NOT THE OBJECT. You only need to pass reference types by reference if you want to change the reference (NOT THE OBJECT) inside of the function.

This topic is closed to new replies.

Advertisement