Skip to content

Commit 9b63e6f

Browse files
authored
Merge pull request #406 from realpython/python-profiling
Python Profiling - Initial Commit (Materials)
2 parents da81627 + dd0f5cf commit 9b63e6f

File tree

7 files changed

+214
-0
lines changed

7 files changed

+214
-0
lines changed

python-profiling/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Profiling in Python: How to Find Performance Bottlenecks
2+
3+
This folder holds sample code that supplements the Real Python tutorial [Profiling in Python: How to Find Performance Bottlenecks](https://realpython.com/python-profiling/).
4+
5+
## Profiler 1: `time`
6+
7+
```shell
8+
$ python profile_time.py
9+
sleeper()
10+
Real time: 1.75 seconds
11+
CPU time: 0.00 seconds
12+
13+
spinlock()
14+
Real time: 1.80 seconds
15+
CPU time: 1.80 seconds
16+
```
17+
18+
## Profiler 2: `timeit`
19+
20+
```shell
21+
$ python profile_timeit.py
22+
Average time is 0.15 seconds
23+
```
24+
25+
## Profiler 3: `cProfile`
26+
27+
```shell
28+
$ python profile_cprofile.py
29+
fib(35) = 9227465
30+
29860712 function calls (10 primitive calls) in 9.487 seconds
31+
32+
Ordered by: call count
33+
34+
ncalls tottime percall cumtime percall filename:lineno(function)
35+
29860703/1 9.487 0.000 9.487 9.487 profile_cprofile.py:5(fib)
36+
1 0.000 0.000 0.000 0.000 pstats.py:118(init)
37+
1 0.000 0.000 0.000 0.000 pstats.py:137(load_stats)
38+
1 0.000 0.000 0.000 0.000 pstats.py:108(__init__)
39+
1 0.000 0.000 0.000 0.000 cProfile.py:51(create_stats)
40+
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
41+
1 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}
42+
1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
43+
1 0.000 0.000 0.000 0.000 {built-in method builtins.len}
44+
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
45+
```
46+
47+
## Profiler 4: `pyinstrument`
48+
49+
Make sure to install the third-party library [`pyinstrument`](https://pypi.org/project/pyinstrument/) from the supplied `requirements.txt` file into a virtual environment first:
50+
51+
```shell
52+
$ python3 -m venv venv/
53+
$ source venv/bin/activate
54+
(venv) $ python -m pip install -r requirements.txt
55+
```
56+
57+
Then, you can run the sample script while you're still in the same virtual environment:
58+
59+
```shell
60+
(venv) $ python profile_pyinstrument.py
61+
n = 10 [2.8, 2.8, 2.8, 3.2, 2.4]
62+
n = 100 [3.04, 3.28, 3.24, 2.92, 3.24]
63+
n = 1,000 [3.176, 3.192, 3.188, 3.076, 3.132]
64+
n = 10,000 [3.1376, 3.1328, 3.172, 3.1292, 3.1376]
65+
n = 100,000 [3.14636, 3.13424, 3.15104, 3.14212, 3.14612]
66+
n = 1,000,000 [3.141736, 3.141708, 3.14168, 3.141828, 3.140708]
67+
n = 10,000,000 [3.14143, 3.1403824, 3.1411704, 3.1408808, 3.1420068]
68+
69+
_ ._ __/__ _ _ _ _ _/_ Recorded: 11:49:03 Samples: 243
70+
/_//_/// /_\ / //_// / //_'/ // Duration: 24.334 CPU time: 24.298
71+
/ _/ v4.5.0
72+
73+
Program: profile_pyinstrument.py
74+
75+
24.300 <module> profile_pyinstrument.py:1
76+
└─ 24.300 estimate_pi profile_pyinstrument.py:6
77+
├─ 23.200 <genexpr> profile_pyinstrument.py:7
78+
│ ├─ 15.200 point profile_pyinstrument.py:14
79+
│ │ ├─ 7.700 Random.uniform random.py:520
80+
│ │ │ [4 frames hidden] random, <built-in>
81+
│ │ │ 6.200 [self] None
82+
│ │ └─ 7.500 [self] None
83+
│ ├─ 4.600 [self] None
84+
│ └─ 3.400 hits profile_pyinstrument.py:10
85+
│ ├─ 2.400 [self] None
86+
│ └─ 1.000 abs None
87+
│ [2 frames hidden] <built-in>
88+
└─ 1.100 [self] None
89+
```
90+
91+
## Profiler 5: `perf`
92+
93+
Make sure to follow the setup instructions in the [Python 3.12 Preview: Support For the Linux perf Profiler](https://realpython.com/python312-perf-profiler/) tutorial.
94+
95+
Next, record samples into a local binary file named `perf.data`:
96+
97+
```shell
98+
$ sudo perf record -g -F 999 $HOME/python-custom-build/bin/python3 -X perf profile_perf.py
99+
```
100+
101+
Finally, display a report by issuing the following command while you're in the same folder:
102+
103+
```shell
104+
$ sudo perf report
105+
```
106+
107+
For an alternative view, try this instead:
108+
109+
```shell
110+
$ sudo perf report --hierarchy --sort comm,dso,sample
111+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from cProfile import Profile
2+
from pstats import SortKey, Stats
3+
4+
5+
def fib(n):
6+
return n if n < 2 else fib(n - 2) + fib(n - 1)
7+
8+
9+
with Profile() as profile:
10+
print(f"{fib(35) = }")
11+
Stats(profile).strip_dirs().sort_stats(SortKey.CALLS).print_stats()

python-profiling/profile_perf.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from concurrent.futures import ThreadPoolExecutor
2+
3+
4+
def find_divisors(n):
5+
return [i for i in range(1, n + 1) if n % i == 0]
6+
7+
8+
def slow_function():
9+
print("Slow thread started")
10+
try:
11+
return find_divisors(100_000_000)
12+
finally:
13+
print("Slow thread ended")
14+
15+
16+
def fast_function():
17+
print("Fast thread started")
18+
try:
19+
return find_divisors(50_000_000)
20+
finally:
21+
print("Fast thread ended")
22+
23+
24+
def main():
25+
with ThreadPoolExecutor(max_workers=2) as pool:
26+
pool.submit(slow_function)
27+
pool.submit(fast_function)
28+
29+
print("Main thread ended")
30+
31+
32+
if __name__ == "__main__":
33+
main()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from random import uniform
2+
3+
from pyinstrument import Profiler
4+
5+
6+
def estimate_pi(n):
7+
return 4 * sum(hits(point()) for _ in range(n)) / n
8+
9+
10+
def hits(point):
11+
return abs(point) <= 1
12+
13+
14+
def point():
15+
return complex(uniform(0, 1), uniform(0, 1))
16+
17+
18+
for exponent in range(1, 8):
19+
n = 10**exponent
20+
estimates = [estimate_pi(n) for _ in range(5)]
21+
print(f"{n = :<10,} {estimates}")
22+
23+
24+
with Profiler(interval=0.1) as profiler:
25+
estimate_pi(n=10_000_000)
26+
27+
profiler.print()

python-profiling/profile_time.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import time
2+
3+
4+
def sleeper():
5+
time.sleep(1.75)
6+
7+
8+
def spinlock():
9+
for _ in range(100_000_000):
10+
pass
11+
12+
13+
for function in sleeper, spinlock:
14+
t1 = time.perf_counter(), time.process_time()
15+
function()
16+
t2 = time.perf_counter(), time.process_time()
17+
print(f"{function.__name__}()")
18+
print(f" Real time: {t2[0] - t1[0]:.2f} seconds")
19+
print(f" CPU time: {t2[1] - t1[1]:.2f} seconds")
20+
print()

python-profiling/profile_timeit.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from timeit import timeit
2+
3+
4+
def fib(n):
5+
return n if n < 2 else fib(n - 2) + fib(n - 1)
6+
7+
8+
iterations = 100
9+
total_time = timeit("fib(30)", number=iterations, globals=globals())
10+
11+
print(f"Average time is {total_time / iterations:.2f} seconds")

python-profiling/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pyinstrument==4.5.0

0 commit comments

Comments
 (0)