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

Method Dispatch performance

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

Megamorphic performance

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.