Pragmatic Functional Java: Performance Implications

Sergiy Yevtushenko - Nov 30 '21 - - Dev Community

After posting Introduction and Motivation, I've started receiving comments which express concerns related to possible performance penalty caused by the additional classes such as Option<T> and Result<T>.

General Considerations

Before discussing benchmark results, I should note, that even without measurements it is clear that related performance impact should be quite low. There are no complex computations inside these classes, just few function calls, which are rather easy to optimize. Moreover, these classes are designed in a way which eliminates branching completely - there are no if statements nor conditional expressions (?) inside, just method calls. Lack of branching eliminates branch prediction misses and positively affects performance.

And, frankly, given that most of the time we're using heavy, slow and resource hungry frameworks like Spring, any performance impact caused by PFJ style code will be negligible.

Nevertheless, all of the above are just general considerations and sensible portion of guesstimate. It will be much safer to do measurements and check results.

Approach to Benchmarking

The idea behind benchmarks is rather simple - mimic some simple use case similar to what can happen in real life and pass mixed input to it.

The mixed input consists of two possible input values, each of which triggers a different execution path. For example, when null handling is benchmarked, the method under test received a mix of valid strings and null values. The valid string is converted to upper case (serves the purpose of business logic). The null string just returns null. Similar approach is used for Optional benchmark and other two benchmarks (exceptions vs Result).

Since in real applications proportions between both cases can be different, benchmarks measures several cases with different proportions of the different input values:

  • 0% - no invalid values (only happy day scenario cases)
  • 10% - 10% of input consists of invalid values (which trigger null handling, exceptions, Option.empty() or Result.failure())
  • same for 25%, 50%, 75%, 90% of invalid values in input
  • 100% - no valid inputs at all - shows pure overhead caused by error/missing value handling

Since only valid cases involve some real processing, you might notice that time per iteration is getting smaller when proportion of valid values is decreased.

Option

Below provided results obtained on MacBook Pro M1:
(results provided in pairs for convenient comparison)

OptionPerformanceTest.nullable0    avgt    6  36.107 ±  0.456  us/op
OptionPerformanceTest.option0      avgt    6  35.085 ±  0.409  us/op

OptionPerformanceTest.nullable10   avgt    6  32.365 ±  0.064  us/op
OptionPerformanceTest.option10     avgt    6  31.938 ±  0.323  us/op

OptionPerformanceTest.nullable25   avgt    6  26.910 ±  0.828  us/op
OptionPerformanceTest.option25     avgt    6  26.347 ±  0.113  us/op

OptionPerformanceTest.nullable50   avgt    6  18.158 ±  0.119  us/op
OptionPerformanceTest.option50     avgt    6  17.688 ±  0.086  us/op

OptionPerformanceTest.nullable75   avgt    6   9.146 ±  0.198  us/op
OptionPerformanceTest.option75     avgt    6   8.844 ±  0.181  us/op

OptionPerformanceTest.nullable90   avgt    6   3.716 ±  0.022  us/op
OptionPerformanceTest.option90     avgt    6   3.599 ±  0.055  us/op

OptionPerformanceTest.nullable100  avgt    6   0.084 ±  0.001  us/op
OptionPerformanceTest.option100    avgt    6   0.087 ±  0.008  us/op

Enter fullscreen mode Exit fullscreen mode

There is a sensible temptation to say that Option performs better, but numbers too close to claim real advantage. So, I'm just glad to see that there is no negative performance impact caused by Option.

Result

Conditions are the same as above:

ResultPerformanceTest.exception0    avgt    6  36.621 ±  0.045  us/op
ResultPerformanceTest.result0       avgt    6  35.215 ±  0.208  us/op

ResultPerformanceTest.exception10   avgt    6  32.651 ±  0.129  us/op
ResultPerformanceTest.result10      avgt    6  31.983 ±  0.112  us/op

ResultPerformanceTest.exception25   avgt    6  27.373 ±  0.240  us/op
ResultPerformanceTest.result25      avgt    6  26.472 ±  0.130  us/op

ResultPerformanceTest.exception50   avgt    6  18.239 ±  0.769  us/op
ResultPerformanceTest.result50      avgt    6  17.671 ±  0.101  us/op

ResultPerformanceTest.exception75   avgt    6   9.213 ±  0.597  us/op
ResultPerformanceTest.result75      avgt    6   9.019 ±  0.090  us/op

ResultPerformanceTest.exception90   avgt    6   3.705 ±  0.021  us/op
ResultPerformanceTest.result90      avgt    6   3.618 ±  0.019  us/op

ResultPerformanceTest.exception100  avgt    6   0.087 ±  0.001  us/op
ResultPerformanceTest.result100     avgt    6   0.086 ±  0.001  us/op

Enter fullscreen mode Exit fullscreen mode

Conclusions also the same - there is no visible performance impact caused by the Result. Nevertheless, I should note, that benchmark puts exceptions benchmark is somewhat more convenient conditions than in real life. In real applications, exceptions are often logged and this may trigger execution of some expensive parts of the exception handling code like formatting stack trace. As one could see, this does not help exceptions to outperform Result.

Summary

Pragmatic Functional Java provides several advantages over idiomatic Java and does not cause any negative performance impact.

Full benchmark code is available here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .