Introduction
Java provides a few designs to define implementations in sub-classes/types, i.e. through interfaces, abstract classes, etc. What is the performance profile of these designs? The Java runtime needs to determine which implementation should be invoked at these ‘method call-sites’. What may not be common knowledge is that there are performance costs associated with the method dispatch, i.e. in determining which method to invoke at the call-site.
In this post, let us explore the performance impacts of different implementations:
- static implementations,
- implementations to an interface,
- implementations to an abstract class, and
- direct calls, without sub-classing.
Methods
Above lists the different approaches, it is also important to consider the number of sub-classes/implementations that underlie an interface or abstract class.
With that, here are the benchmarks:
- No-morphic: Direct call, no dispatch.
- Monomorphic: Single class covering
- static implementation,
- implementation to an interface, and
- implementation to an abstract class.
- Bimorphic: Two classes that share
- implementations to an interface, and
- implementations to an abstract class.
- Megamorphic: Three classes that share
- implementations to an interface, and
- implementations to an abstract class.
The method that is called just returns a field value.
Benchmarks
No-morphic & Monomorphic
Method | Operations / sec | |
---|---|---|
No-morphic | 28,565,468 | |
Monomorphic | via static | 28,322,441 |
Monomorphic | via abstract | 20,370,466 |
Monomorphic | via interface | 13,999,464 |
The no-morphic (direct call) is the fastest of all the approaches. Static method follows this no-morphic/direct call approach, which does not require any method resolution, the compiler knows the method for execution at compile time. It’s no surprise that the performance between a direct call and a static method are comparable.
Now, let’s consider the abstract class and interface approaches, the checks required by the abstract class approach and the interface approach is the difference. The interface needs to check the actual class word, something we won’t go into detail here. More information on this can be found here: https://shipilev.net/blog/2015/black-magic-method-dispatch/#__strong_c2_dynamic_interface_ref_strong
Bimorphic & Megamorphic
Bimorphic is the name given to method call-sites that have two method implementations. Megamorphic is the name given to call-sites that have three or more method implementations. Let’s introduce more types and review the performance:
Method | Operations / sec | |
---|---|---|
Monomorphic | via abstract | 20,370,466 |
Monomorphic | via interface | 13,999,464 |
Bimorphic | via abstract | 11,921,088 |
Bimorphic | via interface | 11,558,515 |
Megamorphic | via abstract | 1,002,057 |
Megamorphic | via interface | 794,102 |
With abstract classes, having two implementations vs one implementation almost halves the performance (20,370,466 v 11,921,088). While with interfaces there is a decline in performance, but it’s not so severe. What’s worth highlighting is that the megamorphic performance is substantially worse for both the abstract class and interface approaches. Performance is a less than 10% that the bimorphic for abstract classes and interface approaches.
Megamorphic
As a check, I updated the megamorphic benchmark to support one, two and three types. This means that while the class hierarchy had three implementations, each test uses only one, two or three types.
Megamorphic | Method | Operations / sec |
---|---|---|
1 type | via abstract | 14,142,390 |
1 type | via interface | 14,133,993 |
2 types | via abstract | 11,892,201 |
2 types | via interface | 11,507,743 |
3 types | via abstract | 1,002,057 |
3 types | via interface | 794,102 |
The two-type performance is in-line with the original bimorphic test:
Method | Operations / sec | |
---|---|---|
Bimorphic | via abstract | 11,921,088 |
Bimorphic | via interface | 11,558,515 |
2 types | via abstract | 11,892,201 |
2 types | via interface | 11,507,743 |
However, the one-type performance is not in-line with the monomorphic abstract class performance:
Method | Operations / sec | |
---|---|---|
Monomorphic | via abstract | 20,370,466 |
1 type | via abstract | 14,142,390 |
Perhaps as there are more abstract class implementations, more work needs to be done to resolve the method for dispatch, bringing its performance profile closer to the interface approach performance.
Closing
Structuring code to make the most of performance may not be essential, perhaps not even desirable in most cases. However, keeping implementations/class hierarchies concise and narrow should be a consideration when writing clean code. Potentially, it’s even a case for deferring interface definition till necessary, rather than defining them up front out of habit.