Pointer dereferencing overhead vs branching / cond

2020-08-16 11:33发布

问题:

In heavy loops, such as ones found in game applications, there could be many factors that decide what part of the loop body is executed (for example, a character object will be updated differently depending on its current state) and so instead of doing:

void my_loop_function(int dt) {
  if (conditionX && conditionY) 
    doFoo();
  else
    doBar();

  ...
}

I am used to using a function pointer that points to a certain logic function corresponding to the character's current state, as in:

void (*updater)(int);

void something_happens() {
  updater = &doFoo;
}
void something_else_happens() {
  updater = &doBar;
}
void my_loop_function(int dt) {
  (*updater)(dt);

  ...
}

And in the case where I don't want to do anything, I define a dummy function and point to it when I need to:

void do_nothing(int dt) { }

Now what I'm really wondering is: am I obsessing about this needlessly? The example given above of course is simple; sometimes I need to check many variables to figure out which pieces of code I'll need to execute, and so I figured out using these "state" function pointers would indeed be more optimal, and to me, natural, but a few people I'm dealing with are heavily disagreeing.

So, is the gain from using a (virtual)function pointer worth it instead of filling my loops with conditional statements to flow the logic?

Edit: to clarify how the pointer is being set, it's done through event handling on a per-object basis. When an event occurs and, say, that character has custom logic attached to it, it sets the updater pointer in that event handler until another event occurs which will change the flow once again.

Thank you

回答1:

The function pointer approach let's you make the transitions asynchronous. Rather than just passing dt to the updater, pass the object as well. Now the updater can itself be responsible for the state transitions. This localizes the state transition logic instead of globalizing it in one big ugly if ... else if ... else if ... function.

As far as the cost of this indirection, do you care? You might care if your updaters are so extremely small that the cost of a dereference plus a function call overwhelms the cost of executing the updater code. If the updaters are of any complexity, that complexity is going to overwhelm the cost of this added flexibility.



回答2:

I think I 'll agree with the non-believers here. The money question in this case is how is the pointer value going to be set?

If you can somehow index into a map and produce a pointer, then this approach might justify itself through reducing code complexity. However, what you have here is rather more like a state machine spread across several functions.

Consider that something_else_happens in practice will have to examine the previous value of the pointer before setting it to another value. The same goes for something_different_happens, etc. In effect you 've scattered the logic for your state machine all over the place and made it difficult to follow.



回答3:

Now what I'm really wondering is: am I obsessing about this needlessly?

If you haven't actually run your code, and found that it actually runs too slowly, then yes, I think you probably are worrying about performance too soon.

Herb Sutter and Andrei Alexandrescu in C++ Coding Standards: 101 Rules, Guidelines, and Best Practices devote chapter 8 to this, called "Don’t optimize prematurely", and they summarise it well:

Spur not a willing horse (Latin proverb): Premature optimization is as addictive as it is unproductive. The first rule of optimization is: Don’t do it. The second rule of optimization (for experts only) is: Don’t do it yet. Measure twice, optimize once.

It's also worth reading chapter 9: "Don’t pessimize prematurely"



回答4:

Testing a condition is:

  • fetch a value
  • compare (subtract)
  • Jump if zero (or non-zero)

Perform an indirection is:

  • Fetch an address
  • jump.

It may be even more performant!

In fact you do the "compare" before, in another place, to decide what to call. The result will be identical. You did nothign more that an dispatch system identical to the one the compiler does when calling virtual functions. It is proven that avoiding virtual function to implement dispatching through switches doesn't improve performance on modern compilers.

The "don't use indirection / don't use virtual / don't use function pointer / don't dynamic cast etc." in most of the case are just myths based on historical limitations of early compiler and hardware architectures..



回答5:

The performance difference will depend on the hardware and the compiler optimizer. Indirect calls can be very expensive on some machines, and very cheap on others. And really good compilers may be able to optimize even indirect calls, based on profiler output. Until you've actually benchmarked both variants, on your actual target hardware and with the compiler and compiler options you use in your final release code, it's impossible to say.

If the indirect calls do end up being too expensive, you can still hoist the tests out of the loop, by either setting an enum, and using a switch in the loop, or by implementing the loop for each combination of settings, and selecting once at the beginning. (If the functions you point to implement the complete loop, this will almost certainly be faster than testing the condition each time through the loop, even if indirection is expensive.)