-
Notifications
You must be signed in to change notification settings - Fork 758
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
experimental: owned objects API #1300
Conversation
Wow, thank you for this big effort! I'm sorry that I don't have time to give a thorough check, but let me give a design question: I think that we should design new APIs centered on owned objects, and Currently, our APIs are built around borrowed objects such as
Also, I was thinking of |
@kngwyu good questions! Once I understand I agree that having the core types be owned can be nicer. The piece I am not sure I understand is
|
The only strong point I have is owned-first so I think there can be various ideas about
I meant so.
It can be problematic, but
But is |
I think that both are about the same for I wonder if for the moment while we are experimenting with this branch we can have
And then learn about which one we find is nicer to use in practice. |
Also, closing this PR in favour of #1308 which is the branch we'll actually merge eventually. |
This is an experimental extension to the pyo3 API to provide a new set of types which do not use pyo3's internal objects store.
There's two objectives from this new API:
Design
The goal with this PR is to keep the existing pyo3 API 100% backwards-compatible. There should be no changes needed to existing code if we merge this API. This allows it to be opt-in for now while we collect feedback from the new design, and make changes if needed.
PyO3 can make use of the new API internally to benefit from speedups where possible.
Because this is 100% backwards-compatible, and it's also a very big amount of changes, I would like to merge this API and start getting feedback. The promise to users is that we can change (or remove) the entire
pyo3::experimental
module at any time until we're happy with it.When we are ready to make this new API official, the next steps would be to deprecate owned references and finally remove the pyo3 object store completely.
New APIs
In the
pyo3::experimental
module, I've added some new APIs, explained below. I've also made apyo3::experimental::prelude
module. Anyone who wants to test this API can swap this for the original prelude (along with the other new imports).I welcome feedback and alternative designs for all of these. I think it's important to solve this problem of pyo3's memory management. I don't claim to have the answer. I think what I have here is a pretty good start.
New
PyOwned<'py, T>
typeTo be able to return owned pointers from the new objects API there is a new
PyOwned<'py, T>
type. It's basicallyPy<T>
with aPython<'py>
, so it's bound to the GIL.New
objects::PyAny<'py>
etc.These "objects" are very similar to the existing
types::PyAny
(etc.). They are also used as references. This time they are always borrowed (becausePyOwned
is the owned reference).An owned object
PyOwned<'py, Any>
will deref to&'a PyAny<'py>
:'a
lifetime is Rust's usual lifetime for borrowed data. This is essential for tracking that the borrowed reference is only used while the owned object is kept alive.'py
lifetime is the GIL lifetime. It is different to'a
because the GIL lifetime is the same all the way through functions. If it is mixed in with'a
(like in thetypes
API) it becomes shorter and shorter each time aPyOwned
is borrowed, which is very annoying.The motivation with these borrowed objects is exactly the same for the same reason we changed
&'a PyAny
to have the same memory representation as*mut ffi::PyObject
. It's the fastest and most direct representation we can have for Python objects.It also means that migrating to the new API is easier. Where we currently have
switching to the new API compiles immediately for this:
For functions with return values, there is a little more work:
New
types::Any
etc.The new
'py
lifetime on&'a PyAny<'py>
is important for the reasons above. However it's really annoying inPy<T>
andPyOwned<'py, T>
, because:Py<PyAny<'py>>
there's no GIL, so the'py
lifetime doesn't make sense.PyOwned<'py, PyAny<'py>>
we already have the lifetime; it's annoying to have to repeat it.The solution I've come up with in this API is for the type parameter in
Py<T>
to be a type tag. In fact, it's just the sametypes::PyAny
which we already use. I wanted to give it a different name so that we don't confuse it with the newobjects::PyAny
.So I re-exported the old types without the
Py
- prefix, e.g.Any
,Dict
,Bytes
, and so on. I chose this because it removes repetition ofPy
aswell - we just havePy<Any>
,Py<Dict>
,PyOwned<'py, Any>
etc.experimental::FromPyObject
andexperimental::PyTryFrom
To be compatible with
&'a PyAny<'py>
, these two traits needed to be changed to also have input lifetimes'a
and'py
.For the moment I just copied them into the new experimental module and added the extra lifetime. I'm tempted to make this opportunity to rework the existing traits (if we want to make any changes). On the other hand, keeping them as similar as possible to the existing ones will make migration easier.
Benchmarks
I added some new benchmarks for the experimental API. To make the reference-releasing comparable with the old API I added
unsafe { py.new_pool() }
to the existing benchmarks.In most benchmarks the new api is either the same or a little faster. For some that currently put a lot of owned references into pyo3's store, like dict iteration, the new API is significantly faster. (~30%). Even better, with the new API the memory consumption remains flat during iteration. In the existing API it grows as the references are put into pyo3's store.
Here's a full table of comparison:
52,895 ns/iter (+/- 6,956)
54,954 ns/iter (+/- 2,175)
153,977 ns/iter (+/- 9,671)
153,001 ns/iter (+/- 6,288)
1,933,300 ns/iter (+/- 328,382)
1,780,027 ns/iter (+/- 79,625)
9,538,110 ns/iter (+/- 634,895)
8,688,905 ns/iter (+/- 590,359)
8,099,900 ns/iter (+/- 570,799)
7,842,395 ns/iter (+/- 535,694)
5,013,300 ns/iter (+/- 483,341)
4,292,595 ns/iter (+/- 358,152)
5,418,065 ns/iter (+/- 440,259)
4,900,090 ns/iter (+/- 355,389)
1,759,862 ns/iter (+/- 209,509)
1,304,683 ns/iter (+/- 133,726)
1,262,143 ns/iter (+/- 76,108)
1,109,227 ns/iter (+/- 52,926)
1,257,083 ns/iter (+/- 100,680)
1,037,567 ns/iter (+/- 120,639)
824,397 ns/iter (+/- 46,234)
753,351 ns/iter (+/- 125,318)
573,128 ns/iter (+/- 61,386)
531,383 ns/iter (+/- 21,875)
408,859 ns/iter (+/- 25,755)
407,605 ns/iter (+/- 21,552)