Skip to content

Mem Usage and Increment odd behaviour #236

Open
@ianozsvald

Description

@ianozsvald

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions