Search code examples
pythonimportpygameglobalexternal

Python importing globals faster than local variables?


So along with my friends I am making a game in Pygame, and I just got onto programming the Animation for the main character.

I figured there could be a couple ways to do this. Either make a list of the images and just loop over the list, or make a dictionary containing the coordinates of the pictures on a sprite sheet.

I went with the second option, and now I decided I want to check which one was faster. So I programmed both of them in quite a simplistic way just to quickly check how they perform.

The results were very close to each other around (0.41 with dictionaries, and around 0.40 without them). Then I decided I want to try out what would happen if I went with the first option except I would call pygame.image.load in a separate file and store the animation frames as global variables that would than be imported into the main file.

I though it would be really slow, because I read somewhere that python imports are extremely slow... But surprisingly I got a result of 0.021 of a second!

This is a huge change to performance, which might be crucial for my game, so I was wondering if anyone would happen to know why is this method so much faster, and is it only because of certain x or y why it is faster in this case and will be really slow in another.

Here is the code similar to the current state of my game:

    import pygame
import time
start_time = time.time()
playerSpriteSize = 192
img = (0, 0)
class SpriteSheet():
    def __init__(self, filename):
        self.sheet = pygame.image.load(filename).convert()

    def get_image(self, coords, size, flip=False):
        surf = pygame.Surface(size).convert()
        surf.blit(self.sheet, (0, 0), (coords[0], coords[1], size[0], size[1]))
        pygame.transform.flip(surf, False, True)
        surf.set_colorkey((0, 0, 0))
        surf = pygame.transform.flip(surf, flip, False)
        return surf

pygame.init()

displaySurface = pygame.display.set_mode((400, 400))
x = SpriteSheet("playerAnimation.png")


animationList = {0: (0, playerSpriteSize*0), 1: (0, playerSpriteSize*1), 2: (0, playerSpriteSize*2),
                 3: (0, playerSpriteSize*3), 4: (0, playerSpriteSize*4), 5: (0, playerSpriteSize*5),
                 6: (0, playerSpriteSize*6)}
rounda = 0
for i in animationList:
    a1 = x.get_image(animationList[rounda], (playerSpriteSize, playerSpriteSize))
    displaySurface.blit(a1, (30, 30))
    rounda += 1
    pygame.display.update()


print("--- %s seconds ---" %(time.time() - start_time))

Now this is the code that used images loaded before the main game loop:

import pygame
import time
start_time = time.time()
imageSize = 192
img = (0, 0)
class SpriteSheet():
    def __init__(self, filename):
        self.sheet = pygame.image.load(filename).convert()

    def get_image(self, coords, size, flip=False):
        surf = pygame.Surface(size).convert()
        surf.blit(self.sheet, (0, 0), (coords[0], coords[1], size[0], size[1]))
        pygame.transform.flip(surf, False, True)
        surf.set_colorkey((0, 0, 0))
        surf = pygame.transform.flip(surf, flip, False)
        return surf

pygame.init()

displaySurface = pygame.display.set_mode((400, 400))
x = SpriteSheet("playerAnimation.png")

a1 = x.get_image((0, 0), (imageSize, imageSize))
a2 = x.get_image((0, 192), (imageSize, imageSize))
a3 = x.get_image((0, 384), (imageSize, imageSize))
a4 = x.get_image((0, 576), (imageSize, imageSize))
a5 = x.get_image((0, 768), (imageSize, imageSize))
a6 = x.get_image((0, 960), (imageSize, imageSize))
a7 = x.get_image((0, 1152), (imageSize, imageSize))

animationList = [a1, a2, a3, a4, a5, a6, a7]

for i in animationList:
    displaySurface.blit(i, (30, 30))
    pygame.display.update()

print("--- %s seconds ---" %(time.time() - start_time))

and this is the code(split into 2 files) that runs in around 0.021 seconds: File 1 (main file)

import pygame
import time
from mainDifferentExtern import a1, a2, a3, a4, a5, a6, a7


start_time = time.time()
imageSize = 192
img = (0, 0)
class SpriteSheet():
    def __init__(self, filename):
        self.sheet = pygame.image.load(filename).convert()

    def get_image(self, coords, size, flip=False):
        surf = pygame.Surface(size).convert()
        surf.blit(self.sheet, (0, 0), (coords[0], coords[1], size[0], size[1]))
        pygame.transform.flip(surf, False, True)
        surf.set_colorkey((0, 0, 0))
        surf = pygame.transform.flip(surf, flip, False)
        return surf

pygame.init()

displaySurface = pygame.display.set_mode((400, 400))
x = SpriteSheet("playerAnimation.png")

animationList = [a1, a2, a3, a4, a5, a6, a7]

for i in animationList:
    displaySurface.blit(i, (30, 30))
    pygame.display.update()

print("--- %s seconds ---" %(time.time() - start_time))

and the file that it imports from:

import pygame
imageSize = 192


class SpriteSheet():
    def __init__(self, filename):
        self.sheet = pygame.image.load(filename).convert()

    def get_image(self, coords, size, flip=False):
        surf = pygame.Surface(size).convert()
        surf.blit(self.sheet, (0, 0), (coords[0], coords[1], size[0], size[1]))
        pygame.transform.flip(surf, False, True)
        surf.set_colorkey((0, 0, 0))
        surf = pygame.transform.flip(surf, flip, False)
        return surf

pygame.init()
pygame.display.set_mode((1, 1))
x = SpriteSheet("playerAnimation.png")

a1 = x.get_image((0, 0), (imageSize, imageSize))
a2 = x.get_image((0, 192), (imageSize, imageSize))
a3 = x.get_image((0, 384), (imageSize, imageSize))
a4 = x.get_image((0, 576), (imageSize, imageSize))
a5 = x.get_image((0, 768), (imageSize, imageSize))
a6 = x.get_image((0, 960), (imageSize, imageSize))
a7 = x.get_image((0, 1152), (imageSize, imageSize))

EDIT: As suggested by match, I have changed the method of measuring the time, and instead went with python profiling (using cProfile), but I got roughly the same results, except the times for the first two methods came out to be a bit longer


Solution

  • I believe the difference here is due to python compiling bytecode for any imported modules. This in turn will speed up the loading/execution of the import. You can see these as .pyc files in the directory alongside the original code.

    I suspect that if you do the following your results will balance out again:

    rm *.pyc
    PYTHONDONTWRITEBYTECODE=1 python mygame.py
    

    Also bear in mind that a chance of 0.02 seconds is well within the normal 'drift' of any system - which is why tools like timeit run the same code many thousands of times and average the results.

    On that note - consider how many times your potentially slow operation occurs in the code - if you only load the images once, and your performance gain by doing it differently is 0.02 seconds - while the game is expected to run for minutes or hours, then this is looking suspiciously like a premature optimisation.