This article is about some discoveries I made along the way to improve performance (in terms of execution time of assertion enabled code) in the next version of GContracts [0].
Hidden Magic is Hidden
Let's have a look at the following code sample showing a Groovy Fibonacci number calculator class:
The Calculator#main(args) method calculates all Fibonacci numbers up to 1.000.000 for 200.000 times. When I was running this code with default JVM parameters, I got the following output: 330 ms. As the code is already statically typed, adding @CompileStatic was a no-brainer and fastened the code to run in 8 ms.
Let's assume we wanted to add a precondition to the Calculator#fib(long) method ensuring limit to be equal or greater than 2. With GContracts, the method signature would be modified to:
In GContracts early days, it simply introduced a precondition assertion statement as the first statement (except the first statement was a super call) in the related method. With version 1.2.9, the Groovy AST browser [1] shows the following AST for the Calculator#fib method:
WTF? There isn't even a sign of an assertion statement. So what has been the reason to introduce ContractExecutionTracker, ViolationTracker and all the other stuff (code)?
Class Hierarchies and other Horror Scenarios
ContractExecutionTracker is used to keep track of assertion executions. This has to be done in order to detect assertion circles and therefore avoid stack overflows.
Without assertion tracking, even creating a new instance of Rocket would result in a stack-overflow. This is caused by the class invariant embedded into the constructor and the Rocket#getSpeed() method. When a new instance is created the class invariant is executed after initialization (the constructor) has finished. Within the class invariant, #getSpeed() is called which itself triggers the class invariant again - voilĂ , we're in a loop.
The Rocket example has been trivial. As GContracts inherits class invariants, pre- and postconditions in class hierarchies there are trickier scenarios with circling assertion checks. To sum up: ContractExecutionTracker keeps its own assertion stack trace for the current thread to check whether an assertion check is already in the call stack.
The ViolationTracker that can be seen in the screenshot above is used to keep track of thrown AssertionError instances. Whenever assertions are triggered along the class hierarchy it keeps track of all violations and re-throws the first violation in the assertion line.
In a nutshell, as of 1.2.9 GContracts is in a state that isn't "just" introduce assert statements into code.
Performance, Performance, Performance
Let me come back to introducing the precondition to our Calculator example. Running the calculator with the precondition enabled results in 200 ms execution time. That's quite a heavy load compared to the 8 ms it took without @Requires. On the other hand-side, its far away from being a total fail. The Calculator#main(args) method calls the Fibonacci calculation 200.000 times, meaning that value tracking assertion tracking and error tracking do take less than 1 ms per 1000 iterations.
If we replace @Requires with a Groovy power assertion statement the code takes 50 ms.
Further performance measurement has shown that the addition of assertion tracking and violation error tracking adds about 140 ms to the precondition check. ViolationTracker and ContractExecutionTracker are already small and performance-optimized classes, so there wasn't much room for performance improvement at this side.
As I've noted before, assertion and violation error tracking has been introduced to detect issues in combination with circling assertions or assertions in class hierarchies.
The thing is, the burden these features have on the execution time only make sense if class hierarchies and more complex assertions are involved.
Assertion and violation error tracking is only needed in the case of more complex assertions and class hierarchies. The projects where I've seen GContracts applied did not use its ability to inherit assertions and most of the time the contract checks were simple boolean expressions that most of the time didn't even call other instance methods.
Inline Mode
On top of that consideration, I started to implement a so-called "inline mode" in GContracts. Inline mode is a special mode embedding assertion statements directly into the code (as this was the straight-forward approach of the early days). This mode is not triggered for descendant classes and for assertions with either (static) method or constructor calls.
Running the calculator example with GContracts 1.2.10-SNAPSHOT results in the following AST view:
The classes generated from the specified closure expressions are present. This code is still needed for future descendant classes, but the actual precondition check is done "inline" with a simple assertion statement.
There is another optimization that can be applied in the case of GContracts. As GContracts enforces usage of side-effect free functions in assertions, the (negated) boolean expression is evaluated beforehand in the if statement. This trick allows to avoid the assert statement execution as long as the boolean expression is true. Only if the expression is false, Groovy's ValueRecorder and other power assertion related code is executed.
Running the above calculation with the inlined preconditions of GContracts 1.2.10-SNAPSHOT results in the execution time of 8 ms. Compared to the 200 ms with GContracts 1.2.9, this is a significant performance improvement and as fast as assertion enabled code can be in this case. Of course, the performance heavily depends on the boolean expression that is executed within the if statement's boolean expression. But on the other hand, checking for assertions requires at least executing that expression - it is as fast as it can get with assertion checking enabled.
Conclusion
This article should share some knowledge about the performance tweaks being introduced in the next version of GContracts. Projects will benefit with greatly improved performance for a vast majority of use-cases (and even with better performance in the not so common ones ;-)) - and this even in "hot" code places.