Measuring the execution time of a program or code snippet is essential for understanding its performance and identifying potential bottlenecks.
Python provides various methods and modules to measure execution time, each suited for particular scenarios:
time.time()
: Ideal for measuring overall wall-clock time when absolute timestamps are important.time.perf_counter()
: Provides the highest available resolution in wall-clock time, perfect for precise benchmarking of small code segments.time.monotonic()
: Use this when comparing relative time intervals within your code, ensuring measurements aren’t affected by system clock adjustments.time.process_time()
: Isolates the CPU time consumed by your process, excluding time spent waiting for external factors.time.thread_time()
: Utilize this in multithreaded environments to track the CPU time used by a specific thread.timeit module
: Perfect for accurately timing small code snippets, taking into account repeated executions for reliable results.datetime module
: Useful when you need human-readable timestamps along with execution duration measurements.cProfile
: Indispensable for pinpointing performance bottlenecks in complex code, providing statistics on the time spent within individual functions.line_profiler
: Offers line-by-line performance measurements of your Python code for in-depth analysis.
Each of these tools and methods has its specific scenarios where it excels, from quick and simple timing with time.time()
to detailed performance analysis with cProfile
and line_profiler
. Choosing the right tool depends on the level of precision you need, the nature of the code being measured, and whether the focus is on wall-clock time, CPU time, or detailed profiling.
Let’s get started!
Wall-clock time vs. CPU time
When measuring execution time in Python, it’s crucial to distinguish between wall-clock time and CPU time.
Wall-clock time measures the real-world elapsed time from the moment your code begins execution to when it completes. This includes any time spent waiting for other processes, input/output operations, or other system-related delays.
CPU time, on the other hand, focuses purely on the amount of time your computer’s processor directly dedicates to executing your code’s instructions.
Understanding this difference is key when choosing the appropriate timing method.
Using time.time()
The time module in Python provides various functions for measuring execution time. One of the simplest functions is time.time()
. This function returns the current system time as a Unix Epoch timestamp, which represents the number of seconds elapsed since January 1, 1970, 00:00:00 UTC.
To measure execution time, you can capture the time.time()
value before and after executing your code, then calculate the difference between the two timestamps.
For example, let’s calculate how long it takes to square the numbers from 0 to 99,999.
import time
start_time = time.time()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = time.time()
elapsed_time = end_time - start_time
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.012177228927612305 seconds
It’s important to note that the time.time()
function measures wall-clock time, meaning it reflects the real-world time that passes during the execution of your code.
Using time.perf_counter()
The time module offers another function, time.perf_counter()
, which provides wall-clock time with higher precision compared to time.time()
.
The time.perf_counter()
function returns the value (in fractional seconds) of a performance counter, i.e. a clock with the highest available resolution to measure a short duration. It includes time elapsed during sleep states and operates system-wide. Therefore, it’s ideal for benchmarking code snippets where accuracy is paramount.
Let’s revisit the earlier example where we calculated the time to square numbers from 0 to 99,999. This time, we’ll use the time.perf_counter()
function and observe the difference.
import time
start_time = time.perf_counter()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = time.perf_counter()
elapsed_time = end_time - start_time
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.008495999965816736 seconds
If you compare the result with time.time()
, you’ll notice that time.perf_counter()
provides greater precision.
Using time.monotonic()
The time.monotonic()
function also provides wall-clock time, with the added characteristic of being monotonic, meaning it always increases (cannot go backwards) and is unaffected by system clock updates. This makes time.monotonic()
ideal for measuring relative durations between events within your code, rather than relying on absolute wall-clock timestamps.
Now, let’s revisit the same example, but this time using the time.monotonic()
function.
import time
start_time = time.monotonic()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = time.monotonic()
elapsed_time = end_time - start_time
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.015999999828636646 seconds
Using time.process_time()
Unlike previous functions that focus on measuring wall-clock time (the real-world time that passes during code execution), the time.process_time()
function specifically measures CPU time. This means it measures only the time during which the CPU is actively executing instructions for your Python code.
This makes it especially useful for timing code blocks without worrying about the interference from the time spent in sleep or I/O operations that do not use CPU resources.
Let’s use time.process_time()
to measure the CPU time required to square numbers from 0 to 99,999.
import time
start_time = time.process_time()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = time.process_time()
elapsed_time = end_time - start_time
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.015625 seconds
Note that the behavior of time.process_time()
remains consistent across different operating systems. This consistency makes it well-suited for reliably comparing code performance or benchmarking tasks on various platforms.
Using time.thread_time()
The time.thread_time()
function is similar to time.process_time()
, but with a crucial difference: It measures the CPU time consumed specifically by the current thread. This function is helpful in multithreaded environments where you want to isolate the performance of individual threads.
To illustrate, let’s measure the CPU time used in the squaring numbers example from a thread’s perspective. Even though our example code uses a single thread, understanding the concept is important:
import time
start_time = time.thread_time()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = time.thread_time()
elapsed_time = end_time - start_time
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.015625 seconds
Since we’re using a single-threaded program, the output of time.thread_time()
is the same as that of time.process_time()
. In a multithreaded environment, however, these functions would likely report different CPU time usage.
Using timeit module
The timeit module is designed for accurately measuring the execution time of small code snippets. It can be used in three ways: directly in your Python scripts, via the command line interface, and with magic commands.
- In Python Scripts: Functions like
timeit.timeit()
andtimeit.repeat()
execute your code multiple times, providing greater precision and smoothing out variations in timing. - Via Command Line: You can also use the timeit module from the command line, which is perfect for quick performance tests or comparisons between different code snippets.
- With Magic Commands: For convenient use within interactive environments like IPython or Jupyter Notebooks, timeit offers the
%timeit
and%%timeit
magic commands.
Let’s explore how to use the timeit module to measure execution time, through various examples. For a more detailed understanding of the timeit module, please refer to our comprehensive tutorial.
timeit.timeit()
The timeit.timeit()
function is the most common and convenient way to accurately measure the execution time of small code snippets.
It works by running your code snippet multiple times and then returning the total time taken (in seconds) for all those executions. This repeated execution helps average out any random fluctuations on your system, leading to a more reliable and precise measurement of your code’s performance.
Here’s a simple example to illustrate how to use timeit.timeit()
.
The code below measures the execution time of a simple Python loop that sums up integers from 0 to 99. It uses the timeit.timeit()
function to execute the loop specified in the my_code
string 1,000,000 times (the default number of executions in timeit.timeit()
) and then prints the total time taken for these executions.
import timeit
my_code = """
total = 0
for i in range(100):
total += 1
"""
time_taken = timeit.timeit(my_code)
print("Total time taken:", time_taken)
# Total time taken: 1.729800899978727
The timeit.timeit()
function offers several arguments to customize how it measures the execution time of your code. One of the most important is the number
argument. This argument specifies how many times to run your code snippet. By default, it’s set to 1,000,000 (one million) to get a statistically reliable timing.
In the example below, the number
argument is set to 10, meaning the loop will be executed 10 times to measure the overall execution time.
time_taken = timeit.timeit(my_code, number=10)
print("Total time taken:", time_taken)
# Total time taken: 1.950003206729889e-05
It’s important to understand that the timeit.timeit()
function returns the total time it took to execute your code snippet the specified number of times.
To get the average execution time for a single run, you need to divide this total time by the value you set for the number
argument. This division helps you achieve a more accurate benchmark.
print("Average time per execution:", time_taken/10)
# Average time per execution: 1.950003206729889e-06
timeit.repeat()
The timeit.repeat()
function is similar to timeit.timeit()
, but with an important enhancement. Instead of running your timing experiment just once, it runs it multiple times and provides a list of the execution times from each run.
The timeit.repeat()
function shares many of its arguments with timeit.timeit()
. The key addition is the repeat
argument. This argument controls how many times the entire timing experiment is repeated, giving you multiple execution measurements. The default is 5 repetitions.
Let’s revisit the example from timeit.timeit()
, but use timeit.repeat()
to run the timing experiment multiple times:
import timeit
import random
def my_function():
total = 0
for i in range(100):
total += random.random()
time_taken = timeit.repeat('my_function()', setup='import random', globals=globals(), repeat=3, number=10)
print("Total time taken:", time_taken)
# Total time taken: [5.7300087064504623e-05, 5.220016464591026e-05, 5.1999930292367935e-05]
The output is a list of timings, each in seconds. Each value in the list represents the total time it took to execute your code snippet 10 times during one measurement cycle. Since we repeated the measurement 3 times (repeat=3), you get 3 timings in the list.
%timeit and %%timeit Commands
Within interactive Python environments like Jupyter Notebook or IPython, the %timeit
and %%timeit
magic commands offer a convenient way to measure the execution time of your code.
The %timeit
magic command is designed to measure the execution time of single lines of Python code.
To use it, simply type %timeit
followed by your code snippet. For example, let’s see how long it takes to calculate the sum of numbers from 0 to 9:
In [1]: %timeit total = sum(range(10))
409 ns ± 0.462 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
%timeit
automatically determines the number of repetitions and loops to ensure accurate results while minimizing the execution time. You can customize this behavior with various options, such as:
-n
: Controls the number of loops executed within each run.-r
: Specifies how many times the timing experiment is repeated.-o
: Uses the best (fastest) timing out of all runs.
In [1]: %timeit -r 3 -n 100 total = sum(range(10))
419 ns ± 5.07 ns per loop (mean ± std. dev. of 3 runs, 100 loops each)
The %%timeit
command is similar to %timeit
but is used for timing multiple lines of code in a cell. The same options that apply to %timeit
also apply to %%timeit
, allowing you to customize the timing process.
In [1]: %%timeit -r 3 -n 100
...: import random
...:
...: def my_function():
...: total = 0
...: for _ in range(10):
...: total += random.random()
...:
539 ns ± 14.8 ns per loop (mean ± std. dev. of 3 runs, 100 loops each)
Using datetime Module
Python’s datetime module allows for timestamp-based execution time measurements. Using datetime.datetime.now()
, you can capture timestamps before and after code execution. The difference between these timestamps represents the elapsed wall-clock time.
When you subtract two datetime
objects, the result is a timedelta
object. This object represents the duration between the two timestamps. This timedelta
object has a convenient method called total_seconds()
that converts the time difference into a total number of seconds, giving you an execution time measurement.
Let’s calculate how long it takes to square the numbers from 0 to 99,999 using datetime Module:
from datetime import datetime
start_time = datetime.now()
# Code block you want to measure
for i in range(100000):
result = i * i
end_time = datetime.now()
elapsed_time = (end_time - start_time).total_seconds()
print("Execution time:", elapsed_time, "seconds")
# Execution time: 0.004011 seconds
Profiling with cProfile
The cProfile module is a powerful tool for identifying performance bottlenecks in your code. Unlike the other techniques that measure overall execution time, cProfile breaks down execution time statistics by individual function calls. This detailed level of information aids in targeting specific parts of your code for optimization.
import cProfile
def my_function():
for i in range(100000):
result = i * i
cProfile.run('my_function()')
# Output:
# 4 function calls in 0.005 seconds
#
# Ordered by: standard name
#
# ncalls tottime percall cumtime percall filename:lineno(function)
# 1 0.000 0.000 0.005 0.005 <string>:1(<module>)
# 1 0.005 0.005 0.005 0.005 Temp.py:3(my_function)
# 1 0.000 0.000 0.005 0.005 {built-in method builtins.exec}
# 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Profiling with line_profiler
Another popular profiler tool that offers detailed insights is called line_profiler. Unlike the cProfile module, which offers function-level performance analysis, line_profiler delivers line-by-line performance measurements of your Python code. This means it can pinpoint exactly which lines of code are consuming the most execution time.
If you need to identify performance bottlenecks within specific lines of a function, rather than just the overall function execution time, the line_profiler is an invaluable tool.
To use it, first install line_profiler using pip
pip install line_profiler
And, then use the @profile
decorator on the functions you want to profile:
@profile
def my_function():
# Your code here
for i in range(100000):
result = i * i
# Use kernprof to run your script
# kernprof -l -v yourscript.py