# Efficiency - Wed, Oct 16

## Measuring Efficiency

## Memoization

Remember results that have been computed before:

```
def memo(f):
cache = {}
def memoized(n):
if n not in cache:
cache[n] = f(n)
return cache[n]
return memoized
```

Multiple arguments? No problem!

```
def memo(f):
cache = {}
def memoized(*n):
if n not in cache:
cache[n] = f(*n)
return cache[n]
return memoized
```

There is a built-in memoization decorator:

```
from functools import lru_cache
@lru_cache(None)
```

## Exponentiation

One more multiplication lets us double the problem size:

```
def exp(b, n):
if n == 0:
return 1
else:
return b * exp(b, n - 1)
```

Here, doubling the input doubles the processing time. 1024x the input takes 1024x the time.

```
def exp_fast(b, n):
if n == 0:
return 1
elif n % 2 == 0:
return square(exp_fast(b, n // 2))
else:
return b * exp_fast(b, n - 1)
```

Here, doubling the input increases time by a constant time C. 1024x the input increases the time by 10 times C.

## Orders of Growth

### Quadratic Time

Incrementing `n`

increases time by `n`

times a constant.

- comparing two lists of length
`n`

for common elements

### Exponential Time

Incrementing `n`

multiplies time by a constant.

- tree-recursion with
`n`

recursive calls to process

### Linear Time

Incrementing `n`

increases time by a constant.

- sum of the digits of a number with length
`n`

### Logarithmic Time

Doubling `n`

only increments time by constant.

- the efficient exponent function for
`n`

### Constant Time

Increasing `n`

doesn't affect time.

## Space

### Which environment frames do we need to keep during evaluation?

At any moment, there is a set of active environments. All values and frames in active environments consume memory, but the memory occupied by other values and frames can be recycled. Python automatically takes care of this.

### What are active environments?

- Environments for any function calls that are currently being evaluated.
- Parent environments of functions named in active environments.