Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Magic Optimisation

0 views
Skip to first unread message

simonw...@gmail.com

unread,
Sep 4, 2005, 11:49:06 PM9/4/05
to
Hello People.

I've have a very tight inner loop (in a game app, so every millisecond
counts) which I have optimised below:

def loop(self):
self_pool = self.pool
self_call_exit_funcs = self.call_exit_funcs
self_pool_popleft = self.pool.popleft
self_pool_append = self.pool.append
check = self.pool.__len__
while check() > 0:
task = self_pool_popleft()
try:
task.next()
except StopIteration:
self_call_exit_funcs(task)
return
self_pool_append(task)

This style of optimisation has shaved _seconds_ from my iteration
cycle, esp. when I have many registered tasks, so this style of
optimisation is very important to me.

However, it is very ugly. Does anyone have any tips on how I could get
this optimisation to occor magically, via a decorator perhaps?

Sw.

Paul McGuire

unread,
Sep 5, 2005, 12:00:33 AM9/5/05
to
This isn't much prettier, but what if you extract the try-except
overhead out from the while loop? You only expect the exception to
fire one time, at the end of the list. You can also eliminate any
localization of variables for calls that are not called in the loop,
such as self_pool (which does not seem to be used at all), and
self_call_exit_funcs.

-- Paul

def loop(self):


self_pool_popleft = self.pool.popleft
self_pool_append = self.pool.append
check = self.pool.__len__

try:


while check() > 0:
task = self_pool_popleft()

task.next()
self_pool_append(task)
except StopIteration:
self.call_exit_funcs(task)
return

Paul Rubin

unread,
Sep 5, 2005, 12:06:09 AM9/5/05
to
simonw...@gmail.com writes:
> However, it is very ugly. Does anyone have any tips on how I could get
> this optimisation to occor magically, via a decorator perhaps?

Have you tried psyco?

simonw...@gmail.com

unread,
Sep 5, 2005, 12:10:23 AM9/5/05
to
I guess it is hard to see what the code is doing without a complete
example.

The StopIteration is actually raised by task.next(), at which point
task is removed from the list of generators (self.pool). So the
StopIteration can be raised at any time.

The specific optimisation I am after, which will clean up the code a
lot, is a way to auto-magically create self_attribute local variables
from self.attribute instance variables.

Sw.

simonw...@gmail.com

unread,
Sep 5, 2005, 12:12:24 AM9/5/05
to
Yes. It slows down the loop when there are only a few iterators in the
pool, and speeds it up when there are > 2000.

My use case involves < 1000 iterators, so psyco is not much help. It
doesn't solve the magic creation of locals from instance vars either.

Sw.

Delaney, Timothy (Tim)

unread,
Sep 5, 2005, 12:13:03 AM9/5/05
to pytho...@python.org
simonw...@gmail.com wrote:

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/277940

For an even bigger improvement (in general), look at psyco:
http://psyco.sourceforge.net/.

Tim Delaney

Paul Rubin

unread,
Sep 5, 2005, 12:43:50 AM9/5/05
to
simonw...@gmail.com writes:
> My use case involves < 1000 iterators, so psyco is not much help. It
> doesn't solve the magic creation of locals from instance vars either.

How about using __slots__ to put those instance vars at fixed offsets
in the pool object (self then needs to be a new-style class instance).
That might or might not avoid the dict lookups.

simonw...@gmail.com

unread,
Sep 5, 2005, 1:31:52 AM9/5/05
to
> def loop(self):
> self_pool = self.pool
> self_call_exit_funcs = self.call_exit_funcs
> self_pool_popleft = self.pool.popleft
> self_pool_append = self.pool.append
> check = self.pool.__len__
> while check() > 0:
> task = self_pool_popleft()
> try:
> task.next()
> except StopIteration:
> self_call_exit_funcs(task)
> return
> self_pool_append(task)

Stupid me. the 'return' statement above should be 'continue'. Sorry for
the confusion.

simonw...@gmail.com

unread,
Sep 5, 2005, 1:40:46 AM9/5/05
to
Psyco actually slowed down the code dramatically.
I've fixed up the code (replaced the erroneous return statement) and
uploaded the code for people to examine:

The test code is here: http://metaplay.dyndns.org:82/~xerian/fibres.txt

These are the run times (in seconds) of the test file.

without psyco:

0.00294482
0.03261255
0.06714886
0.87395510

with psyco.full():

0.00446651
0.05012258
0.15308657
11.23493663

Tomasz Lisowski

unread,
Sep 5, 2005, 4:20:13 AM9/5/05
to
simonw...@gmail.com napisał(a):

Then you can avoid continue by writing:

while check() > 0:
task = self_pool_popleft()
try:
task.next()
except StopIteration:
self_call_exit_funcs(task)

else:
self_pool_append(task)

Tomasz Lisowski

Paul McGuire

unread,
Sep 5, 2005, 10:27:41 AM9/5/05
to
I still think there are savings to be had by looping inside the
try-except block, which avoids many setup/teardown exception handling
steps. This is not so pretty in another way (repeated while on
check()), but I would be interested in your timings w.r.t. your current
code.

def loop(self):
self_pool_popleft = self.pool.popleft
self_pool_append = self.pool.append

self_call_exit_funcs = self.call_exit_funcs


check = self.pool.__len__
while check() > 0:

try:
while check() > 0:
task = self_pool_popleft()
task.next()
self_pool_append(task)
except StopIteration:

self_call_exit_funcs(task)

-- Paul

Paul McGuire

unread,
Sep 5, 2005, 10:33:38 AM9/5/05
to
Raymond Hettinger posted this recipe to the Python Cookbook. I've not
tried it myself, but it sounds like what you're looking for.

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/277940

-- Paul

Bengt Richter

unread,
Sep 5, 2005, 5:39:31 PM9/5/05
to

Why not let popleft trigger an exception out of while True instead,
and prevent tasks from raising StopIteration, and let them yield a None
to indicate keep scheduling with no special action, and something else
for optional differentiation of various exit options, e.g., zero for die,
and nonzero for suspension waiting for event(s) E.g., a returned integer
could be an event mask or single index (+ vs -) for thing(s) to wait for.
If you work things right, event check in the loop can be an if like
if waitedfor&events: process_events(), which most of the time is a fast no-op).

Then (without event stuff, and untested ;-) maybe something like:

def loop(self):
self_pool_popleft = self.pool.popleft
self_pool_append = self.pool.append
self_call_exit_funcs = self.call_exit_funcs

try:
while True:
task = self_pool_popleft()
if task.next() is None:
self_call_exit_funcs(task)
else:
self_pool_append(task)
except Indexerror:
pass

You could even consider putting the bound task.next methods in
the deque instead of the task, and using deque rotation instead
of popping and appending. Then, if you put the task.next's in
reverse order in the deque to start with, self_pool[-1] will be
the first, and self_pool_rotate() will bring the next into position.
Which would make it look like (untested!):

def loop(self):
self_pool_pop = self.pool.pop
self_call_exit_funcs = self.call_exit_funcs
self_pool_rotate = self.pool.rotate
try:
while True:
if self.pool[-1]() is None:
self_call_exit_funcs(self_pool_pop())
else:
self_pool_rotate()
except Indexerror:
pass

IWT if the pool remains unchanged most of the time, pool_rotate() ought
to be faster than popping and appending. Note that exit_funcs will need
a mapping of task.next -> task most likely, unless communication is
entirely via a mutable task state object, and all that's needed is to
me map to that and mess with exit state there and trigger a final .next()
to wrap up the generator.

Regards,
Bengt Richter

Bengt Richter

unread,
Sep 5, 2005, 9:38:44 PM9/5/05
to

^^^^^^^^^^^^^^^^^^^^^^ oops, need to switch if and else branches
> else:
> self_pool_append(task)
^^^^^^^^^^^^^^^^^^^^^^ oops, need to switch if and else branches


> except Indexerror:
> pass
>
>You could even consider putting the bound task.next methods in
>the deque instead of the task, and using deque rotation instead
>of popping and appending. Then, if you put the task.next's in
>reverse order in the deque to start with, self_pool[-1] will be
>the first, and self_pool_rotate() will bring the next into position.
>Which would make it look like (untested!):
>
> def loop(self):
> self_pool_pop = self.pool.pop
> self_call_exit_funcs = self.call_exit_funcs
> self_pool_rotate = self.pool.rotate
> try:
> while True:
> if self.pool[-1]() is None:
> self_call_exit_funcs(self_pool_pop())

^^^^^^^^^^^^^^^^^^^^^^ oops, need to switch if and else branches
> else:
> self_pool_rotate()
^^^^^^^^^^^^^^^^^^^^^^ oops, need to switch if and else branches


> except Indexerror:
> pass
>
>IWT if the pool remains unchanged most of the time, pool_rotate() ought
>to be faster than popping and appending. Note that exit_funcs will need
>a mapping of task.next -> task most likely, unless communication is
>entirely via a mutable task state object, and all that's needed is to
>me map to that and mess with exit state there and trigger a final .next()
>to wrap up the generator.
>

Sorry. I wonder what else I goofed up ;-)

Regards,
Bengt Richter

simonw...@gmail.com

unread,
Sep 5, 2005, 11:37:02 PM9/5/05
to

Paul McGuire wrote:
> I still think there are savings to be had by looping inside the
> try-except block, which avoids many setup/teardown exception handling
> steps. This is not so pretty in another way (repeated while on
> check()), but I would be interested in your timings w.r.t. your current
> code.

Your suggested optimisation worked nicely. It shaved 0.02 seconds from
a loop over 10000 iterators, and about 0.002 seconds from a loop over
1000 iterators.

ABO

unread,
Sep 14, 2005, 7:18:44 AM9/14/05
to
Bengt Richter wrote:
> On 5 Sep 2005 07:27:41 -0700, "Paul McGuire" <pt...@austin.rr.com> wrote:
>
> >I still think there are savings to be had by looping inside the
> >try-except block, which avoids many setup/teardown exception handling
> >steps. This is not so pretty in another way (repeated while on
> >check()), but I would be interested in your timings w.r.t. your current
[...]

> Why not let popleft trigger an exception out of while True instead,
> and prevent tasks from raising StopIteration, and let them yield a None
> to indicate keep scheduling with no special action, and something else
[...]

The rule of thumb with exceptions is use them for infrequent events. If
you keep to this you end up with clean and fast code.

Provided task.next() raises StopIteration less than about 25% of the
time it is called, it is cleaner and more efficient to use an exception
than to return None. It is also more efficient to handle terminating by
allowing popleft trigger an exception. Try the following;

def loop(self):
self_call_exit_funcs = self.call_exit_funcs


self_pool_popleft = self.pool.popleft
self_pool_append = self.pool.append

while True:
try:


task = self_pool_popleft()
task.next()
self_pool_append(task)
except StopIteration:
self_call_exit_funcs(task)

except IndexError:
break

There are other "optimisations" that could be applied that make this
code faster but uglier. For example, putting another "while True: loop
inside the try block to avoid the try block setup each iteration. Also,
exception handling is slower when you specify the exception class (it
has to check if the exception matches), so you might be able to arrange
this with an anonymous accept: block around the task.next() to handle
the StopIteration exception.

Another thing that disturbs me is the popfirst/append every iteration.
Depending on how many loops through all the tasks before one
"finishes", you might find it more efficient to do this;

def loop(self):
self_pool = self.pool

self_pool_remove = self_pool.remove
self_call_exit_funcs = self.call_exit_funcs
while self_pool:
try:
for task in self_pool:
task.next()
except:
self_pool_remove(task)
self_call_exit_funcs(task)

Terry Reedy

unread,
Sep 14, 2005, 10:16:40 AM9/14/05
to pytho...@python.org

"ABO" <a...@google.com> wrote in message
news:1126696724.7...@g47g2000cwa.googlegroups.com...

> There are other "optimisations" that could be applied that make this
> code faster but uglier. For example, putting another "while True: loop
> inside the try block to avoid the try block setup each iteration.

In CPython, 'setting up' try-blocks is (intentionally) very fast, perhaps
as fast or faster than one interation loop.

tjr

Paul McGuire

unread,
Sep 14, 2005, 10:34:26 AM9/14/05
to
Terry -

If setting up a try-block is as fast (or "takes as long") as one
iteration loop, then wont putting a try-block inside a loop double the
execution time?

-- Paul

Terry Reedy

unread,
Sep 14, 2005, 6:52:19 PM9/14/05
to pytho...@python.org

"Paul McGuire" <pt...@austin.rr.com> wrote in message
news:1126708466....@g43g2000cwa.googlegroups.com...

It will double (more or less) the loop overhead. The effect of that
depends on the overhead/payload ratio. And, I was responding to the idea
that adding a loop to avoid most try statements would be significantly
faster.

Terry J. Reedy

0 new messages