Logo (2 kids swinging on a swing)

60 FPS

There is a limit to what you can do in 16 ⅔ milliseconds. If it takes you 17 milliseconds to calculate a frame then you've failed and are only running at 58 FPS.

So what kinds of things end up wasting cpu time?

What can you do to speed up your code?

The information below will vary based on the hardware used but hopefully it can be of help.

If you take a look at this profiling script you will see that each class gets the same variables assigned to it. The only difference is how many functions get called in the process.

1,000,000 Loops - 1.1580 seconds
class Class1: def __init__(self): self.a = 1 self.b = 0 self.c = None self.z = 2 for x in range(1000000): a = Class1()
1,000,000 Loops - 1.5977 seconds
class Class2: def __init__(self): self.a = 1 self.call1() self.c = None self.z = 2 def call1(self): self.b = 0 for x in range(1000000): a = Class2()
1,000,000 Loops - 1.9694 seconds
class Class3: def __init__(self): self.a = 1 self.call1() self.c = None self.call2() def call1(self): self.b = 0 def call2(self): self.z = 2 for x in range(1000000): a = Class3()
1,000,000 Loops - 2.3546 seconds
class Class4: def __init__(self): self.call0() self.call1() self.c = None self.call2() def call0(self): self.a = 1 def call1(self): self.b = 0 def call2(self): self.z = 2 for x in range(1000000): a = Class4()

In the table you can see that the time to call a million loops takes longer for each additional funcion called. It is roughly 0.4 seconds longer each time. 0.4 seconds in a million loops means it takes about 0.0004 milliseconds per function call.

This means that you could do about 40,000 function calls in a single frame, but it also means you'd have time to do nothing else.

So one way to cut down on wasted time is to not create and call extra functions for no reason. I'm not saying write all of your code in as few functions as possible. Just realize that each function takes time to call and be smart about it.

class Example2: def __init__(self): self.reseta() self.resetb() self.resetc() self.resetz() def reset(self): self.reseta() self.resetb() self.resetc() self.resetz() def reseta(self): self.a = 1 def resetb(self): self.b = 0 def resetc(self): self.c = none def resetz(self): self.z = 2 def do(self): # Do stuff # Then Reset self.reset()
class Example1: def __init__(self): self.a = 1 self.b = 0 self.c = None self.z = 2 def do(self): # Do stuff # Then Reset self.a = 1 self.b = 0 self.c = None self.z = 2

If you create an instance of Example1 and call the do function you end up calling 2 functions.

If you create an instance of Example2 and call the do function you end up calling 11 functions. You've roughly wasted 0.0036 milliseconds for the additional function calls which may not seem like much, but it all adds up.

a = None

20,000,000 Loops - 1.2191 seconds
a = None for i in range(20000000): if a == None: pass
20,000,000 Loops - 0.9911 seconds
a = None for i in range(20000000): if a is None: pass
20,000,000 Loops - 1.2120 seconds
a = None for i in range(20000000): if a != None: pass
20,000,000 Loops - 0.9312 seconds
a = None for i in range(20000000): if a is not None: pass

b = 'Test'

20,000,000 Loops - 1.2708 seconds
b = 'Test' for i in range(20000000): if b == None: pass
20,000,000 Loops - 0.9399 seconds
b = 'Test' for i in range(20000000): if b is None: pass
20,000,000 Loops - 1.3807 seconds
b = 'Test' for i in range(20000000): if b != None: pass
20,000,000 Loops - 1.0024 seconds
b = 'Test' for i in range(20000000): if b is not None: pass

Both ways pass and fail tests the same. So you should always use is. Is is best used to compare python types like str or int.

If you create 2 strings with the same text is will say they are the same but that is because python is assigning both variables to the same string in the underlying c code. You can verify this by using the id function on each variable. If they return the same number then they are the same object.

WARNING: Multiple assignment should only be used with immutable objects.

a = None

20,000,000 Loops - 1.2846 seconds
a = None for i in range(20000000): x = a y = a
20,000,000 Loops - 1.6735 seconds
a = None for i in range(20000000): x = a y = a z = a
20,000,000 Loops - 0.9998 seconds
a = None for i in range(20000000): x = y = a
20,000,000 Loops - 1.1333 seconds
a = None for i in range(20000000): x = y = z = a

b = 0

20,000,000 Loops - 1.2594 seconds
b = 0 for i in range(20000000): x = b y = b
20,000,000 Loops - 1.8537 seconds
b = 0 for i in range(20000000): x = b y = b z = b
20,000,000 Loops - 1.0023 seconds
b = 0 for i in range(20000000): x = y = b
20,000,000 Loops - 1.2631 seconds
b = 0 for i in range(20000000): x = y = z = b

c = {}

20,000,000 Loops - 1.4074 seconds
c = {} for i in range(20000000): x = c y = c
20,000,000 Loops - 1.7338 seconds
c = {} for i in range(20000000): x = c y = c z = c
20,000,000 Loops - 1.0480 seconds
c = {} for i in range(20000000): x = y = c
20,000,000 Loops - 1.1935 seconds
c = {} for i in range(20000000): x = y = z = c

Assigning something to multiple variables at the same time is fast but you need to make sure the object you are assigning is immutable.

Immutable objects like None, integers, or tuples do not allow the modification of the object in place. Changes to these variables will cause a new object to be created so changing one variable will never change another.

Mutable objects like a dictionary or list allow you to modify the object in place. This makes it appear like changes to one variable cause changes to the other variables but in fact both variables point to the same object.

Empty List or Tuple

20,000,000 Loops - 4.5650 seconds
for i in range(20000000): x = list(())
20,000,000 Loops - 4.3342 seconds
for i in range(20000000): x = tuple([])
20,000,000 Loops - 0.7779 seconds
for i in range(20000000): x = ()
20,000,000 Loops - 1.0145 seconds
for i in range(20000000): x = []

List or Tuple Containing '1'

20,000,000 Loops - 8.7496 seconds
for i in range(20000000): x = list(('1', ))
20,000,000 Loops - 8.7719 seconds
for i in range(20000000): x = tuple(['1'])
20,000,000 Loops - 0.6384 seconds
for i in range(20000000): x = ('1', )
20,000,000 Loops - 3.4372 seconds
for i in range(20000000): x = ['1']

List or Tuple Containing 1, 2

20,000,000 Loops - 8.5801 seconds
for i in range(20000000): x = list((1, 2))
20,000,000 Loops - 9.0448 seconds
for i in range(20000000): x = tuple([1, 2])
20,000,000 Loops - 0.6529 seconds
for i in range(20000000): x = (1, 2)
20,000,000 Loops - 3.7368 seconds
for i in range(20000000): x = [1, 2]

Python provides lists and tuples as sequence types. Lists are mutable allowing you to change their contents after being created. Tuples are immutable and are faster to create as you can see in the table above.

As you can see in the table above it is a lot faster to create a tuple than a list. You can also see that converting between a tuple and a list takes a fair amount of time. So create a list or tuple depending on what you are going to do with it.

Creating code that turns a tuple into a list so it can modify something and then convert it back to a tuple just wastes cpu time.

5,000,000 Loops - 1.9969 seconds
class NotStaticA: def test(self, a): return a for i in range(5000000): x = NotStaticA().test(i)
5,000,000 Loops - 1.8878 seconds
class StaticA: @staticmethod def test(a): return a for i in range(5000000): x = StaticA().test(i)

As you can see a static method runs faster than a non static method. So if you don't use self inside a function make it a static method and get rid of self.

Loops can be a great place where cpu time can get wasted. Listed here are a number of ways that can make a loop faster. These suggestions make a big difference if the loop is running a lot. If the loop is running 2 or 3 times then you will not see much of a difference. If the loop is only running once, why is there a loop?

The fastest loop is a loop that does nothing.

10,000,000 loops - 0.3576 seconds
for z in range(1000000): pass

If you want your loop to be as fast as possible try doing the following:

  1. Never create an object inside a loop that can get created above it.
    10,000,000 loops - 2.1863 seconds
    y = () for z in range(10000000): x = (list, tuple) if type(y) in x: pass
    10,000,000 loops - 1.4352 seconds
    y = () x = (list, tuple) for z in range(1000000): if type(y) in x: pass
  2. If you need to access an objects attributes inside the loop and the value in the attribute never changes then assign it to a variable before the loop.
    10,000,000 loops - 0.6395 seconds
    class A: def __init__(self): x = 1 y = A() for z in range(10000000): if y.x: pass
    10,000,000 loops - 0.3735 seconds
    class A: def __init__(self): x = 1 y = A() x = y.x for z in range(10000000): if x: pass
  3. If you need to call the function of an object inside the loop and the object never changes then assign the function (not called) to a variable before the loop. It takes time to lookup a function on an object.
    10,000,000 loops - 2.6545 seconds
    x = [] for z in range(1000000): x.append(z)
    10,000,000 Loops - 2.0179 seconds
    x = [] y = x.append for z in range(1000000): y(z)
  4. If you need to access an objects attributes inside the loop and the value in the attribute does change but the object is always the same then assign the object to a variable before the loop.
    10,000,000 loops - 5.5933 seconds
    class A: def __init__(self): self.value = 0 @property def x(self): value = self.value self.value += 1 return value class B: def __init__(self, y): self.y = y b = B(A()) for z in range(1000000): if b.y.x: pass
    10,000,000 loops - 4.8776 seconds
    class A: def __init__(self): self.value = 0 @property def x(self): value = self.value self.value += 1 return value class B: def __init__(self, y): self.y = y b = B(A()) a = b.y for z in range(1000000): if a.x: pass