33features are added and extra layers of complexity to the main features.
44Benchmarking is an approach that helps developers use profiling metrics
55and their code intuition to optimize programs further. This module uses
6- cProfile to compare the performance of two functions with each other.
6+ cProfile to compare the performance of two functions with each other,
7+ showcasing the impact of caching/memoization with `functools.cache`.
78"""
89
910import cProfile
1011import io
1112import pstats
12- import time
13+ from functools import cache
1314
14- # Module-level constants
15- _SLEEP_DURATION = 0.001
1615
16+ def fib_naive (n : int ) -> int :
17+ """Calculate Fibonacci number recursively without caching.
1718
18- def finish_slower () -> None :
19- """Finish slower by sleeping more."""
20- for _ in range (20 ):
21- time .sleep (_SLEEP_DURATION )
19+ This has O(2^N) time complexity due to redundant calculations.
20+ """
21+ if n < 2 :
22+ return n
23+ return fib_naive (n - 1 ) + fib_naive (n - 2 )
2224
2325
24- def finish_faster () -> None :
25- """Finish faster by sleeping less."""
26- for _ in range (10 ):
27- time .sleep (_SLEEP_DURATION )
26+ @cache
27+ def fib_cached (n : int ) -> int :
28+ """Calculate Fibonacci number recursively with caching.
29+
30+ This has O(N) time complexity as each value is computed only once.
31+ """
32+ if n < 2 :
33+ return n
34+ return fib_cached (n - 1 ) + fib_cached (n - 2 )
2835
2936
3037def main () -> None :
3138 # Create a profile instance
3239 profile = cProfile .Profile ()
3340
41+ # Clear the cache before benchmarking to ensure clean metrics
42+ fib_cached .cache_clear ()
43+
3444 profile .enable ()
3545
36- for _ in range ( 2 ):
37- finish_slower ( )
38- finish_faster ( )
46+ # Compare naive vs cached Fibonacci calculations for N=15
47+ fib_naive ( 15 )
48+ fib_cached ( 15 )
3949
4050 profile .disable ()
4151
@@ -46,17 +56,17 @@ def main() -> None:
4656 buffer = io .StringIO ()
4757 ps = pstats .Stats (profile , stream = buffer ).sort_stats ("cumulative" )
4858
49- # Notice how many times each function was called. In this case, the main
50- # bottleneck for `finish_slower` and `finish_faster` is `time.sleep`
51- # which occurred 60 times. By reading the code and the statistics, we
52- # can infer that 40 occurrences came from `finish_slower` and 20 came
53- # from `finish_faster`. It is clear why the latter function runs faster
54- # in this case, but identifying insights like this are not simple in
55- # large projects. Consider profiling in isolation when analyzing complex
56- # classes and functions
59+ # print_stats writes the tabular profiling info to the buffer
5760 ps .print_stats ()
58- time_sleep_called = any ("60" in line and "time.sleep" in line for line in buffer .getvalue ().split ("\n " ))
59- assert time_sleep_called is True
61+ output = buffer .getvalue ()
62+
63+ # Naive recursive fib(15) requires exactly 1,973 calls.
64+ # Cached recursive fib(15) requires exactly 16 calls (inputs 0 to 15).
65+ naive_called = any ("1973" in line and "fib_naive" in line for line in output .split ("\n " ))
66+ cached_called = any ("16" in line and "fib_cached" in line for line in output .split ("\n " ))
67+
68+ assert naive_called is True
69+ assert cached_called is True
6070
6171
6272if __name__ == "__main__" :
0 commit comments