Repository navigation
Mem Usage and Increment odd behaviour #236
Description
Hi @fabianp , it has been ages, I hope you're well. I'm seeing a bug related to #234.
I'm updating my High Performance Python book for the 2nd edition. Compared to the first edition's report of memory_profiler's behaviour, the Increment is definitely looking odd.
I'm using a fresh install of memory_profiler, psutil and Python 3.7 in Anaconda on Linux.
I have two functions, I've profiled them each separately and once together. They have similar (but not identical) outputs, suggesting that the comment by @soundgnome in #234 about nested calls may not be so clear cut.
I'll lay out the 3 scenarios in case you have thoughts. This is a Julia set demo from my Profiling chapter. An outer function prepares x & y arrays of complex coordinates which consumes RAM, an inner function then iterates over these, calculating the Julia set in a cpu-bound scenario.
Scenario 1 - just profiling the inner function calculate_z_serial_purepython:
$ python -m memory_profiler julia1_memoryprofiler.py
Length of x: 1000
Total elements: 1000000
calculate_z_serial_purepython took 6689.149292945862 seconds
Filename: julia1_memoryprofiler.py
Line # Mem usage Increment Line Contents
================================================
9 126.363 MiB 126.363 MiB @profile
10 def calculate_z_serial_purepython(maxiter, zs, cs):
11 """Calculate output list using Julia update rule"""
12 133.973 MiB 7.609 MiB output = [0] * len(zs)
13 136.988 MiB 0.000 MiB for i in range(len(zs)):
14 136.988 MiB 0.000 MiB n = 0
15 136.988 MiB 0.000 MiB z = zs[i]
16 136.988 MiB 0.000 MiB c = cs[i]
17 136.988 MiB 0.258 MiB while n < maxiter and abs(z) < 2:
18 136.988 MiB 0.000 MiB z = z * z + c
19 136.988 MiB 0.000 MiB n += 1
20 136.988 MiB 0.000 MiB output[i] = n
21 136.988 MiB 0.000 MiB return output
In the earlier edition with Python 2.7 line 12 cost +7MB, line 13 cost +32MB (due I expect to range not being a generator in Python 2.7 and so making a full list which we avoid now with Python 3.x). Line 17 costs +0.2MB, previously it cost 0MB (but this might have been masked by the process' overall growth due to the range statement?).
Scenario 2 - profiling the outer function calc_pure_python:
$ python -m memory_profiler julia1_memoryprofiler.py
Length of x: 1000
Total elements: 1000000
calculate_z_serial_purepython took 94.99498796463013 seconds
Filename: julia1_memoryprofiler.py
Line # Mem usage Increment Line Contents
================================================
24 48.113 MiB 48.113 MiB @profile
25 def calc_pure_python(draw_output, desired_width, max_iterations):
26 """Create a list of complex co-ordinates (zs) and complex parameters (cs), build Julia set and display"""
27 48.113 MiB 0.000 MiB x_step = (x2 - x1) / desired_width
28 48.113 MiB 0.000 MiB y_step = (y1 - y2) / desired_width
29 48.113 MiB 0.000 MiB x = []
30 48.113 MiB 0.000 MiB y = []
31 48.113 MiB 0.000 MiB ycoord = y2
32 48.113 MiB 0.000 MiB while ycoord > y1:
33 48.113 MiB 0.000 MiB y.append(ycoord)
34 48.113 MiB 0.000 MiB ycoord += y_step
35 48.113 MiB 0.000 MiB xcoord = x1
36 48.113 MiB 0.000 MiB while xcoord < x2:
37 48.113 MiB 0.000 MiB x.append(xcoord)
38 48.113 MiB 0.000 MiB xcoord += x_step
39 # set width and height to the generated pixel counts, rather than the
40 # pre-rounding desired width and height
41 # build a list of co-ordinates and the initial condition for each cell.
42 # Note that our initial condition is a constant and could easily be removed,
43 # we use it to simulate a real-world scenario with several inputs to our function
44 48.113 MiB 0.000 MiB zs = []
45 48.113 MiB 0.000 MiB cs = []
46 125.961 MiB 0.000 MiB for ycoord in y:
47 125.961 MiB 0.258 MiB for xcoord in x:
48 125.961 MiB 0.512 MiB zs.append(complex(xcoord, ycoord))
49 125.961 MiB 0.512 MiB cs.append(complex(c_real, c_imag))
50
51 125.961 MiB 0.000 MiB print("Length of x:", len(x))
52 125.961 MiB 0.000 MiB print("Total elements:", len(zs))
53 125.961 MiB 0.000 MiB start_time = time.time()
54 136.609 MiB 10.648 MiB output = calculate_z_serial_purepython(max_iterations, zs, cs)
55 136.609 MiB 0.000 MiB end_time = time.time()
56 136.609 MiB 0.000 MiB secs = end_time - start_time
57 136.609 MiB 0.000 MiB print(calculate_z_serial_purepython.__name__ + " took", secs, "seconds")
58
59 136.609 MiB 0.000 MiB assert sum(output) == 33219980 # this sum is expected for 1000^2 grid with 300 iterations
In the earlier edition with Python 2.7 line 46 above had cost +79MB, here the Increment is 0MB and the few following lines cost +0.2+0.5+0.5==+1.2MB. The Mem Usage at line 46 suggests a +77MB change.
The Generation of output in line 54 here costs +10MB, before it cost +32MB.
Scenario 3 - Combined profiling of both functions:
calculate_z_serial_purepython took 6658.487464427948 seconds
Filename: julia1_memoryprofiler.py
Line # Mem usage Increment Line Contents
================================================
9 126.168 MiB 126.168 MiB @profile
10 def calculate_z_serial_purepython(maxiter, zs, cs):
11 """Calculate output list using Julia update rule"""
12 133.625 MiB 7.457 MiB output = [0] * len(zs)
13 136.816 MiB 0.000 MiB for i in range(len(zs)):
14 136.816 MiB 0.000 MiB n = 0
15 136.816 MiB 0.000 MiB z = zs[i]
16 136.816 MiB 0.000 MiB c = cs[i]
17 136.816 MiB 0.258 MiB while n < maxiter and abs(z) < 2:
18 136.816 MiB 0.000 MiB z = z * z + c
19 136.816 MiB 0.000 MiB n += 1
20 136.816 MiB 0.000 MiB output[i] = n
21 136.816 MiB 0.000 MiB return output
Filename: julia1_memoryprofiler.py
Line # Mem usage Increment Line Contents
================================================
24 48.273 MiB 48.273 MiB @profile
25 def calc_pure_python(draw_output, desired_width, max_iterations):
26 """Create a list of complex co-ordinates (zs) and complex parameters (cs), build Julia set and display"""
27 48.273 MiB 0.000 MiB x_step = (x2 - x1) / desired_width
28 48.273 MiB 0.000 MiB y_step = (y1 - y2) / desired_width
29 48.273 MiB 0.000 MiB x = []
30 48.273 MiB 0.000 MiB y = []
31 48.273 MiB 0.000 MiB ycoord = y2
32 48.273 MiB 0.000 MiB while ycoord > y1:
33 48.273 MiB 0.000 MiB y.append(ycoord)
34 48.273 MiB 0.000 MiB ycoord += y_step
35 48.273 MiB 0.000 MiB xcoord = x1
36 48.273 MiB 0.000 MiB while xcoord < x2:
37 48.273 MiB 0.000 MiB x.append(xcoord)
38 48.273 MiB 0.000 MiB xcoord += x_step
39 # set width and height to the generated pixel counts, rather than the
40 # pre-rounding desired width and height
41 # build a list of co-ordinates and the initial condition for each cell.
42 # Note that our initial condition is a constant and could easily be removed,
43 # we use it to simulate a real-world scenario with several inputs to our function
44 48.273 MiB 0.000 MiB zs = []
45 48.273 MiB 0.000 MiB cs = []
46 126.168 MiB 0.000 MiB for ycoord in y:
47 126.168 MiB 0.258 MiB for xcoord in x:
48 126.168 MiB 0.512 MiB zs.append(complex(xcoord, ycoord))
49 126.168 MiB 0.512 MiB cs.append(complex(c_real, c_imag))
50
51 126.168 MiB 0.000 MiB print("Length of x:", len(x))
52 126.168 MiB 0.000 MiB print("Total elements:", len(zs))
53 126.168 MiB 0.000 MiB start_time = time.time()
54 136.816 MiB 136.816 MiB output = calculate_z_serial_purepython(max_iterations, zs, cs)
55 136.816 MiB 0.000 MiB end_time = time.time()
56 136.816 MiB 0.000 MiB secs = end_time - start_time
57 136.816 MiB 0.000 MiB print(calculate_z_serial_purepython.__name__ + " took", secs, "seconds")
58
59 136.816 MiB 0.000 MiB assert sum(output) == 33219980 # this sum is expected for 1000^2 grid with 300 iterations
In the above we see +136MB in line 54 (in Scenario 2 above it grew by +10MB), in the Python 2.7 version it grew by +32MB. The Mem Usage change at line 54 is +10MB.
In all 3 scenarios we see evidence that the Increment column is not matching to the Mem Usage column, so the Increment column is looking pretty unhelpful.
The Mem Usage column still looks correct and will have obvious value in a memory-diagnosis exercise so I can still use this in the book, but I'm wondering if we have an explanation for the odd Increment behaviour?
Has Python 3.x changed its default memory allocation behaviour for lists? I'm pretty sure there's no clever unboxing or other clever techniques (unlike in PyPy) in CPython.
Cheers (and I hope you're well!), Ian.
Activity
ianozsvald commentedon Jun 20, 2019
On the
READMEthere's an example snippet of code, the current behaviour is a little different to what's documented in theREADME:In the above lines 3 & 4 are the same but line 5 with
deldoes not track the decrease in RAM used (in theREADMEthis decrease is tracked.ianozsvald commentedon Jun 20, 2019
I'll tentatively propose that there's a problem tracking pure-Python code vs
numpycode. I've written a pair of functions which create a 10,000,000 element list or array and then create a second (the first isones, the second performs+= 1). We'd expect both the Python version and thenumpyversion to allocate the same RAM for the first and second operations.What I'm seeing is that the
numpyversion has correct tracking (that's the second example) and the Python version has the incorrectIncrementcolumn. The above was run twice, both times the behaviours were the same.Maybe in Python 3 the AST tracking code (this is a guess) needs to be updated due to changes in how Python runs?
When I ran the above with a smaller length (1,000,000 elements, 10* smaller) both the Python and
numpyversions tracked similar +8MB allocations on their respective lines.brupelo commentedon Jun 21, 2019
@ianozsvald First time I've used this package and I've noticed also an strange behaviour.
With version 0.55.0 I wouldn't get negative increments (as exposed in the pypi package on the examples) while with 0.54.0 the behaviour would match... maybe it's an introduced bug with 0.55.0?
Maybe one of the fixes for #195 is somewhat related with this?
fabianp commentedon Jun 21, 2019
Hi @ianozsvald ! Thanks a lot for reporting this.
My guess is that in the Python version, Python is silently executing the line multiple times as if it were a loop, which has always resulted in problematic reporting. Pull requests are highly appreciated :-)
ianozsvald commentedon Nov 2, 2019
Hey @fabianp . A colleague of mine @dxe4 is trying to diagnose this at a hackathon today, I'm going to post the original Python file here for him (and others if you want to try!).
Note that this runs for Python 3.7, it ought to work with Python 2.7 if you add:
dxe4 commentedon Nov 2, 2019
code:
run 1:
output 1:
run 2:
output 2:
run 3 (reversed order):
output 3:
print(sys.getsizeof(ones) / (1024 * 1024))inmyfn_pygives3.81475830078125so running
myfn_npbeforemyfn_pycausesonesinmyfn_pyto report 0.027 opposed to 3.8 but a sleep in between prevents this. and vice versa callingmyfn_pybeforemyfn_npcausesonesinmyfn_npto report 0dxe4 commentedon Nov 2, 2019
i may be wrong, i think what happens here is that even if some objects are garbage collected the OS may have not freed the memory yet, so measuring memory from an OS perspective may result to the above
brianzhang01 commentedon Sep 8, 2020
@dxe4 you may want to force
gc.collect()in your examples. Also Python's memory management has interesting allocation / free / reuse strategies which make predicting memory usage difficult. It involves blocks / pools / arenas. I think the arenas are what are allocated but the memory for arenas can only get freed if all the pools inside are unused. https://rushter.com/blog/python-memory-managment/@ianozsvald in your second comment on this thread you mentioned the example in the README not correctly showing the effects of the
delstatement. I noticed this as well, running on two separate machines (Mac OS X and Linux). I tracked the introduction of the issue to between releases 0.54 and 0.55, and after looking at the commits in that period, I suspect it's due to PR #202 . This also matches with what @brupelo observed. However, when I try to profile some of my code under 0.54, I'm getting a different set of strange results.brianzhang01 commentedon Sep 8, 2020
It looks like #280 tried to address many of these factors. You may want to rebuild and try again to see if that solves your problem.
fabianp commentedon Sep 8, 2020
RexYuan commentedon Feb 8, 2021
Just reporting that I just tried the README code on mac big sur 11.2 with pip installed memory_profiler 0.58.0 and didn't see the intended decrement too