Advanced C Static Polymorphism Computer Games Engineering CO




















- Slides: 20
Advanced C++ Static Polymorphism Computer Games Engineering - CO 4302
POLYMORPHISM COST? ◉ What performance issues are there with this code? struct Entity { virtual ~Entity() {} virtual void Choose. Target() = 0; Matrix float Vector 3 Colour }; m. Location, m. Sidekick; m. HP, m. MP, m. Energy, m. Recharge; m. Target, m. Spawn. Point; m. Skin, m. Flag; struct Sheep : public Entity { void Choose. Target() { if (m. HP > 10) m. Target = Random. Point(); } }; struct Robot : public Entity { void Choose. Target() { if (m. HP > 10) m. Target = Player. Position(); } }; std: : vector<Entity*> entities; // Assume 1000 s of entities in above vector for (auto entity : entities) { entity->Choose. Target(); }
COST 1: LACK OF COMPILER OPTIMISATION ◉ The Choose. Target functions test HP before calling another function ◉ They seem like good candidates for optimisation or inlining ◉ E. g. check the HP before making the call to Choose. Target. ◉ However, C++ compilers do not inline virtual functions and will be limited in their ability to optimise this code ◉ We can make this optimisation ourselves, but maintainability will be reduced (moving entity-specific code outside of the entity class). ◉ In a project using virtual functions where performance is a factor, this lack of optimisation can be significant.
COST 2: BRANCH TARGET PREDICTION MISSES ◉ The extra machine code for a virtual call is minimal ◉ Just a couple of instructions ◉ However, it involves a branch whose target is not fixed ◉ The CPU fetches instructions ahead of those it is executing ◉ When a branch is encountered it predicts if it will be followed and where it will go. The latter part is branch target prediction. ◉ Prediction is based on a simple history ◉ When processing mixed objects with virtual methods, branch target prediction will often fail, causing a CPU stall. ◉ Similar to the branch prediction issues we met in an earlier lab
DOD RECAP
SIDE NOTE: LINKED LISTS AND SIMILAR
COST 3: VTABLES AND CACHE MISSES ◉ Of course this is also a small memory cost as well as a speed concern. ◉ As well as the per-object vtable pointer, there are the vtables themselves, one for each polymorphic class. They also use up some cache (might have an impact)
DOD APPROACH ◉ One DOD approach we could use: std: : vector<Entity. Target*> entity. Targets; // or arrays std: : vector<Entity. Stats*> entity. Stats; //. . . for (auto entity. Target : entity. Targets) entity. Target->Choose. Target(); ◉ But Choose. Target() is virtual. Will this work in a DOD approach?
DOD AND VTABLES? ◉ The above is not very DOD - instead get rid of polymorphism, have per-type lists: std: : vector<Sheep. Target*> sheep. Targets; // Or better make a Target. Collection template class std: : vector<Robot. Target*> robot. Targets; //. . . for (auto sheep. Target : sheep. Targets) sheep. Target->Choose. Target(); for (auto robot. Target : robot. Targets) robot. Target->Choose. Target(); ◉ Now compiler will also be able to optimise / inline the call. Branch targets are always the same. Improve more by keeping entities with < 10 HP at the end.
WHEN TO AVOID POLYMORPHISM ◉ So for code processing large numbers of objects, dynamic polymorphic calls can lead to serious performance issues. ◉ Although we have already observed that typical OO use impairs performance in these cases. Virtual functions just make it worse. ◉ Note: it is not C++ virtual functions per se. If you hand-coded your own dynamic function call system it would likely hit some of the same problems. ◉ What about non-time critical code? ◉ When we introduced DOD concepts, it was not the intention that all code should follow those ideas. Keeping higher level parts in a more standard OO style helps maintainability. ◉ In such code, polymorphism has its place. But it can be overused…
POLYMORPHISM FOR INTERFACES ◉ What problems are there with this code? class Graphics. Engine { virtual ~Graphics. Engine() {} public: virtual VBuffer* Create. Vertex. Buffer() = 0; virtual void Set. Vertex. Buffer(VBuffer*) = 0; virtual void Set. Vertex. Layout(Layout*) = 0; //. . . and more }; class DXEngine : public Graphics. Engine { public: VBuffer* Create. Vertex. Buffer() { …code… } void Set. Vertex. Buffer(VBuffer*) { …code… } void Set. Vertex. Layout(Layout*) { …code… } }; class GLEngine : public Graphics. Engine { public: VBuffer* Create. Vertex. Buffer() { …code… } void Set. Vertex. Buffer(VBuffer*) { …code… } void Set. Vertex. Layout(Layout*) { …code… } }; //. . . VBuffer* cube = engine->Create. Vertex. Buffer(); //. . . engine->Set. Vertex. Buffer(cube); engine->Set. Vertex. Layout(generic. Vertex); //. . .
THE TROUBLE WITH INTERFACES ◉ At first glance, the code seems conceptually sensible. ◉ Open. GL and Direct. X drive the same underlying hardware and work in broadly the same way. This seems a good way to abstract away implementation details. ◉ However, if you tried to implement this you would hit problems. ◉ Open. GL and Direct. X handle vertex layouts very differently. In (earlier) Open. GL they are inextricably linked to vertex buffers. Open. GL (up to around v 4) cannot set a vertex layout separately from a vertex buffer. But Direct. X can. ◉ This makes it very hard to write a clean interface: ◉ You must remove flexibility from Direct. X ◉ Or add some documented limitations to Open. GL ◉ Or add extra features in the implementation class and use dynamic cast or similar… ◉ Regardless, the implementation is going to be hampered by this attempt at polymorphism
THE TROUBLE WITH INTERFACES ◉ Perhaps the interface is badly designed. ◉ Maybe it should operate at a higher level, e. g. : Model* cube = engine->Load. Model("cube"); cube->Set. Material(0, Normal. Mapping); ◉ Certainly this would be more achievable, but this is solving a different problem. Surely the intent had been to create a low-level engine that could use different APIs? ◉ How would you implement post-processing, deferred rendering, shadows, etc. in such a high-level interface? Such things would have to be a implemented as a feature of the engine rather than something the engine user could try.
UNNECESSARILY DYNAMIC POLYMORPHISM ◉ Take a step back and consider why we tried to write such code: ◉ We have two APIs that ultimately do the same thing, so we wanted to have a common way to work with them. ◉ We used virtual inheritance to define an interface so user code would be portable. ◉ However, virtual inheritance is a dynamic mechanism. It can handle objects that are unknown until runtime. ◉ We don’t need this flexibility. Usually know the API we are using at compile time. ◉ We might want to support both APIs. But that is still just two known objects. ◉ Dynamic polymorphism is overkill for this problem. Let’s consider alternatives.
TWO CLASSES, COMMON INTERFACE ◉ The simplest way to support two classes with the same interface is simply to define the same functions in each. ◉ But not connect them in any other way class DXEngine { public: VBuffer* Create. Vertex. Buffer() { …code… } void Set. Vertex. Buffer(VBuffer*) { …code… } void Set. Vertex. Layout(Layout*) { …code… } //. . . and more }; class GLEngine { public: VBuffer* Create. Vertex. Buffer() { …code… } void Set. Vertex. Buffer(VBuffer*) { …code… } void Set. Vertex. Layout(Layout*) { …code… } }; // Using #ifdef for compile-time type selection #ifdef GL GLEngine engine; #else DXEngine engine; #endif //. . . VBuffer* cube = engine. Create. Vertex. Buffer(); //. . . engine. Set. Vertex. Buffer(cube); engine. Set. Vertex. Layout(generic. Vertex); //. . .
COMMON INTERFACE ◉ Although crude, the above is equivalent to the polymorphic version, since we only wanted a common interface ◉ The use of #ifdef may seem ugly, but in fact is an effective way of dealing with small differences required between APIs ◉ Documentation is vital, this kind of thing can have maintainability problems VBuffer* cube = engine. Create. Vertex. Buffer(); //. . . #ifdef GL // Ugly, but probably better than dynamic polymorphism equivalents engine. Set. Vertex. Buffer. And. Layout(cube, generic. Vertex); // Only in GL class #else engine. Set. Vertex. Layout(generic. Vertex); // Only in DX class. So trying to use … engine. Set. Vertex. Buffer(cube); // …this in GL will cause error #endif
STATIC POLYMORPHISM ◉ What if we want stronger checks on our shared interface, or want some functionality shared between our classes. ◉ Both of which we can do with dynamic polymorphism ◉ Static polymorphism, or compile-time polymorphism is not a builtin feature of the language, but can be achieved with templates ◉ We use a mechanism called the Curiously Recurring Template Pattern (CRTP) ◉ The base class uses a template parameter which is the inherited class, which allows the base class to access its implementation ◉ Example on next slide
CRTP template <typename Impl> // Implementation struct Graphics. Engine { Texture* Load. Texture(string name ) { // Some kind of common caching code Texture* loaded = Lookup. Texture(name); if (loaded != nullptr) return loaded; return impl()->Load. Texture. Impl(name); } void Set. Texture(Texture* t) { impl()->Set. Texture. Impl(t); } // Key part - get pointer to implementation Impl* impl() { // Safe because Impl will inherit from this return static_cast<Impl*>(this); } }; struct DXEngine : Graphics. Engine<DXEngine> { friend class Graphics. Engine<DXEngine>; private: Texture* Load. Texture. Impl(string name) { code } void Set. Texture. Impl(Texture* t) { code } }; struct GLEngine : Graphics. Engine<GLEngine > { // Open. GL versions of functions }; template<typename Impl> void Run(Graphics. Engine<Impl>& engine) { Texture* t = engine. Load. Texture("cube. jpg"); engine. Set. Texture(t); } DXEngine engine; Run(engine);
CRTP ◉ CRTP has most of the features of dynamic polymorphism. ◉ It uses inheritance, so base class features can be used by all implementations. ◉ Unlike the earlier crude method, there is only one base interface so less prone to error. Although there is more boiler-plate code ( impl()->function() ) ◉ Types are selected at compile time ◉ So it will be optimised / inlined (all the boiler-plate code will compile away) ◉ Because the engine user actually works with the implementation class, we can use #ifdef or std: : is_same to select implementation-specific code paths like earlier slide ◉ However, the base class is not generic - needs an explicit parameter ◉ So cannot have a container of mixed objects using CRTP for example ◉ std: : vector< Entity<? ? ? > > entities; // Must specify an entity type
WHEN TO USE CRTP ◉ CRTP is a widely used idiom ◉ See this site for another introduction with other kinds of example http: //eli. thegreenplace. net/2011/05/17/the-curiously-recurring-template-pattern-in-c ◉ However, it is only useful where the base class has some functionality ◉ I. e. There is some common code between the implementations ◉ It’s especially useful where the base class contains some implementation that uses features implemented by the inherited classes. ◉ Where there is little or no common code, and you don’t need run time dynamism then the crude method of similar classes works well enough.