In a comment on OE Hive on this node http://www.oehive.org/node/1793 Guillaume Morin mentioned the performance penalty from inheritance. This surprised me, so I asked for test code, which he provided.
In his test he has two class hierarchies. One has 5 private data members and five methods in the base class and no inheritance or imterfaces. The other has one of the members in the leaf class, one in its parent, and three in the grandparent. The five methods are all in the leaf class and reference interfaces. The constructor for the child takes five arguments, calls a super passing up 4 values and assigns one to its member. The class above it passes three in the super and assigns one. And the grandparent assigns the remaining three values.
Running a loop that creates 10000 of each of the first type and then 10000 of the leaf class of the second type on my machine (with no tuning etc) give 30.081s for the version without inheritance and 66.324s for the version with inheritance.
I thought that the chained supers might be the issue so I created a second version. In mine, I eliminated the methods and the interfaces, preferring to test that separately. I changed all the private data members to public properties and eliminated all of the logic in the constructors. The assignment of values to the properties was made in a separate assign statement following the NEW.
That gave me 14.789s for the version with no inheritance and 47.619s for the version with inheritance.
What's up??? The two leaf classes are functionally equivalent and the same logic is being used internally with both and externally with both. The difference in definition should be resolved at compile time. But, here we are seeing a 3X performance penalty!!!!
Consulting in Model-Based Development, Transformation, and Object-Oriented Best Practice http://www.cintegrity.com
Another data point .... I just reran the test, but eliminated all the actual assignments. The numbers were essentially the same. So the big performance penaly is coming in the NEW, not in the access. I suppose this is less disturbing since it only happens once per object, but this seems like an extreme variation.
tamhas wrote:Another data point .... I just reran the test, but eliminated all the actual assignments. The numbers were essentially the same. So the big performance penaly is coming in the NEW, not in the access. I suppose this is less disturbing since it only happens once per object, but this seems like an extreme variation.
At the risk of asking the obvious, what version are you running? And can you attach your test code?
Here is Guillaume's original
And here is mine
In one case, 10,000 constructors are executed and in the other, 30,000 constructors are executed.
Are you saying that executing an empty constructor is the issue? And that one actually has 30,000 objects instead of 10,000?
No, there are not 30,000 objects, there are 10,000.
I understood your inheritance hiearchy to have 3 levels: leaf, parent, and
grandparent. So there are 30,000 constructors, 3 per object.
When you create a leaf object, its constructor is run. The leaf object class
constructor runs the parent class constructor first, then the stuff you put
in it. The parent constructor runs the grandparent constructor, etc. all the
way up to the top of the hierarchy.
Same with destructors.
gus bjorklund, progress software
If we wish to count lines of code, we should not regard them as lines
produced but as lines spent. (Edsger Dijkstra)
And you are going to run all these constructors even though they are all empty? And running the empty constructors is going to triple the time it takes to create the object?
Perhaps I misunderstood. I thought you have constructors. I did not examine
your code nor run it. I only looked at the description of what was being
done. I will take a look.
You didn't think inheritance was free did you?
Even if you have not written explicit constructors, there is still stuff
that has to be done at the point where your constructor code would be
executed. Data members and properties have to be allocated and initialised.
Inherited constructors have to be run. I don't know exactly what the r-code
has in it but will find out.
Each has a constructor, but the constructors are all empty. They are all just
Yes, the public properties in those superclasses obviously have to get instantiated, but there are exactly the same number of total properties in the two cases. I.e., exactly the same amount of work needs to be done. And yet, the inheritance version is taking much longer to do that work.
I wouldn't have minded a small difference, but 3X seems like something is wrong. Why would this not be something which was resolved at compile time? Why would the leaf class in the inheritance hierarchy not be optimized to be identical in R-code to the case with no inheritance? At runtime, there is no significance to the property coming from a parent.
For giggles I ran this on my home pc (the code from TMH's zip file). -l 50000 -mmax 65534 and everything run from .r code.
With -q and with empty constructors:
With -q and without constructors:
Without -q and with empty constructors
Without -q and without empty constructors
There is a bit of overhead from just having the empty constructors there, but it doesn't really add much. -q has the greatest affect.
What is the difference ? This still shows the same ratio between with and without inheritance.
You are right. My point was to make it clear that the empty constructors don't really make a difference.
We of course are always looking for ways to improve performance and class instantiation is one of those currently being investigated. That being said it was a design decision with OO to keep each class as a separate r-code file. The benefits are numerous, mostly around being able to change a class in a hierarchy without having to recompile. This is some of the beauty of the ABL where you can put a .p earlier on your propath to change behavior. To some extent you can do that in OO ABL. Java follows the same model where of course .NET tightly couples the hierarchy.
So at compile time we look at the hierarchy and set up our dispatch table as appropriate and we keep a "digest" value for each r-code used. If at runtime the digest's match, we can quickly use the dispatch table (this is what happens with -q). If not, we need to redo the dispatch table at runtime which is something to avoid if possible.
As far as runtime class instantiation, each class is run, the setup block is run (like block 0 for procedures) and then the contrsuctor is run which must immediatelty instatiation the super class and the same happens up the hierarchy. This is necessary with strong-typing. As I said we are looking at ways to improve this like maybe keep pools of object instances for reuse, etc.
Let me know any suggestions/comments you might have.