Let's say I have an interface called ParentClass
. ParantClass
has two implementations, ParentClassA
and ParentClassB
. There is also the ChildClass
interface, with a ChildClassA
and ChildClassB
implementation. The ParentClass
interface has a function called createChild
, which returns a pointer to a ChildClass
interface of the appropriate type (ParentClassA::createChild
returns a ChildClassA
while ParentClassB::createChild
returns a ChildClassB
).
The code tends to look like this:
ChildClass *child = parentClass->createChild();
child->destroy();
The inefficiency here is that the ParentClass
interface will only ever return a ChildClass
interface of the same implementation, so there's a ton of vtables being used but there's only one implementation that they lead to.
A theoretically more efficient solution would be for ParentClass::createChild
to return an opaque pointer, and to have all member functions contained in ChildClass
to be moved to ParentClass
and have the opaque pointer be passed as the first argument. This will result in only a single vtable, but the code ends up being a bit uglier.
And that code tends to look like this:
ChildClass *child = parentClass->createChild();
parentClass->destroyChild(child);
This part of my application is used quite frequently, so performance is an important consideration. But readability and maintainable code is as well. I'm not sure which approach I should be using.
3 Answers 3
Don't put the car before the horse. Until you have proof that performance is impaired by some inefficiency it doesn't matter where the inefficiencies are. Write the simplest and most maintainable code you can, refactor if any issues arise.
-
That's an interesting concern, but it should be a comment, not an answer. You don't know what's the process OP used to conclude he needed such an optimization. Your answer doesn't add much value to someone who knows he needs such a solution, but not how to implement it.Vincent Savard– Vincent Savard2017年06月02日 14:00:12 +00:00Commented Jun 2, 2017 at 14:00
-
@VincentSavard The title asks about optimization but in the body it looks like the OP wants to choose between performance on one side and readability and maintainability on the other. I answered to the body because TBH I forgot about the title while I was reading the question. After the fact I think that the body is a better description of the OP's actual concern.Stop harming Monica– Stop harming Monica2017年06月02日 15:37:33 +00:00Commented Jun 2, 2017 at 15:37
To expand on @amon's comment, you can use templates to do the polymorphism at compile-time, instead of virtual doing it at runtime.
If the implementation of ParentA and ParentB are identical apart from uses of ChildA and ChildB, just write a template<typename Child> class Parent{ ... };
directly.
Otherwise, you may still want to have an interface, but that too can be a template. You would have something like
template<typename Child>
struct Parent {
using child_type = Child;
virtual Child * createChild() = 0;
// rest of interface, using Child type parameter directly
};
struct ParentA : public Parent<ChildA> {
// use of final is a strong hint for the compiler to devirtualize
ChildA * createChild() final;
// more implementation for ChildA
};
If you do have concrete subclasses for parent, you may want to have a helper to get the parent for each child
template<typename Child>
struct child_traits;
template<>
struct child_traits<ChildA> {
using parent_type = ParentA;
}
Then with either choice, the other parts of your program use Child (and possibly Parent
template<typename Child>
void ParentCollaborator(ParentArgs args) {
Parent<Child> parent(args);
Child * child parent.makeChild();
// template type deduction allows omitting <Child> here
doParentChildStuff(parent, *child);
}
-
Is there any way to switch between the two at runtime? The application picks one implementation at runtime and uses that until it shuts down.User9123– User91232017年06月03日 05:14:53 +00:00Commented Jun 3, 2017 at 5:14
-
If you propagate the templating through all the use of parent and child,
main
can just have anif (isA) runProgram<A>(); else runProgram<B>();
or similarCaleth– Caleth2017年06月03日 11:33:51 +00:00Commented Jun 3, 2017 at 11:33
I don't see how the two options you propose are different from a dispatching efficiency perspective.
The first approach uses a dispatch for ParentClass
, then ChildClass
, while the latter does a dispatch for ParentClass
, then ParentClass
again.
Your second solution is not much different because ParentClass
also has two implementations.
Now if somehow ParentClass
had non-virtual implementation of destroyChild
, that would be different, but there is nothing in your post suggesting that, and in fact, they way I read it, you have two different implementations of the same interface, which says to me that destroyChild
is virtual.
In a straightforward runtime, these would both be relatively the same performance. The only difference is that the latter has a better chance of having warmed the cache by using the same vtable twice.
Still, in a looping test, I'm not sure you'd notice, as after the first iteration the cache would be warm to all and you'd have to exhaust the cache during the loop to have that become an issue.
In addition to the data cache for the vtable, the CPU is going to do branch prediction, and, it will do it well in a looping test. So, you'll have two caching mechanisms working on these dispatches. (Not to mention the code cache.)
I would go with whatever makes your code more maintainable.
Explore related questions
See similar questions with these tags.
ParentClass *child = parentClass->createChild();
so that the child can be used just like the parent?