Oh, to answer your earlier question about why I avoid dynamic polymorphism. I did some tests in the past and found that virtual function calls aren't that expensive, so that's not it just to get that out of the way. It's due to the individual allocations of each derived class and not being next to each other in memory. Granted, CRTP doesn't avoid this, but allows you to have static dispatch. So if you combined CRTP with something like std::variant, then you could get data locality and static dispatch.
Check this out:
Code: Select all
100 iterations of 10,000,000 elements in 3 different vectors. Displayed results are averaged: sum_time / num_iterations
// In this run, the static polymorphism used if constexpr instead of a runtime if to determine if it could
// cast to derived type and call a function that was a part of the Base class.
Dynamic polymorphism test result: 0.192118
Mixed polymorphism test result: 0.125081
Static polymorphism test result: 0.074151
// In this run, the static polymorphism used the runtime if just like the other two tests
Dynamic polymorphism test result: 0.177580
Mixed polymorphism test result: 0.126362
Static polymorphism test result: 0.119815
All the tests were adding and multiplying some vectors and a little branching.
I'm really quite shocked here, I feel as though the dynamic polymorphism did quite a bit better than expected.
Seems the biggest improvement can be had if you can use constexpr if to determine unique behavior not apart of a base interface.
if the Static polymorphism result is 100%, the Mixed would be about 105% slower and the Dynamic would be about 148% slower most other things being equal ( runtime if branching ). However, being able to determine at compile time what type you are dealing with, either using the visitor pattern or a constexpr if, the Mixed is 168% slower and the Dynamic is 259% slower.
The original goal was to check on data locality, but it seems this test wasn't sufficient enough to make a solid determination.
Honestly, here's what I'm taking away form this.
Dynamic polymorphism is slow. It prohibits data locality and if you need to know the specific type outside of the derived class, no matter which route you take, branching could be more of an issue. In this run, I cast directly to the derived type after checking it's enum type.
Mixed polymorphism, as I'm calling it, using std::variant to promote data locality, and some templated base class that is templated on the derived class for some static dispatch can help some. In most cases, if you aren't branching to determine an objects base type, it's just as fast as Static polymorphism because data locality is the dominant factor. In these tests, I casted to the templated base class that was derived class matching it's enum type.
Static polymorphism using std::variant for data locality and the C++ type system is king when needing to know when you can access unique data or member functions.
There is also the old visitor pattern that I didn't try, where you pass the base object to a visitor object member function, this gets you a derived visitor. Then you pass the derived visitor to a member function on the original object, this gets you the derived object. Then you can call whatever function you need from there. I know, that's confusing, that's why I never used it, but I guess in all fairness, it might be worth a test.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com