Conversation
|
Hi @tjstum, You are right that Python types that could be part of reference cycles should have a However, I don't see how nanobind could completely automate this aspect (besides special cases like instance dictionaries that you already noticed). How should it know what fields/data structure members to traverse in complex user-defined types? Best, |
|
Hi Wenzel, Thanks for writing back. I agree that there's probably not much that nanobind can do in the general case to completely automate this. I was thinking that it might be a good default, though, for nanobind to create type objects that use the Like I said initially, there might be something that I'm missing, so I'm interested in your thoughts. This seems to me to be animprovement for the majority of nanobind classes, though! |
|
I should also mention that I'm happy to update this pull request to show my proposed change, in case it's not clear what I am suggesting |
|
Aha, got it! Yes, I think that makes sense. I think that we would need two separate versions of |
|
Thanks! I just updated my branch with the proposed code change. I am happy to create a new PR or edit commits or whatever you'd like. |
ec3373c to
b5ed696
Compare
|
Hi @tjstum, I thought a bit more about this, and I am not sure that this PR is the right way to go for nanobind. First of all, it seems that this is really a Python issue and not a nanobind issue. Unless I misunderstood, you should be able to create the same reference cycle with any other non-GCable Python object. It's just that nanobind is really noisy about leaks and will complain in this case, while Python leaks silently. So if anything, I feel like the onus is on Python to implement some special handling of cycles involving instances and their class object. Second, Integrating a type with the GC comes with certain costs: the storage per object goes up by at least 16 bytes to store indirections in a bidirectional linked list. Which is fine a big and heavy class, but it's costly for simple types that themselves might only consume a few bytes. I hope the reasoning makes sense -- let me know if I misunderstood something about your issue. |
|
Thanks for the thoughtful consideration, @wjakob. It has always occurred to me that this situation is quite odd... The instances themselves are not containers and therefore it seems strange to require every nanobind type include As to this change... I am curious as to your thinking about the use of I think the reason that this doesn't come up in other non-GCable Python types is that they don't support setting arbitrary attributes on the type object: >>> str.foo = "foo"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot set 'foo' attribute of immutable type 'str'
>>> int.five = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot set 'five' attribute of immutable type 'int'Perhaps nanobind could do something similar; perhaps nanobind classes should either have the I can understand if you want to leave this up to the authors of binding code and not have nanobind automatically choose. Please let me know what you think about my suggested compromise above. If you are still uninterested, I will totally understand ... and in that case, I will send a PR to update the docs with the result of this discussion. |
|
I've done this a few times last year, and here is how you make an actionable issue:
You could tag The point about immutable type objects is interesting! That may indeed be a possible direction. I would still go the route of reporting to CPython to see what they say. |
|
By the way, setting an immutable flag on the type would not work: nanobind uses Basically I think the conclusion (before having heard from the CPython folks is):
Coming back to a question you asked earlier: isn't the cost also prohibitive for |
|
I will close this PR for now since it is unlikely that it would be merged, but I am happy to continue the discussion here. |
|
Thanks! I'll put together a reproducer for CPython and see what they say. I did see that in the original bpo issue, there was quite a bit of resistance to special casing this situation (at least within the gc module). Your point about the limitation of using |
This wouldn't be usable in practice. Even a trivial
This sounds more reasonable. We could have a |
Right! Of course. My mistake.
I'll look into that! The attribute sounds like a great compromise. |
Hello again! I wasn't totally sure what the best area of the repo was the best to bring this up in. Please forgive me if I picked incorrectly
While cleaning up some leak warnings in our extension module, I was looking into how
nanobind::enum_handles the cycles that are created between the type -> enumeration members that are instances, stored as attributes on the type -> the type. Our extension module has a few cases of this pattern: we have our own version of enums (with all the instances being stored as attributes of the type). We also sometimes expose "special" instances of a non-enumeration class as an attribute on the type object itself. That could be done in Python (as my example code shows here) or in C++.I think the original issue is in bpo-40217. In 3.8, it looks like CPython tried to handle this automatically (by adding a
tp_traverseslot; python/cpython#19414). However, it was reverted (I think? python/cpython#20264), and it seems that it's now incumbent on heap types to define atp_traversethat callsPy_VISITon the type object. This is exactly whatnanobind::enum_does, andnanobind::class_will do it if the objects have an instance dict. It seems, though, that nanobind should do this for all created types. Otherwise they leak at shutdown.There's almost certainly some nuance from this issue that I'm missing, so I figured I'd bring this up for discussion (showing the issue) before proposing any specific fix. Thanks for your consideration.