Let us first define what “optimization” of a program is. By optimization we mean changing the program code in order to reduce the usage of some resources of the system by making it more complex.
Rule 1: The later, the better
This rule seems to be the easiest to apply. After all, it encourages a reduction in workload. But that feeling is deceptive. Very often there is a temptation to optimize prematurely. As a result, the program structure becomes more complicated. It becomes more difficult to read it. As a consequence, the code becomes harder to maintain. But even this is not the worst thing. The problem is that you may waste a lot of time on optimizing the wrong code.
Conclusion: At the first stage of development, write the simplest code (simple, but not primitive! simple = understandable), which solves the task at hand without thinking about resource limitations.
Rule 2. Measurements first – then changes
Suppose the first stage is passed. A simple version of the program works properly, but not effectively enough. And here the question arises, how to measure effectiveness? There is no such thing as abstract efficiency. It is always determined relative to something. For example, efficiency with respect to memory consumption or efficiency with respect to the use of processor resources. This brings us to the next step: choose optimization criteria. Any relevant characteristic of an application can be considered as such criteria (from the size of an executable file to the speed of a network transfer).
On no account should you try to meet the criteria “by eye”. You will be wrong anyway. Find a way to measure accurately (or as closely as possible). For some characteristics this is easier (e.g. program size), for others it is more difficult (e.g. the maximum number of simultaneous users).
So you have made sure that the program does not meet one or more of the selected criteria. What now? Run to rewrite everything? Take your time. If you begin to change the code now, you will again work blindly and do a lot of unnecessary work. We know there is a problem somewhere. But where is it? Again we need additional tools for measurements. A typical solution is to use a profiler. The profiler itself is a tool that knows how to calculate the number of calls, time-to-find, and other useful numbers for each program procedure. The implementation of the profiler can be anything (much depends on the platform): from an external application to a library. If you find a more suitable tool for your purposes, use it.
Fine. Suppose we have managed to find the bottleneck that slows everything down. That’s what we need to work with.
Conclusion: any optimization must be reasonable. It is quite possible that it is not necessary. If you don’t measure it, you won’t know about it.
Rule 3. Check algorithms and data structures
When reviewing a problem function, first check whether the algorithms and data structures used to solve the problem are appropriate. This can be the reason for the ineffective work of the application.
A rough example: The function uses a linear array search. While this is fine for arrays of small size, it becomes a problem for large arrays. In this case, you can use a set instead of an array if you don’t want the elements to be ordered. Alternatively, instead of a simple linear search, you can try a binary search.
Another example: you use fast sorting, but it doesn’t work fast enough. See if it would improve if you changed it to merge or insert sorting. Quick sorting is not always the best.
Note that the examples discussed above are better not considered optimizations, but corrections of design flaws.
If the function in question already uses the most suitable standard algorithms and data structures, you can’t do without multithreading. I recommend to see a good example from my other article where we accelerated the image processing algorithm 4 times with QtConcurrent.
But this is not a complete list of possible optimizations. Often it is possible to reduce the amount of necessary calculations by introducing additional unobvious constraints. Think of the “Eratosthenes Lattice” algorithm for finding prime numbers. Another possible optimization is to use approximate calculations. If two decimal places are enough for you, there is no point in looking for a value up to the sixth. Another tactic is to use constant tables. For example, to get the values of trigonometric functions. I think you can think of more than one trick that can improve the efficiency of your program. It all depends on the context.
Conclusion: Start with the most obvious ways of optimization. Be sure to re-measure the performance of the new code (after any changes), otherwise how will you know that it is not even worse than it was? If that wasn’t enough, then navigate through the situation. Try to add parallel processing or approximate calculation methods.
Rule 4. Document it
If the optimization was successful, it is likely that the code you were working on became less comprehensible. This can be a problem in terms of further maintenance of the system. Therefore, right after the optimization is done, it won’t hurt to write a comment stating:
- The rationale for this code. That is, why the optimization was necessary. It is desirable to add the numerical values that you obtained as a result of measurements (before the optimization and after it);
- Explanation of the chosen solution. What is the essence of optimization. What makes it possible to increase the efficiency, etc;
- Indication of the options that you’ve had time to try, but they didn’t meet expectations. Perhaps someone else will be working on the same code and decide that it can be made even better. You’ll make your colleagues’ work easier if you share your experience with them in the comments, pointing out what has already been done so they don’t waste their time.
Conclusion: If the code is complex, it requires explanations. These explanations can help you in the future.
Rule 5. If nothing helped
But what to do if you have already tried everything you can, but the program still does not fit the criteria of optimality? Here you can have several options:
- Discuss the current version of the system with the customer. Perhaps a small discrepancy in terms of performance will suit him;
- Increasing the system requirements. If you cannot solve the problem programmatically, you can try to find a hardware solution. We put more memory, more processors, connect the Internet channel with more bandwidth;
- Switch to another (lower level) programming language. This does not mean that you need to rewrite the entire application. That is why we were looking for bottlenecks that require optimization. You only need to rewrite the problematic functions in a more efficient programming language in the form of plugins. For instance, the system is written in Java and you write the module in C++. Or the system is written in C++, and you do an assembler plug-in.
Conclusion: If a problem doesn’t have a solution at one level of abstraction, it can be found at another.