Performance issues may eventually arise in any piece of software. Even when you program consciously, you are not immune against slower execution times. In this article we show you how to quickly track down the origins of performance issues and fix them.

Finding the performance bottleneck

A performance issue can suddenly appear in production or when load-testing the system. It typically occurs when the system runs against large amounts of input data, whereas test cases only deal with a limited number of dummy / simplified data.

Once you notice slow performance, the first thing to do is to find where lays the cause. In this case you cannot just go in debug mode, as you would do to find the method returning the wrong result or corrupting the state of an object.

In this case, your debugger will be a profiler, which allows you to compute the total execution time of every called method during an execution. There exist many good profilers, such as the NetBeans profiler or XRebel (a lightweight profiler developed by ZeroTurnaround).

It can be difficult to play all the steps needed to spot the performance bottleneck, yet this is an absolute necessity. You may already have an idea of what causes the problem at first thought, but even if you’re right, this must be confirmed by objective metrics.

In the end, the process is similar to writing a test that fails because of a bug, and then making the test pass by fixing the bug. Later the test serves as a regression test to ensure the bug never happens again. After fixing a performance issue, you can run the profiler again to get some confidence that the bottleneck has truly disappeared.

Still, the most difficult task is to actually get rid of this bottleneck.

A naive way to improve the performance

Once you’ve found the source of the bad performances, you still have to fix it. Although this seems an appealing task, there is a pitfall to avoid.

Indeed, as far as performance is concerned, too many developers try to optimize specific instructions of code by writing them differently, avoiding some computations, etc. Such optimizations often lead to hard-to-read code, and may introduce new bugs.

If you try to fix your performance issue this way, say for an operation that takes a dozens seconds to perform under heavy load, you may be able to reduce the time by a handful of seconds in the end. However, the inherent bottleneck still exists. You did nothing bad, but you certainly can do better.

Algorithm complexity to the rescue

Consider instead algorithm complexity. A vast majority of performance problems come from an unnecessary high complexity. You’ve already found the place where the lengthy computation takes place thanks to the profiler. All you need to do now is to find out the algorithm complexity of that piece of code and to reduce this complexity.

In a nutshell, the complexity of an algorithm indicates how much the execution time of this algorithm scales as the size of the input grows. Examples of common complexities are the following:

  • O(1), constant time (e.g. directly return a class attribute)
  • O(n), linear (e.g. iterate once over all elements in a list)
  • O(n²), quadratic (e.g. the bubble sort)
  • O(n³), cubic (e.g. naive multiplication of two matrices)

You don’t need a constant time O(1) everywhere in your code. However, reducing the complexity of a method from O(n³) to O(n²) may lead to dramatic reductions in execution time, especially as this method is called often and deals with large inputs. Of course, going from linear or quadratic to constant time is always helpful, but this might not be always feasible.

Also keep in mind that improving algorithm complexity is often achieved at the cost of additional memory consumption. Indeed, in order to avoid unnecessary computations, one typically stores more intermediate results in memory. Thanks to the large amount of memory available in today’s computers this is commonly not a problem. Yet it can be a concern in specific contexts (e.g. embedded software or applications dealing with extremely large amounts of data).

Reducing the complexity can thus resolve a lack of performance without sacrificing the readability of the code. Furthermore, where the naive approach may cut execution time by half, the complexity-centered approach can reduce it to a few milliseconds.

 

Tell us about the last time you encountered a performance issue? What was its cause and how did you fix it?