# Program performance

We’ve talked about making programs more readable and concise. We have not yet
talked very much about how to make programs *fast*. Today, we’ll start to learn
how to reason carefully about program performance.

Here’s a Python program:

def sum(l: list) -> int: s = 0 for x in l: s += x return s

What’s the running time of this Python program? As we talked about in the first lecture, one way of answering this question would be to pull out our pocket watch and measure it. As we showed in that lecture, though, this approach has flaws. This function’s running time is going to depend on the machine it is run on, what other programs are running, and a fair amount of random chance! Moreover, its running time is going to depend on the data it’s passed in. We probably can’t run it on all possible lists of numbers, so how can we predict how fast it will run on an arbitrary list?

Let’s see if we can figure out how long it’s going to take without running it. We can start by counting the “basic operations,” like adding numbers or assigning to a variable, executed in the function:

def sum(l: list) -> int: s = 0 # 1 operation for x in l: # loop executes len(l) times s += x # 2 operations return s # 1 operation

So for a list of length `L`

, the program executes (2 * L) + 2 operations. If we
assume that each operation takes the same amount of time, we can say that its
running time is proportional to (2 * L) + 2.

Notice how many assumptions we’re making here! For one, we’re not counting the for-loop itself as an operation–do we need to? Python’s operations probably don’t actually take the same amount of time–does it matter?

For some purposes, the answers to these questions **absolutely** matter. Some
code is run inside Google every time a user does a web search–the exact
running time of that code is **very** important to Google! For our purposes in
this class, though–and for most of the code I’ve ever written–more precision
than we’ve talked about here just isn’t important.

In fact, even this much precision is more than we’ll generally use going
forward! Given that we’re not really sure about either of the constants in (2 *
L) + 2–after all, if returning from a function takes a while it could be more
like (2 * L) + 10, and if addition takes a while it could be (10 * L) +
2–we’ll just ignore them and say that our function’s running time is
*linear in L* –that is, it does some constant number of operations for each element of
L. Next class we’ll introduce some commonly-used notation for expressing this
idea.

Before that, let’s look at a couple more functions. How about this one?

def member(l: list, el) -> bool: for x in l: if x == el: return True return False

How long does this function take, for a list of length *L*?

The running time of `member`

depends on its arguments even more than the
running time of `sum`

did! If we call it on a list whose first element is `el`

,
it really doesn’t matter how long the list is–it’s going to take the same
amount of time. This is the *best-case* running time: *constant time*.

Computer programming is an engineering discipline, though. If I’m building a
bridge, I’m probably not all that interested in the maximum weight it can
tolerate in the **best case**! What we’re usually interested in (though we’ll see some
exceptions, later in the course) is *worst-case* running time: how long will
the function take on the worst possible input or inputs?

In the case of `member`

, it will take the longest when `el`

isn’t actually in
`l`

. In that case, it runs in linear time.

We’ll take a look at one last function:

def distinct(l: list) -> list: d = [] for x in l: if x not in d: d.append(x) return d

(As it turns out, `x not in d`

is basically equivalent, in Python, to calling
`not member(d, x)`

. So, it takes linear time in the length of `d`

.)

What’s the best case? Well, if all of the elements in `l`

are the same,
`x not in d`

never takes more than a constant number of operations. So in that
case, the function runs in linear time.

How about the worst case? What if the elements are all different? See the lecture capture for details, but in this case, the function takes this many operations:

(0 + 1 + 2 + ... + (L - 1)) + <some constant>

As it happens, this can be simplified to:

(L * (L - 1)) / 2 + <some constant>

So in this worst-case scenario, our function’s running time is *quadratic in L*.