[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

Re: [pygame] Re: Python optimization help



On 20.01.2012 04:45, Robert Xiao wrote:
> If you are CPU-bound, extra threads won't help in Python due to the GIL (it limits an entire Python process to one core, even with multiple threads). 

He doesn't use threads though, he uses multiprocessing, which makes your
point somewhat moot. Also, from my own experiments, multiple threads DO
use multiple cores, they just interfere with (i.e. block) each other due
to the GIL. But you're right in that also multiple processes must
compete for the available processing power, even if they do that more
efficiently than threads in Python.

>> On 1/19/2012 5:32 PM, Silver wrote:
>>> I'm trying to speed up this program as much as possible. Anyone able to
>>> help?
>>>
>> Waugh. Old version...

It would be nice, when you post code for others to examine, to

a) remove any unused code (i.e. "Consumer" and "Task"),
b) remove comments that refer to old versions of the code,
c) include a clean way to exit the program and
d) use more descriptive variable names.

Bad examples: "x", "lastx", "itemlist", "result" (when you really mean
"gravitars"), "i" in the for loop in "attract" - "body" or
"other_gravitar" would have been better here.

> Oddly enough, lowering the amount of extra workers in the pool results
> in a higher speed?

How do you measure speed? Where's your FPS counter? Have you timed your
"attract" function with the timeit module?

Your example uses 12 workers but your code is totally CPU bound. How
many CPUs / cores does your computer have? Adding workers doesn't
magically add new cores ;) Communication between the main process and
the worker processes adds some overhead as well as process switching
(the latter should be very fast on Unices though), so even if your
system had 12 cores, the speed wouldn't scale linearly from 1 to 12 cores.

One way to speed up your program without resorting to
hardware-acceleration would be to implement your main calculation
function "attract" in C with the help of Cython (cython.org). A little
static type information added can sometimes do wonders.

Finally a few oddities in your code, which aren't necessarily speed related:

- Why do you use pool.map_async() when you then use the blocking
res.get() immediately anyway? Just use pool.map().

- Your way of constructing the list of arguments to the workers isn't
very clear with the use of the self-referencing list. Also, passing the
full list with each job in each iteration certainly adds some overhead
(though probably negligible compared to the time spent in the "attract"
function). Consider using a shared state for this list via
multiprocessing.Manager().

- The "genlist" function seems a bit pointless. Why not create a
two-item list directly in your __main__ statement block and maintain the
list item counter "x" also there? No need for ugly globals.

    gcount = 2
    gravitars = [Gravitar(i) for i in range(gcount)]

- The use of (screen) "width" and "height" as global variables in
"Gravitar" and "attract" is unclean and prone to subtle errors in a
multiprocessing environment. I would introduce a world object, which is
passed to "attract" (or accessible via a shared state).

Attached is a IMHO much cleaner, but not yet optimized, version of your
script.

HTH, Chris
#!/usr/bin/env python

import math
import multiprocessing
import random
import time

import pygame

NUM_PROCS = 12
NUM_GRAVITARS = 2
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 550
DECELERATION = .999

class Gravitar(object):
    def __init__(self, id):
        self.id = id
        self.x = random.random() * SCREEN_WIDTH
        self.y = random.random() * SCREEN_HEIGHT
        self.dx = 0
        self.dy = 0
        self.mass = random.randint(4, 8)

    def __repr__(self):
        return ("<Gravitar id=%(id)i mass=%(mass)i x=%(x)i, y=%(y)i "
            "dx=%(dx)i, dy=%(dy)i>") % self.__dict__

## primary function
def attract(gravitars):
    exc = 0
    gravitar = gravitars[0]

    for other in gravitars[1]:
        if other.id != gravitar.id:
            if gravitar.mass > other.mass:
                greatermass = gravitar.mass
            else:
                greatermass = other.mass

            xdiff = gravitar.x - other.x
            ydiff = gravitar.y - other.y
            dist = math.sqrt((xdiff ** 2) + (ydiff ** 2))

            if dist < greatermass:
                dist = greatermass

            # landmark
            acceleration = (((2 * (other.mass * gravitar.mass)) / (dist ** 2))
                / gravitar.mass)

            xc = xdiff / dist
            yc = ydiff / dist
            gravitar.dx -= acceleration * xc
            gravitar.dy -= acceleration * yc
        else:
            exc += 1

    if exc > 1:
        #print "Exc is" + str(exc)
        pass
        #raise ValueError, exc

    gravitar.x += gravitar.dx
    gravitar.y += gravitar.dy

    if gravitar.x > SCREEN_WIDTH or gravitar.x < 0:
        gravitar.dx *= -1

    if gravitar.y > SCREEN_HEIGHT or gravitar.y < 0:
        gravitar.dy *= -1

    gravitar.dx *= DECELERATION
    gravitar.dy *= DECELERATION

    return gravitar


def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

    # Establish communication queues
    pool = multiprocessing.Pool(processes=NUM_PROCS)

    gcount = NUM_GRAVITARS
    gravitars = [Gravitar(i) for i in range(gcount)]
    loop_exit = False
    clock = pygame.time.Clock()
    elapsed = 0.0

    while True:
        screen.fill((0,0,0))

        for gravitar in gravitars:
            pygame.draw.circle(screen, (255,255,255),
                (int(gravitar.x), int(gravitar.y)), int(gravitar.mass))

        pygame.display.update()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                loop_exit = True
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    loop_exit = True
                else:
                    gravitars.append(Gravitar(gcount))
                    gcount += 1
                    print gravitars

        if loop_exit:
            break

        gravitars = pool.map(attract, [[g, gravitars] for g in gravitars])

        elapsed += clock.tick()

        # print FPS every five seconds
        if elapsed > 5000:
            print "FPS:", clock.get_fps()
            elapsed = 0.0

if __name__ == '__main__':
    main()