[Python-Dev] accumulator display syntax

Guido van Rossum guido at python.org
Tue Oct 21 00:09:11 EDT 2003


> with a.py having:
> def asum(R):
> sum([ x*x for x in R ])
>> def gen(R):
> for x in R: yield x*x
> def gsum(R, gen=gen):
> sum(gen(R))
>> I measure:
>> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(100)' 'a.asum(R)'
> 10000 loops, best of 3: 96 usec per loop
> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(100)' 'a.gsum(R)'
> 10000 loops, best of 3: 60 usec per loop
> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(1000)' 'a.asum(R)'
> 1000 loops, best of 3: 930 usec per loop
> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(1000)' 'a.gsum(R)'
> 1000 loops, best of 3: 590 usec per loop
> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(10000)' 'a.asum(R)'
> 100 loops, best of 3: 1.28e+04 usec per loop
> [alex at lancelot auto]$ timeit.py -c -s'import a' -s'R=range(10000)' 'a.gsum(R)'
> 100 loops, best of 3: 8.4e+03 usec per loop
>> not sure why gsum's advantage ratio over asum seems to be roughly
> constant, but, this IS what I measure!-)

Great! This is a plus for iterator comprehensions (we need a better
term BTW). I guess that building up a list using repeated append()
calls slows things down more than the frame switching used by
generator functions; I knew the latter was fast but this is a pleasant
result.
BTW, if I use a different function that calculates list() instead of
sum(), the generator version is a few percent slower than the list
comprehension. But that's because list(a) has a shortcut in case a is
a list, while sum(a) always uses PyIter_Next(). So this is actually
consistent: despite the huge win of the shortcut, the generator
version is barely slower.
I think the answer lies in the bytecode:
>>> def lc(a):
 return [x for x in a]
 
>>> import dis
>>> dis.dis(lc)
 2 0 BUILD_LIST 0
 3 DUP_TOP 
 4 LOAD_ATTR 0 (append)
 7 STORE_FAST 1 (_[1])
 10 LOAD_FAST 0 (a)
 13 GET_ITER 
 >> 14 FOR_ITER 16 (to 33)
 17 STORE_FAST 2 (x)
 20 LOAD_FAST 1 (_[1])
 23 LOAD_FAST 2 (x)
 26 CALL_FUNCTION 1
 29 POP_TOP 
 30 JUMP_ABSOLUTE 14
 >> 33 DELETE_FAST 1 (_[1])
 36 RETURN_VALUE 
 37 LOAD_CONST 0 (None)
 40 RETURN_VALUE 
>>> def gen(a):
 for x in a: yield x
 
>>> dis.dis(gen)
 2 0 SETUP_LOOP 18 (to 21)
 3 LOAD_FAST 0 (a)
 6 GET_ITER 
 >> 7 FOR_ITER 10 (to 20)
 10 STORE_FAST 1 (x)
 13 LOAD_FAST 1 (x)
 16 YIELD_VALUE 
 17 JUMP_ABSOLUTE 7
 >> 20 POP_BLOCK 
 >> 21 LOAD_CONST 0 (None)
 24 RETURN_VALUE 
>>>
The list comprehension executes 7 bytecodes per iteration; the
generator version only 5 (this could be more of course if the
expression was more complicated than 'x'). The YIELD_VALUE does very
little work; falling out of the frame is like falling off a log; and
gen_iternext() is pretty sparse code too. On the list comprehension
side, calling the list's append method has a bunch of overhead. (Some
of which could be avoided if we had a special-purpose opcode which
called PyList_Append().)
But the executive summary remains: the generator wins because it
doesn't have to materialize the whole list.
--Guido van Rossum (home page: http://www.python.org/~guido/)


More information about the Python-Dev mailing list

AltStyle によって変換されたページ (->オリジナル) /