-
-
Notifications
You must be signed in to change notification settings - Fork 18.1k
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
CoW: Use weakref callbacks to track dead references #55539
Conversation
9cc2a3e
to
e47fb58
Compare
def _weakref_cb(item: weakref.ref, selfref: weakref.ref = weakref.ref(self)) -> None: | ||
self = selfref() | ||
if self is not None: | ||
self.dead_counter += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this ever reachable without the GIL? This doesn't appear thread safe so thinking through what impacts that might have
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, can you explain a bit more? Is pandas preparing to be thread-safe and use the no-GIL feature of the coming python version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We release the GIL in quite a few places in the Cython code today, mostly for downstream libraries like Dask to take advantage of. Though I guess this in its current state wouldn't work without the GIL since it is already using weakref
on main
#55518 needs to be back ported. This is something that we can consider for main and 2.2, but I want to test this more before we rely on it. |
6eb63ce
to
c0f401d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good though will defer to @phofl who is more familiar with this. I think he is traveling this week so thanks for your patience in advance
pandas/_libs/internals.pyx
Outdated
|
||
def __cinit__(self, blk: Block | None = None) -> None: | ||
def _weakref_cb(item: weakref.ref, selfref: weakref.ref = weakref.ref(self)) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure of all of the impacts but I think cinit
is mostly used for C-level structures. I think setting this callback should instead be done in init
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just updated it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it's doesn't work because cinit is called before init, so we need the callback ready there, unless we move everything to init. I changed it back 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm OK. And this definitely doesn't cause any memory leaks or issues with reference counting then right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be correct as long as the reference list is not modified manually without the classes methods or run under concurrency.
Another thing is that taking the length of the list of refs directly won't give the correct length too (the actual value is len(referenced_blocks) - dead_counter). From what I looked, the exact length was never used anywhere else in the code right now, but I can implement the len method if needed.
If someone modified the list manually without the classes methods, the counter would be wrong and the memory consumption could grow a lot (just like the current solution at main) and the has_reference() will be plain wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant multi threading
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I understood this, but again, this is a realistic scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem would be that self.dead_counter += 1
isn't atomic. I can add a lock to it but, correct if I'm wrong, the rest of the code isn't thread-safe too. e.g. list comprehension from _clear_dead_references doesn't seem to be thread-safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a sample of how _clear_dead_references is not really thread safe:
import threading
class Test:
def __init__(self):
self.v = list(range(1000))
def add(self, x):
self.v.append(x)
def rebuild(self):
self.v = [x for x in self.v]
def simulate(self):
for _ in range(1000):
self.add(1)
self.rebuild()
def race(self):
threads = [threading.Thread(target=self.simulate) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(len(self.v))
test1 = Test()
test2 = Test()
test1.race()
test2.race()
I couldn't reproduce the same with pandas code, but I think it's more about timing than something else. The point is, is this class supposed to be thread-safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope that works, was just not sure if I was missing something obvious
583b908
to
c1b9b83
Compare
Any news? |
Personally I would be more partial to exploring #55631 (IMO I think the mental model of using a set would be simpler) |
Can you run the |
c1b9b83
to
98addad
Compare
Sorry for the delay. Here are the benchmarks:
|
How does this correspond to the other timings in the issue? |
Not sure if I fully understand what is being asked, but this ~20% increase is from the overhead in the constructor. What this PR will be better is mainly at |
Updates? |
This pull request is stale because it has been open for thirty days with no activity. Please update and respond to this comment if you're still interested in working on this. |
Co-authored-by: José Lucas Silva Mayer <[email protected]>
98addad
to
3c79e28
Compare
Hey @phofl, what is the current status of this PR? |
Hi, @phofl, can you give an update? |
Thanks for the PR but it does seem there much bandwidth or interest from the core team to pursue this approach so closing this out for now. Can reopen if there's renewed interest |
doc/source/whatsnew/vX.X.X.rst
file if fixing a bug or adding a new feature.This is an alternative solution to #55518. What it happening here:
weakref.ref
callback to count the number of dead references present at thereferenced_blocks
list.add_reference
andadd_index_reference
, but this is just moving the performance hit to these methods.has_reference
. (_clear_dead_references
is amortized to O(1))(also check another possibilities at #55008)
Let me know if I should continue working on this solution.