Memory use of pickling/unpickling enum in Python
Asked Answered
D

0

7

While investigating the memory use of an application that exchanges message through PyZMQ using tracemalloc, I noticed a weird behavior of pickling and unpickling enums: memory looks like it is leaking.

This seems impossible to me so I am guessing I am measuring something in an incorrect manner. Could someone let me know if this a real leak or, if it is not, how I should be measuring memory usage to avoid seeing such behavior ?

I reduced my test to:

import enum
import gc
import os
import pickle
import tracemalloc
from uuid import SafeUUID


class A:
    pass


@enum.unique
class E(enum.Enum):
    A = 1


obj = A()
tracemalloc.start()
for obj in (A(), E.A, SafeUUID.safe):
    print(obj)
    for j in range(10):
        tracemalloc.clear_traces()
        i = 0
        while True:
            s = pickle.dumps(obj)
            o = pickle.loads(s)

            del s, o

            if i == 100:
                gc.collect()
                origin = tracemalloc.take_snapshot().filter_traces(
                    [tracemalloc.Filter(True, __file__)]
                )
            elif i > 200:
                gc.collect()
                snap = tracemalloc.take_snapshot().filter_traces(
                    [tracemalloc.Filter(True, __file__)]
                )
                print(str(snap.compare_to(origin, "lineno")[0]).rsplit(os.sep, 1)[-1])
                break
            i += 1
    print()

The exact output is noisy but I can get things similar to the following, in which pickling a class instance displays no variation, pickling a local enum see some memory increase, and pickling an enum defined in a different module can result in large memory swings.

<__main__.A object at 0x000002840A56B150>
enum-pickle-leak.py:27: size=1520 B (+1520 B), count=1 (+1), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B
enum-pickle-leak.py:27: size=1520 B (+0 B), count=1 (+0), average=1520 B

E.A
enum-pickle-leak.py:26: size=3640 B (+112 B), count=65 (+2), average=56 B
enum-pickle-leak.py:27: size=4040 B (+168 B), count=46 (+3), average=88 B
enum-pickle-leak.py:27: size=7288 B (+224 B), count=104 (+4), average=70 B
enum-pickle-leak.py:27: size=2528 B (+168 B), count=19 (+3), average=133 B
enum-pickle-leak.py:26: size=392 B (+112 B), count=7 (+2), average=56 B
enum-pickle-leak.py:26: size=168 B (+112 B), count=3 (+2), average=56 B
enum-pickle-leak.py:27: size=4600 B (+112 B), count=56 (+2), average=82 B
enum-pickle-leak.py:27: size=1800 B (+168 B), count=6 (+3), average=300 B
enum-pickle-leak.py:27: size=1744 B (+112 B), count=5 (+2), average=349 B
enum-pickle-leak.py:27: size=3312 B (+112 B), count=33 (+2), average=100 B

SafeUUID.safe
enum-pickle-leak.py:27: size=16.6 KiB (+10.3 KiB), count=283 (+193), average=60 B
enum-pickle-leak.py:27: size=12.9 KiB (+1098 B), count=214 (+20), average=62 B
enum-pickle-leak.py:27: size=12.3 KiB (+494 B), count=202 (+9), average=62 B
enum-pickle-leak.py:27: size=5539 B (+440 B), count=74 (+8), average=75 B
enum-pickle-leak.py:27: size=2678 B (+659 B), count=22 (+12), average=122 B
enum-pickle-leak.py:27: size=3343 B (+887 B), count=34 (+16), average=98 B
enum-pickle-leak.py:27: size=3377 B (+599 B), count=35 (+11), average=96 B
enum-pickle-leak.py:27: size=9367 B (+551 B), count=144 (+10), average=65 B
enum-pickle-leak.py:27: size=2834 B (+434 B), count=25 (+8), average=113 B
enum-pickle-leak.py:27: size=3005 B (+771 B), count=28 (+14), average=107 B
Diffusion answered 28/6, 2024 at 12:40 Comment(7)
FWIW, I get somewhat different output: pastebin.com/aHsftPES. This is on Python 3.10.4 on iOS. There is no variation in either A or E.A, and only smaller swings for SafeUUID. You may want to add some details of your environment in order to assist with reproducing the issue.Deuterium
@Deuterium I can confirm that I see similar output as shown in your paste (3.12.4)Lectionary
Those results were obtained on Windows using Python 3.11.3Diffusion
On Macbook Pro M3, on py3.9 I see stable sizes on all test cases. On 3.11.9 and 3.12.2, I see stable usage on A() and E.A, but SafeUUID.safe has some variation. I think the observation that it's (+0 B) on 3.9 but shows increases on other later versions means you may be onto something here.Sulphurate
python 3.12.3, all stable except SafeUUID.safe, removing the calls to gc.collect() fixes the issue with SafeUUID.safe.Ss
I find puzzling that removing gc collection helps stabilize memory use for the better.Diffusion
There is something going on with gc.collect(), directly implementing SafeUUID still has the same issue, but changing the class name to a single character fixes it?!Ss

© 2022 - 2025 — McMap. All rights reserved.