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.