Skip to content

Fix object lifetime management #421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
andlaus opened this issue Apr 29, 2025 · 9 comments
Open

Fix object lifetime management #421

andlaus opened this issue Apr 29, 2025 · 9 comments

Comments

@andlaus
Copy link
Member

andlaus commented Apr 29, 2025

The odxtools database object currently seem to mess up python's garbage collector: when running

import odxtools
import time

print("loading database", flush=True)
db = odxtools.load_file("my_big_pdx_file.pdx")
print("database loaded", flush=True)
time.sleep(20)
print("deleting database", flush=True)
del db
print("database deleted", flush=True)
time.sleep(20)
print("done", flush=True)

the memory used by the db object is not freed after the "database deleted" string is printed but only at program exit, i.e., after "done" appears.

I suspect that this is due to python's garbage collector getting confused by cyclic references, but unfortunately I have not found a good way to debug this, and my limited experiments with weak references from python's weakref module also did not fix the issue. @kayoub5, @nada-ben-ali, @zariiii9003: any ideas how to get to the bottom of this?

@andlaus
Copy link
Member Author

andlaus commented Apr 29, 2025

hm: it turns out that calling gc.collect() after deleting the database object in the script above fixes the issue. Since this is quite slow and IMO pretty unpythonic, I suppose that this should somehow be done in a better way?

@kayoub5
Copy link
Collaborator

kayoub5 commented Apr 29, 2025

Those are common mis-conception about garbage collectors:

  • Garbage collocation does not run when you are done with the object, quite the opposite, most GCs (I have seen) run when the program ask to allocate more memory, more on why below.
  • Some GCs have "scope exit" logic, basically GC auto run on local objects of functions for example, since are those are easier to detect and clean, your code have global module variables, GC typically don't aggressively run until module logic is over.
  • GCs usually require putting the entire application/thread in a pause state in order to run, so typical imlimentations avoid interrupting your code as much as possible (usually wait for a sleep/ io operation), unless RAM is very limited.
  • A process does not return the memory it longer needs to the os, the reason is that "it may need it again soon", and requesting more RAM from os is slow (in some cases, running a GC is faster than asking os for RAM). so process just mark RAM as "os free to take it back if it really needs to, but I prefer to keep it if possible".

@kayoub5
Copy link
Collaborator

kayoub5 commented Apr 29, 2025

Also GC is robust enough to handle circular references.

The main two root causes of memory leaks, are global objects (examples: a global cache, module variables, etc), and lambda functions, unlike typical objects, where leaks are visible in the object graph through attributes, lambda functions keep references to objects simply because the lambda code uses them, which makes them hard to notice.

@andlaus
Copy link
Member Author

andlaus commented Apr 30, 2025

Isn't python's main memory reference counting? I'm not sure how exactly it is implemented in cpython, but the other refcounting implementation I have experience with (C++'s smart pointers) frees memory of objects which's reference count reaches zero immediately. (This is also a very fast operation...)

lambda functions keep references to objects simply because the lambda code uses them, which makes them hard to notice.

sure, but as soon as the refcount of the lambda function reaches zero, it should be deleted...

@kayoub5
Copy link
Collaborator

kayoub5 commented Apr 30, 2025

A memory freed does not mean the process memory goes down immediately, a process always keep the memory pages for fast re-allocation.

@andlaus
Copy link
Member Author

andlaus commented Apr 30, 2025

A memory freed does not mean the process memory goes down immediately, a process always keep the memory pages for fast re-allocation.

sure, but I assume that the heap allocators will free memory eventually (in my case "eventually" means after deleting a > 5 GB Database object.)

@andlaus
Copy link
Member Author

andlaus commented Apr 30, 2025

(I strongly suspect that there are cycles in the reference graph of Database objects, which break python's reference counting mechanism. My point with this issue is that it would be nice if these cycles could be broken so that we don't need to use the full garbage collector anymore, but I have yet not found a good way of debugging this issue...)

@kayoub5
Copy link
Collaborator

kayoub5 commented Apr 30, 2025

If it has no reason to (OS didn't ask for the RAM back, due to pressure from other processes), it has no reason to give it up.

@andlaus
Copy link
Member Author

andlaus commented Apr 30, 2025

I would be surprised if there was a formal "out of band" mechanism of the Linux kernel to signal to userspace that there is memory pressure...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants