5

At first, I want to test the memory usage between generator and list-comprehension.The book give me a little bench code snippet and I run it on my PC(python3.6, Windows),find something unexpected.

  1. On the book, it said, because list-comprehension has to create a real list and allocate memory for it, itering from a list-comprehension must be slower than itering from a generator.
  2. Ofcourse, list-comprehension use more memory than generator.

FOllowing is my code,which is not satisfy previous opinion(within sum function).

import tracemalloc
from time import time


def timeIt(func):
    start = time()
    func()
    print('%s use time' % func.__name__, time() - start)
    return func


tracemalloc.start()

numbers = range(1, 1000000)


@timeIt
def lStyle():
    return sum([i for i in numbers if i % 3 == 0])


@timeIt
def gStyle():
    return sum((i for i in numbers if i % 3 == 0))


lStyle()

gStyle()

shouldSize = [i for i in numbers if i % 3 == 0]

snapshotL = tracemalloc.take_snapshot()
top_stats = snapshotL.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
    print(stat)

The output:

lStyle use time 0.4460000991821289
gStyle use time 0.6190001964569092
[ Top 10 ]
F:/py3proj/play.py:31: size=11.5 MiB, count=333250, average=36 B
F:/py3proj/play.py:33: size=448 B, count=1, average=448 B
F:/py3proj/play.py:22: size=136 B, count=1, average=136 B
F:/py3proj/play.py:17: size=136 B, count=1, average=136 B
F:/py3proj/play.py:14: size=76 B, count=2, average=38 B
F:/py3proj/play.py:8: size=34 B, count=1, average=34 B

Two points:

  • Generator use more time and same memory space.
  • The list-comprehension in sum function seems not create the total list

I think maybe the sum function did something i don't know.Who can explain this?

The book is High Perfoamance Python.chapter 5.But i did sth myself different from the book to check the validity in other context. And his code is here book_code,he didn't put the list-comprehension in sum funciton.

jpp
  • 159,742
  • 34
  • 281
  • 339
dogewang
  • 648
  • 1
  • 7
  • 15
  • you are testing a tuple against a list, there is no generator – Evgeny Jun 01 '18 at 07:16
  • 4
    @EvgenyPogrebnyak there's no such thing as a tuple comprehension, a generator expression in parentheses is a generator expression. See e.g. https://stackoverflow.com/q/16940293/3001761. – jonrsharpe Jun 01 '18 at 07:17
  • my wrong! `type((x for x in [1,2,3]))` is indeed a generator – Evgeny Jun 01 '18 at 07:30
  • I think you should say which book is telling you this. – BoarGules Jun 01 '18 at 07:49
  • Ok,i supplement it @BoarGules – dogewang Jun 01 '18 at 07:57
  • Is this specific to `sum`, or to list-comp vs. generator in general? The same seems to apply when substituting`sum` for `tuple` or `max`. – tobias_k Jun 01 '18 at 08:58
  • I also tried `max` and `tuple`, `max` behave same as `sum`.But `tuple` is different from `sum`.There is no **iter** operation in `tuple`.I think the built-in function involve **iter** operation is different.@tobias_k – dogewang Jun 01 '18 at 09:42

1 Answers1

2

When it comes to time performance test, I do rely on the timeit module because it automatically executes multiple runs of the code.

On my system, timeit gives following results (I strongly reduced sizes because of the numerous runs):

>>> timeit.timeit("sum([i for i in numbers if i % 3 == 0])", "numbers = range(1, 1000)")
59.54427594248068
>>> timeit.timeit("sum((i for i in numbers if i % 3 == 0))", "numbers = range(1, 1000)")
64.36398425334801

So the generator is slower by about 8% (*). This is not really a surprize because the generator has to execute some code on the fly to get next value, while a precomputed list only increment its current pointer.

Memory evalutation is IMHO more complex, so I have used Compute Memory footprint of an object and its contents (Python recipe) from activestate

>>> numbers = range(1, 100)
>>> numbers = range(1, 100000)
>>> l = [i for i in numbers if i % 3 == 0]
>>> g = (i for i in numbers if i % 3 == 0)
>>> total_size(l)
1218708
>>> total_size(g)
88
>>> total_size(numbers)
48

My interpretation is that a list uses memory for all of its items (which is not a surprize), while a generator only need few values and some code so the memory footprint is much lesser for the generator.

I strongly think that you have used tracemalloc for something it is not intended for. It is aimed at searching possible memory leaks (large blocs of memory never deallocated) and not at controlling the memory used by individual objects.


BEWARE: I could only test for small sizes. But for very large sizes, the list could exhaust the available memory and the machine will use virtual memory from swap. In that case, the list version will become much slower. More details there

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • First,thx for your explanation of the time problem,and I accept that.What's more,I have no objection in list-comprehension use more memory than generator.However,the most confusing thing is list-comp in a sum function seems like generator(in memory respect), it is different from "Create a list-comp and then put it in sum function". – dogewang Jun 01 '18 at 08:48
  • `This is not really a surprize because the generator has to execute some code on the fly...` I suspect this is only half the story / explanation. Try a simple `for` loop: `c = 0; for i in range(n): if i % 3 == 0: c += i` This also has to do something to get the next value, but is faster. My instinct is the implementation of `__next__` for a generator is fundamentally slow. – jpp Jun 01 '18 at 08:56
  • @jpp Making sure of what really happens under the hood would require delving in Python interpretor source code. But a generator adds some functionalities that probably come at a cost... – Serge Ballesta Jun 01 '18 at 09:07
  • @jpp this isn't really surprising. Iterating over a generator takes a lot of overhead, but once you start working with something the size of `range(1000000)` then the generator expression beats the list-comprehension. – juanpa.arrivillaga Jun 01 '18 at 09:38
  • @juanpa.arrivillaga, I believe you. But I don't think "it's not surprising" is a sufficient explanation.. It *does* surprise me, OP, and many others too! Surprise is a subjective phenomenon, not an absolute. We aren't C programmers, don't properly understand the internals, etc. – jpp Jun 01 '18 at 09:40
  • @jpp i mean, this isn't surprising in that it is fairly well known that generators are slow compared to equivalent looping constructs until the list that would be created is quite large (millions-10's millions). There are many such questions, e.g. [this one](https://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa). Just google "generator expression vs list comprehension speed". Anyone who has done serious profiling in Python will stumble upon this eventually – juanpa.arrivillaga Jun 01 '18 at 09:45
  • 1
    @juanpa.arrivillaga: I have updated my answer with your comment. – Serge Ballesta Jun 01 '18 at 09:59