New Native Types and Lighter GILPool#887
Conversation
eaee671 to
4ae2fec
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
This is a great change, and I would actually be in favour of merging this now! It doesn't make any real changes to the user-facing API, and it looks like a very nice optimisation.
I would think that the follow-up question to this is what do we need to do about owned pointers? As I keep seeing in #883, I think that most of the functions returning borrowed objects are subtly unsound, so I think we should be converting all functions and iterators which return borrowed objects to return owned pointers. (For those that really want to avoid the reference count increase, we could add unsafe apis back for PyDict::get_item_raw, perhaps.)
So about owned pointers, I think we have three choices:
&PyAny, like in this PR. We already know this is pretty nice, and so merging this PR now looks good.Py<T>- I'm not sure this is very nice, because every constructor the user calls they will probably want to do:let list = PyList::empty(py); # Py<PyList> let list = list.as_ref(py); # &PyListPyAny<'py>- I think this has benefits for simplicity of reference counting, but comes at a large migration cost because of the added lifetime. It's worth exploring this further in #885 , but I don't think we're ready to merge that yet.
So in summary, I suggest we merge this PR now 😄 and then we can:
- Change all APIs returning borrowed pointers to return owned pointers for soundness
- Continue to think about which design we like for owned objects - we can take our time over this to write benchmarks and play with designs more carefully.
👍 👍 👍 👍 👍
| use crate::object::PyObject; | ||
| use crate::type_object::{PyDowncastImpl, PyTypeInfo, PyTypeObject}; | ||
| use crate::type_object::{PyTypeInfo, PyTypeObject}; | ||
| use crate::types::{PyAny, PyDict, PyModule, PyType}; | ||
| use crate::{AsPyPointer, FromPyPointer, IntoPyPointer, PyTryFrom}; | ||
| use crate::{ | ||
| ffi, AsPyPointer, AsPyRef, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom, | ||
| }; |
There was a problem hiding this comment.
I have been thinking a bit about whether importing everything from crate like this is a good idea.
It simplifies the imports, but it makes a very busy single line or two which often have merge conflicts when multiple people edit the same file around the same time.
These merge conflicts are not so hard to resolve and it's not a big problem, but I've kinda been thinking that leaving the imports over multiple lines reduces the amount of merge conflicts.
Perhaps we could make guidelines as to what should be imported from crate, and what should be imported from a more specific path?
There was a problem hiding this comment.
Ah, sorry about that.
Perhaps we could make guidelines as to what should be imported from crate, and what should be imported from a more specific path?
Hmm ... 🤔
I'll try to find a similar guideline in other projects.
src/gil.rs
Outdated
| pub unsafe fn register_borrowed(_py: Python, obj: NonNull<ffi::PyObject>) -> &PyAny { | ||
| let pool = POOL.get_or_init(); | ||
| &*(pool.borrowed.push_back(obj) as *const _ as *const PyAny) | ||
| pool.owned.push(obj); |
There was a problem hiding this comment.
Idea: could we make the object storage for owned pointers a thread_local!, rather than using the 'static GILPool? Might be even faster and avoid potential threading issues (might even resolve #756).
There was a problem hiding this comment.
If we're going to support Python on embedded platforms (not sure), we can't use thread_local, however, unless we currently support embedded platforms, I have no issues requiring thread_local support.
Some other thing we should think about, how does it interact with futures trying to move between threads (just make them non-Send?), also, does it work with scoped threads?
There was a problem hiding this comment.
Also, does this PR force types like Py<PyAny> to be non-Sendable?
There was a problem hiding this comment.
If we're going to support Python on embedded platforms (not sure), we can't use thread_local, however, unless we currently support embedded platforms, I have no issues requiring thread_local support.
Oh really? Have you some docs on that? I never saw this issue with thread_local before.
Regarding all the Send interactions - I think even if PyAny is not Send, we can explicitly impl Send for Py<T>, right?
There was a problem hiding this comment.
If we're going to support Python on embedded platforms (not sure), we can't use thread_local, however, unless we currently support embedded platforms, I have no issues requiring thread_local support.
Oh really? Have you some docs on that? I never saw this issue with thread_local before.
I didn't find docs that explicitly say that, however rust-lang/rust#62918 alludes to that:
Futures currently have a dependency on TLS for storing a pointer to
Context<'_>.
https://github.com/rust-lang/rust/blob/master/src/libstd/future.rs#L116This makes usage in
no_stdenvironments quite difficult.
There was a problem hiding this comment.
regarding embedded and TLS, rustasync/team#42 (referenced from previously mentioned issue) is much more clear.
There was a problem hiding this comment.
Oh I see; I think what you're saying is that thread_local! isn't available in no_std applications? It would be interesting to think about pyO3 support for no_std, but I think it's a long way off. Also not sure how many users might want this?
There was a problem hiding this comment.
In that case, I think we should just switch to thread_local for now, and, whenever we add no_std support, we could cfg something else in.
There was a problem hiding this comment.
could we make the object storage for owned pointers a thread_local!, rather than using the 'static GILPool?
Possibly we can, but I failed to implement it due to some odd errors... 🤔
Thread-safe release pools
|
Nice - so I think from the benchmarks:
|
I think we should do that in another PR. |
Ok. I'm prepared to write a follow up PR later to change to owned objects. 👍 For now I have one further benchmark for you for
My conclusions:
(aside, if the scale on your chart is really about ~250 ms then your computer is 10x faster than mine - you using a desktop machine? The benchmark is my laptop using battery power, so worst case measurement 😄 ) |
| type Target = PyAny; | ||
| fn as_ref<'p>(&'p self, _py: Python<'p>) -> &'p PyAny { | ||
| unsafe { &*(self as *const _ as *const PyAny) } | ||
| unsafe { &*(self.as_ptr() as *const PyAny) } |
There was a problem hiding this comment.
Not sure, maybe can just be
| unsafe { &*(self.as_ptr() as *const PyAny) } | |
| unsafe { &*self.as_ptr() } |
There was a problem hiding this comment.
Looks we can't do so...? 🤔
I'm using a desktop machine with Core i5-6500 + Arch Linux 5.6.4. |
Interesting. Thank you for your benchmark. Since now our owned pool is thread-safe, it seems good for me to keep |


TL; DR
This PR changes the internal representation of
PyAnyfrom*mut ffi::PyObjecttoUnsafeCell<ffi::PyObject>.Thus,
&PyAnywas 'a pointer to a pointer', but now it is simply a pointer.Related parts (e.g, GILPool) are also changed.
Partly resolves #679.
What does this PR resolve?
We store almost all objects(except
PyObjectandPy<T>) to the internal storage, which costs when we get many objects.This PR resolves 60% of this problem.
Since now
&PyAnyis a pointer, we don't need to store borrowed pointers to the object storage. So we can simply remove it. 50% of the problem is resolved by this.However, for owned pointers, we still need the object storage so that we can do
obj.refcnt -= 1whengildrops. But we can make the storage a bit faster than now. Now we use LinkedList for the storage so that the pointer to a pointer(=&PyAny) has a consistent address, but after this PR, we can use fasterVecsince we don't use that reference. I believe this optimization resolves 10% of the problem.What does not this PR resolve?
We still have the object storage after this PR. How should we remove this?
Yeah, we can remove it if all constructors return
Py<T>instead ofT, say:Pycan decrease its own reference count when dropping, so it would be efficient.Comparison with #885?
#885 also tries to resolve this problem by introducing
PyAny<'py>, which is a GIL-bounded 'owned' object type. One thing I'm worried aboutPyAny<'py>is that it requires users to change too many codes.The pro of this PR is it does not change almost all API, though it suggests that we should gradually change constructors to return
Py<T>.