11

I have a list of items implementing an interface. For the question, let's use this example interface:

interface Person
{
  void AgeAYear();
}

There are two classes

class NormalPerson : Person
{
  int age = 0;

  void AgeAYear()
  {
    age++;
    //do some more stuff...
  }
}


class ImmortalPerson : Person
{
  void AgeAYear()
  {
    //do nothing...
  }
}

For other reasons, I need them both of the list. But for this call, when I loop through my list of Persons, I may be calling empty functions. Will this have a performance impact? If so, how much? Will the empty function, for all intents and purposes, be optimized out?


NOTE: In the real example, the ImmortalPerson has other methods that do have code - it is not just an object that does nothing.

user664939
  • 1,977
  • 2
  • 20
  • 35
  • i thought the definition of immortal was "a being that lives forever," but not really a being that does not age. – NullUserException Aug 25 '11 at 13:52
  • There are many contributors to the amount of time required to call an empty function, but for the sake of rough performance calculations, you can assume 1ns. – Gabe Aug 25 '11 at 13:55

5 Answers5

10

Will this have a performance impact?

Highly unlikely to have a meaningful performance impact.

If so, how much?

You can quantify it exactly for your specific code paths using a profiler. We can not, because we don't know the code paths. We can guess, and tell you it almost surely doesn't matter because this is extremely unlikely to be a bottleneck in your application (are you really sitting there calling Person.AgeAYear in a tight loop?).

Only you can find out precisely by getting out a profiler and measuring.

Will the empty function, for all intents and purposes, be optimized out?

It's certainly possible but it might not; it might even change in future version of the JITter, or change from platform to platform (different platforms have different JITters). If you really want to know, compile your application, and look at the disassembled JITted code (not the IL!).

But I'll say this: this is almost surely, almost definitely not something worth worrying about or putting any time into. Unless you are calling Person.AgeAYear in a tight loop in performance critical code, it's not a bottleneck in your application. You could spend time on this, or you could spend time improving your application. Your time has an opportunity cost too.

jason
  • 236,483
  • 35
  • 423
  • 525
3
  • Will this have a performance impact?

Yes Probably, if the function is called then the call itself will take a small amount of time.

  • If so, by how much?

You will never notice the difference in any real application - the cost of calling a method is very small compared to the cost of doing any "real" work.

  • Will the empty function, for all intents and purposes, be optimized out?

I doubt it - the CLR definitely probably won't perform this sort of optimisation if the method is in a different assembly as the method may change in the future. It might be feasible that this sort of optimisation is done for method calls inside the assembly but it would depend a lot on the code, for example in the following sample:

foreach (IPerson person in people)
{
    person.AgeAYear();
}

The method call can't be optimised out because a different implementation of IPerson might be supplied which actually does something in this method. This would certainly be the case for any calls against the IPerson interface where the compiler can't prove that its always working with a ImmortalPerson instance.

Ultimately you have to ask yourself "What is the alternative?" and "Does this really have a large enough impact to warrant an alternative approach?" . In this case the impact will be very small - I would say that in this case having an empty method in this way is perfectly acceptable.

Justin
  • 84,773
  • 49
  • 224
  • 367
  • "the CLR doesn't perform this sort of optimisation." - That's surprising to me. I thought it can do inlining just as the JVM. Do you know why the difference? – Péter Török Aug 25 '11 at 13:56
  • 1
    @Justin: I don't agree with your assessment that the CLR doesn't perform that sort of optimization. The C# compiler does not, but I think the JITter can and likely (it can certainly inline it, and this seems like a good candidate for inlining, and then it just becomes the empty body). (Edit: This comment rendered somewhat obsolete by your edits to your statement.) – jason Aug 25 '11 at 13:58
  • @Justin: Thinking about it more, I'm not sure if I agree even with your clarification. We are talking about the JITter here. The JITter will know at runtime the method being called, even if the assembly has changed from the last time it ran. It has the information it needs at runtime to know whether or not this method is a good candidate for inlining. – jason Aug 25 '11 at 14:13
  • @Jason I'm not so sure any more - I'm swinging back and forth between "it can't because it would break things" and "but maybe it could if it did this instead". I'm going to have an experiment... – Justin Aug 25 '11 at 14:17
  • @Jason Well a native image is invalidated when any referenced assemblies changes, so I'm no longer sure that cross-assembly calls would be invalidated. – Justin Aug 25 '11 at 14:30
  • @Justin: Yeah, I wasn't think of NGEN'ed images. – jason Aug 25 '11 at 15:34
  • What if you add code to the function at runtime? Or exchange it? It still has to be there... Even if empty. – HankTheTank Jul 22 '18 at 16:15
3

Your logic seems faulty to me, and regardless of the performace impact, calling an empty method smells of poor design.

In your situation, you have one interface which is Of Person. You are stating that in order to be a person, you must be able to age, as enforced by your AgeAYear method. However, according to the logic in your AgeAYear method (or lack thereof), an ImmortalPerson cannot age, but can still be a Person. Your logic contradicts itself. You could attack this problem a number of ways, however this is the first that pops into my head.

One way to accomplish this would be to set up two interfaces:

interface IPerson { void Walk(); }
interface IAgeable : IPerson { void AgeAYear();}

You can now clearly distinguish that you do not have to age to be a person, but in order to age, you must be a person. e.g.

class ImmortalPerson : IPerson
{
    public void Walk()
    {
        // Do Something
    }
}

class RegularPerson : IAgeable
{
    public void AgeAYear()
    {
        // Age A Year
    }

    public void Walk()
    {
       // Walk
    }
}

Thus, for your RegularPerson, by implementing IsAgeable, you also are required to implement IPerson. For your ImmortalPerson, you only need to implement IPerson.

Then, you could do something like below, or a variation thereof:

List<IPerson> people = new List<IPerson>();

people.Add(new ImmortalPerson());
people.Add(new RegularPerson());

foreach (var person in people)
{
   if (person is IAgeable)
   {
      ((IAgeable)person).AgeAYear();
   }
}

Given the setup above, your still enforcing that your classes must implement IPerson to be considered a person, but can only be aged if they also implement IAgeable.

George Johnston
  • 31,652
  • 27
  • 127
  • 172
1

On a relatively modern workstation, a C# delegate or interface call takes around 2 nanoseconds. For comparison:

  • Allocating a small array: 10 nanoseconds
  • Allocating a closure: 15 nanoseconds
  • Taking an uncontested lock: 25 nanoseconds
  • Dictionary lookup (100 short string keys): 35 nanoseconds
  • DateTime.Now (system call): 750 nanoseconds
  • Database call over wired network (count on a small table): 1,000,000 nanoseconds (1 millisecond)

So unless you are optimizing a tight loop, method calls are not likely to be a bottleneck. If you are optimizing a tight loop, consider a better algorithm, such as indexing things in a Dictionary before processing them.

I tested these on a Core i7 3770 at 3.40 GHz, using LINQPad 32-bit with optimization turned on. But due to inlining, optimization, register allocation, and other compiler/JIT behavior, the timing of a method call will vary wildly depending on context. 2 nanoseconds is just a ballpark figure.

In your case, you are looping through lists. The looping overhead is likely to dominate the method call overhead, since looping internally involves lots of method calls. Performance is not likely to be an issue in your case, but if you have millions of items and/or you regularly need to update them, consider changing how you represent the data. For example, you could have a single "year" variable that you increment globally instead of incrementing everyone's "age".

Joey Adams
  • 41,996
  • 18
  • 86
  • 115
1

There is no way for the compiler to understand which of the two functions is going to be called, as that is a function pointer set at run time.

You could avoid it, through checking of some variable inside Person, determining its type or use of dynamic_cast to check it. If the function didn't need calling, then you can ignore it.

Calling a function consists of a few instructions:

  • pushing the arguments on the process stack (none in this case)
  • pushing return addresses and a few other data
  • jumping to the function

And when the function ends:

  • jumping back from the function
  • changing stack pointer to effectively remove stack frame of the called function

It may look a lot to you, but perhaps it is just maybe twice or three times the cost of checking the type of the variable and avoiding the call (in the other case, you have a check of some variable and a possible jump, which takes almost the same time as calling an empty function. You would only be saving the return cost. However, you do the check for the functions that DO need to be called, so in the end you are probably not saving anything!)

In my opinion, your algorithm has a much much greater impact on the performance of your code than a mere function call. So, don't bug yourself with small things like this.

Calling empty functions in large numbers, maybe millions, may have some effect on your program's performance, yet if such a thing happens, it means that you are doing something algorithmically wrong (for example, thinking that you should put NormalPersons and ImmortalPersons in the same list)

Shahbaz
  • 46,337
  • 19
  • 116
  • 182