# 2. Micro-benchmarking

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil" -- Donald Knuth

## 2.1. Evaluation

• Fresh start of Python process

• Clean memory before start

• Same data

• Same start conditions, CPU load, RAM usage, `iostat`

• Do not measure how long Python wakes up

• Check what you measure

## 2.2. `timeit`

### 2.2.1. Programmatic use

Listing 399. Timeit simple statement
```from timeit import timeit

setup = 'name="Jose Jimenez"'
stmt = 'output = f"My name... {name}"'

duration = timeit(stmt, setup, number=10000)

print(duration)
# 0.0005737080000000061
```
Listing 400. Timeit multiple statements with setup code
```from timeit import timeit

setup = """
first_name = 'José'
last_name = 'Jiménez'
"""

TEST = dict()
TEST[0] = 'name = f"{first_name} {last_name}"'
TEST[1] = 'name = "{0} {1}".format(first_name, last_name)'
TEST[2] = 'name = first_name + " " + last_name'
TEST[3] = 'name = " ".join([first_name, last_name])'

for stmt in TEST.values():
duration = timeit(stmt, setup, number=10000)
print(f'{duration:.5}\t{stmt}')

# 0.00071559    name = f"{first_name} {last_name}"
# 0.0026514     name = "{0} {1}".format(first_name, last_name)
# 0.001015      name = first_name + " " + last_name
# 0.0013494     name = " ".join([first_name, last_name])
```
Listing 401. Timeit with `globals()`
```from timeit import timeit

def factorial(n: int) -> int:
if n == 0:
return 1
else:
return n * factorial(n-1)

duration = timeit(
stmt='factorial(500); factorial(400); factorial(450)',
globals=globals(),
number=10000,
)

duration = round(duration, 6)

print(f'factorial time: {duration} seconds')
# factorial time: 2.845382 seconds
```

### 2.2.2. Console use

Listing 402. Timeit
```python3 -m timeit -n100000 -r100 --setup='name="Jose Jimenez"' 'output = f"My name... {name}"'
# 100000 loops, best of 100: 55.9 nsec per loop

python3 -m timeit -n100000 -r100 --setup='name="Jose Jimenez"' 'output = "My name... {name}".format(name=name)'
# 100000 loops, best of 100: 327 nsec per loop

python3 -m timeit -n100000 -r100 --setup='name="Jose Jimenez"' 'output = "My name... %s" % name'
# 100000 loops, best of 100: 124 nsec per loop
```
```-n N, --number=N
how many times to execute ‘statement’

-r N, --repeat=N
how many times to repeat the timer (default 5)

-s S, --setup=S
statement to be executed once initially (default pass)

-p, --process
measure process time, not wallclock time, using time.process_time() instead of time.perf_counter(), which is the default

-u, --unit=U
specify a time unit for timer output; can select nsec, usec, msec, or sec

-v, --verbose
print raw timing results; repeat for more digits precision

-h, --help
print a short usage message and exit
```

## 2.3. Use cases

### 2.3.1. Setup

```DATA = [
{'Sepal length': 5.1, 'Sepal width': 3.5, 'Species': 'setosa'},
{'Petal length': 4.1, 'Petal width': 1.3, 'Species': 'versicolor'},
{'Sepal length': 6.3, 'Petal width': 1.8, 'Species': 'virginica'},
{'Sepal length': 5.0, 'Petal width': 0.2, 'Species': 'setosa'},
{'Sepal width': 2.8, 'Petal length': 4.1, 'Species': 'versicolor'},
{'Sepal width': 2.9, 'Petal width': 1.8, 'Species': 'virginica'},
]
```

## 2.4. Case Studies

### 2.4.1. Unique Keys

• Runtime: Jupyter `%%timeit`

Listing 403. Code 1
```%%timeit

fieldnames = set()

for row in DATA:
fieldnames.update(row.keys())

# 1.55 µs ± 12.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
```
Listing 404. Code 2
```%%timeit

fieldnames = set(key
for record in DATA
for key in record.keys())

# 1.91 µs ± 16.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
```
Listing 405. Code 3 (Is it correct?!)
```%%timeit

fieldnames = set()

for record in DATA
for key in record.keys())

# 416 ns ± 4.24 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
```
Listing 406. Code 4
```%%timeit

fieldnames = set()
fieldnames.update(tuple(x.keys()) for x in DATA)

# 2.05 µs ± 48.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```
Listing 407. Code 5
```%%timeit

fieldnames = list()

for row in DATA:
for key in row.keys():
fieldnames.append(key)

set(fieldnames)

# 2.28 µs ± 90.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```
Listing 408. Code 6
```%%timeit

fieldnames = set()

for row in DATA:
for key in row.keys():

# 2.01 µs ± 54.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```
Listing 409. Code 7
```%%timeit

fieldnames = list()

for row in DATA:
for key in row.keys():
if key not in fieldnames:
fieldnames.append(key)

# 2.09 µs ± 46.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

#### 2.4.1.1. Summary

• Code 3 appends generator object not values, this is why it is so fast!

### 2.4.2. Fibonacci

```%%timeit

def factorial_nocache(n: int) -> int:
if n == 0:
return 1
else:
return n * factorial_nocache(n - 1)

factorial_nocache(500)
factorial_nocache(400)
factorial_nocache(450)

# 283 µs ± 6.63 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
```%%timeit

CACHE = {}

def factorial_cache(n: int) -> int:
if n in CACHE:
return CACHE[n]

if n == 0:
return 1
else:
result = CACHE[n] = n * factorial_cache(n-1)
return result

factorial_cache(500)
factorial_cache(400)
factorial_cache(450)

# 153 µs ± 2.49 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
```
```%%timeit

CACHE = {}

def factorial_cache(n: int) -> int:
result = CACHE.get(n)

if result:
return result

if n == 0:
return 1
else:
result = CACHE[n] = n * factorial_cache(n-1)
return result

factorial_cache(500)
factorial_cache(400)
factorial_cache(450)

# 181 µs ± 10.3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
```
```%%timeit

CACHE = {}

def factorial_cache(n: int) -> int:
if n == 0:
return 1

try:
return CACHE[n]
except KeyError:
CACHE[n] = result = n * factorial_cache(n-1)
return result

factorial_cache(500)
factorial_cache(400)
factorial_cache(450)

# 618 µs ± 6.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
```%%timeit

CACHE = {}

def factorial_1(n: int) -> int:

def factorial(n: int) -> int:
if n == 0:
return 1
return n * factorial(n-1)

if not n in CACHE:
CACHE[n] = factorial(n)

return CACHE[n]

factorial_1(500)
factorial_1(400)
factorial_1(450)

# 283 µs ± 6.44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
```%%timeit

CACHE = {}

def factorial2(n: int) -> int:
if n == 0:
return 1

if n in CACHE:
return CACHE[n]

result = CACHE[n] = n * factorial2(n-1)
return result

factorial2(500)
factorial2(400)
factorial2(450)

# 153 µs ± 9.64 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
```