0
\$\begingroup\$

(In C++) For a type whose instances will be handled in great numbers in performance-critical areas, such as a Vector2 class representing 2D-positions, should such types preferably be made POD? So something like:

struct Vector2f
{
    float x;
    float y;
};

rather than:

struct Vector2f
{
    float x;
    float y;
    float crossProduct();
    float magnitude();
    ... // Etc, functions that could be handy
};

I've read that making types whom you will handle many instances of POD can improve performance, because it is easier for the program to deal with them since it doesn't have to deal with vtables and other things.

When should POD-types be used? Is there a good rule of thumb?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Did you omit code from your example? As written your Vector2f wouldn't be using vtables. I can post an answer explaining why, but I wanted to make sure I wasn't missing something. \$\endgroup\$
    – Pikalek
    Commented Oct 9, 2023 at 20:02
  • \$\begingroup\$ @Pikalek The vtable thing I got the impression of from this answer. But even if that's wrong, is it not the case that POD-types can improve performance, since the program can handle them faster somehow? \$\endgroup\$
    – JensB
    Commented Oct 9, 2023 at 20:09
  • 1
    \$\begingroup\$ Both of those have the same footprint in memory. You can verify this for yourself by checking what you get from the sizeof() operator. Function members of classes and structs aren't stored in anyway in each instance, once the program is compiled they don't act any different than a function declared outside the class. The exception is if you have a class that inherits from another class with virtual functions, that is where the vtable comes in. No virtual functions, no vtable. \$\endgroup\$ Commented Oct 9, 2023 at 22:17

1 Answer 1

3
\$\begingroup\$

There is no difference in your 2 code examples, both would run at the same speed.

Is there a good rule of thumb?

Yes:

Do not attempt to over-optimize your code.


I follow these basic rules:

  1. Avoid square root operations, and when required, store the result if needed multiple times.

  2. Avoid *, / or % by powers of 2. Use <<, >> and & instead.

  3. Use bit fields as much as possible. Parallel condition checks become "all for the cost of one".

  4. Try to avoid nested loops, sometimes unavoidable, but if you hit a 3rd or ouch 4th nesting level, consider other ways to accomplish the task, try a tree, grid or other data structure.

  5. Always break; or return out of loops early if possible.

  6. Avoid recursion, unless the bounds are known and/or maxdepth is provided.

  7. If you find yourself reusing the same heap reference a bunch of times: "object.something" or "object->something" store it in a local variable to help with cache locality.

Like every rule, these were made to be broken with discretion.

Beyond these rules, I ask myself: "Is it worth the hassle?" The profiler times will usually answer this question and identify the critical sections for me.

I only then do I optimize the problem areas.


To address your "vtable" concern, use virtual sparingly, or simply avoid inheritance in your classes and use static "POD" calls with reference parameters to implement common methods.

This is similar to what the compiler does in the background, only avoiding the run-time lookup table since the classes and calls are concrete(fixed addresses are provided) at compile time.

That is overkill, but to eek out every ounce of performance, this answers your question as asked.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Shouldn't the compiler handle micro-optimizations like converting */% to bitwise operations when feasible? For example, here's a C++ compiler correctly converting integer divide-by-two to a shift right instruction. \$\endgroup\$
    – DMGregory
    Commented Oct 10, 2023 at 1:28
  • 1
    \$\begingroup\$ @DMGregory Not necessarily. The C++ standards afik have not specified required optimizations and leaves the topic implementation specific. So depending on the compiler and optimization switches provided, it may or may not.(I have not yet seen the mod % case optimized, no proof, It may be happening?). If the arguments are run-time variables, that meets your in-feasible standard, but valid when the programmer says so. If you write code in the purposed way, it is not up to the compiler to decide. \$\endgroup\$
    – agone
    Commented Oct 10, 2023 at 1:56
  • \$\begingroup\$ @DMGregory, The same goes for C#. Turn +=1; Turn %=2; will never be reduced to the much faster single operation: Turn = Turn ^ 1; We know that Turn will always be constrained to the set [0,1], but the compiler does not. \$\endgroup\$
    – agone
    Commented Oct 10, 2023 at 2:21
  • \$\begingroup\$ Thanks for the answer! One question about the final paragraphs: So is inheritance and polymorphism just not used in game development? \$\endgroup\$
    – JensB
    Commented Oct 10, 2023 at 21:50
  • 1
    \$\begingroup\$ That's a question over which much blood and ink has been spilled, so you likely won't get a universally satisfying answer in a comment thread. Many devs favour "composition over inheritance" for many areas of games, and try to keep runtime polymorphism / virtual calls out of hot paths. But that's not to say that polymorphism has no uses in games — it's powerful for simplifying UI and things that are executed infrequently enough that the overhead is not problematic, or in simpler games that don't need to squeeze every drop of performance out of the chips. Compile-time polymorphism is also used. \$\endgroup\$
    – DMGregory
    Commented Oct 10, 2023 at 22:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .