If you change field_coins
from 5000
to 1000000
and set search_radius
to 0.1
, the application will take quite a bit of time to finish. Here is the updated main execution code with these new parameters:
If you increase the coins further or make the search radius even smaller, it will severely affect the application runtime.
Tip
Warning!
If you run the following code, depending on your machine configuration, it can slow down your machine, take longer time to finish...
Identifying the bottlenecks
In the previous section, we saw how a different choice of input parameters degrades the application runtime. Now, we need some way to accurately measure the execution time and find out the performance bottlenecks or the time consuming blocks of the code.
Measuring the execution time
Let's start by monitoring the time taken by the application. To do this, we will use Python's built-in time
module. The time.perf_counter
function is a performance counter that returns a clock with the highest available resolution. This function can be used to determine the time interval or the system-wide time difference between the two consecutive calls to the function.
Tip
The time.perf_counter
function is available in Python versions 3.3 onwards. If you have an older version of Python (for example, version 2.7), use time.clock()
instead. On Unix, time.clock()
returns a floating point number within seconds that represents the processor time. On Windows, it returns the elapsed wall-clock...
The profiling techniques we have covered so far aim at finding the runtime bottlenecks. Let's briefly discuss memory profiling, another important aspect of profiling.
The memory_profiler package
For memory profiling, we will use a popular Python package called memory_profiler
. It can be installed using pip
. Here is how to install it on Linux from the command line:
The documentation highly recommends installing the psutils
module. It also suggests that, in order for memory_profiler
to work on Windows OS, you will need the psutil
module. The psutil
module can be installed using pip
, as follows:
Just like line_profiler
, the memory_profiler
package uses the @profile
decorator above the function name. Let's add the decorator @profile
just above the generate_random_points
function, and then run the memory profiler...
Algorithm efficiency and complexity
An algorithm is a set of instructions to solve a particular problem. In this context, an algorithm can be a function or even a simple operation that adds two numbers. Let's understand two related terms: algorithm efficiency and algorithm complexity.
Algorithm efficiency indicates the computation resources consumed by an algorithm. Typically, the lower the resource consumption, the better the efficiency. The computational resources can mean several things. One can be talking about the runtime (CPU usage), the memory consumption (RAM or hard disk) or the network consumption, or a combination of these.
The application requirement determines which resource takes precedence over the others. For example, in a web application, the network usage can be more important than the disk space. For a scientific application, you might have all the memory you need but the runtime can be a pain in the neck, and so on. In this book, we will limit our discussion...
In simple terms, the big O or big Oh notation is a way to represent the computational complexity of an algorithm. Here, the O is the letter O, as in order, and not the number zero. The big O indicates an upper bound or the worst-case scenario of the complexity of an algorithm (details to follow in the next section). This concept can be better explained with an example. Let's take a look at the following code:
Let's call this trivial code fragment an algorithm. It is a simple operation that appends a number to the list
inside a for
loop. Here, num
represents the size of the input used by the algorithm. If you increase num
, the algorithm will have to do more work inside the for
loop. Increase it further, and the poor algorithm will have to do even more work. Thus, the time taken by the algorithm depends on the value of num
and can be expressed as a growth function, f(n). Here, n represents the size of the input that corresponds...
This chapter was the first one in the series of three chapters based on performance. It laid the ground work to improve application performance. We learned how to record the runtime using the time
module. We also saw how the timeit
module can be used to measure the performance of small pieces of code. We took a practical problem where an application ran fine when working with a small input, but, as the input grew larger, it slowed down considerably. With this example, we learned how to identify the bottlenecks using cProfile
and display the results using pstats
.
We saw how the line_profiler
module can help locate the time consuming statements inside a function. While most of the discussion was focused on the runtime performance, we briefly covered the memory_profiler
module. This module enabled line-by-line analysis of memory consumption for the given functions. Finally, we learned about the big O notation that represents the computational complexity of an algorithm.
Now that we have...