Skip to content

Support assigning to __class__ #1981

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

Merged
merged 16 commits into from
Jul 15, 2020

Conversation

BenLewis-Seequent
Copy link

Supports the following:

a.__class__ = B

Note at the moment this incorrectly allows, but this is a wider issue(#1979 ):

a.__class__ = int

I used https://docs.rs/arc-swap/0.4.7/arc_swap/, for both the typ and dict fields within PyObject as it seems like that would have a slightly lower overhead than a Mutex. A benchmark with just the dict mutex replaced with ArcSwap showed a small improvement. Even with that, this PR has caused a regression of around 25% 😞, due to the typ field being accesed quite a lot(I think).

Copy link
Member

@coolreader18 coolreader18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great so far!

// TODO: make this RwLock once PyObjectRef is Send + Sync
pub(crate) dict: Option<Mutex<PyDictRef>>, // __dict__ member
pub(crate) typ: ArcSwap<PyObject<PyClass>>, // __class__ member
pub(crate) dict: Option<ArcSwap<PyObject<PyDict>>>, // __dict__ member
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very useful type! I was thinking about using a sort of AtomicArcCell for these fields, but I wasn't sure it would be able to be fully atomic; looks like it is!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It suits out use case pretty well as it's optimised for reading, which is the operation that we really care about here.

@coolreader18
Copy link
Member

re: accessing typ a lot, maybe there could be pub(crate) method that returns an arc_swap::Guard so that internal method lookup doesn't have to go through the cost of load_full()

@BenLewis-Seequent BenLewis-Seequent mentioned this pull request Jul 5, 2020
6 tasks
@coolreader18
Copy link
Member

Or I suppose the field is already pub(crate) you could just do obj.typ.load()

@BenLewis-Seequent
Copy link
Author

I have been able to reduce the performance regression of this PR down to just under 10% by using PyLease(wrapper around arc_swap::Guard) in a lot of places. Is this an acceptable regression? Otherwise I will have to look again where I can improve performance around this.

@BenLewis-Seequent BenLewis-Seequent changed the title [WIP] Support assigning to __class__ Support assigning to __class__ Jul 10, 2020
@coolreader18
Copy link
Member

What's the performance regression like if you use IndependentArcSwap? That might be better, since it shouldn't have to deal with multiple locks on ALL __class__ fields at once

@BenLewis-Seequent
Copy link
Author

That actually made it worse 😢. The benchmark results for bench_rustpy_mandelbrot are as follows:

master: 1,473,571,865 ns/iter (+/- 9,875,746)
ArcSwap: 1,562,154,302 ns/iter (+/- 8,891,651)
IndependentArcSwap: 1,691,609,695 ns/iter (+/- 14,180,686)

I'm a little bit surprised by the amount these change are having on the overall performance.

@coolreader18
Copy link
Member

coolreader18 commented Jul 11, 2020

I guess 3+ atomic ops on every method lookup compared to just an offset of the pyobject does have a decent impact. Kinda just curious now, but how does this compare to a parking_lot::RwLock? If that's easy to do for you. Maybe these "optimizations" are just more work than locking an uncontended RwLock (which is just one atomic op, I think). Idk, if not, the regression is probably fine. Maybe we could have cargo features to disable obscure-ish python features like this in order to get a performance speedup.

@BenLewis-Seequent
Copy link
Author

RwLock test bench_rustpy_mandelbrot 1,318,745,820 ns/iter (+/- 5,858,271) 🎉

The above includes the changes I have made within this PR that reduces the amount of clones that we perform, that is why it's faster than no locking. I'm surprised that the RwLock performs so well.

@coolreader18
Copy link
Member

coolreader18 commented Jul 11, 2020

Under the hood, it looks like arc-swap has their own sort of parking lot with shards and stuff, but that naive implementation with a bunch of work done on each lock + only a limited number of total locks can't beat parking_lot with its 1 atomic op for uncontended locking and "parking lot" for determining contention.

@BenLewis-Seequent
Copy link
Author

This is ready for review now.

Copy link
Member

@coolreader18 coolreader18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!!

// TODO: make this RwLock once PyObjectRef is Send + Sync
pub(crate) dict: Option<Mutex<PyDictRef>>, // __dict__ member
pub(crate) typ: RwLock<Arc<PyObject<PyClass>>>, // __class__ member
pub(crate) dict: Option<ArcSwap<PyObject<PyDict>>>, // __dict__ member
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use RwLock for dict as well?

}

/// Determines if `subclass` is actually a subclass of `cls`, this doesn't call __subclasscheck__,
/// so only use this if `cls` is known to have not overridden the base __subclasscheck__ magic
/// method.
pub fn issubclass(subclass: &PyClassRef, cls: &PyClassRef) -> bool {
subclass.iter_mro().any(|c| c.is(cls))
pub fn issubclass<T: DerefToPyClass + IdProtocol, R: IdProtocol>(subclass: T, cls: R) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason you couldn't just do subclass: &PyClass? I think auto-reborrowing would let you pass &PyLease or &PyClassRef for that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subclass parameter needs to know it's identity for the case: issubclass(A, A), which &PyClass doesn't know as that's just the payload.

@coolreader18 coolreader18 merged commit 0ab0a0a into RustPython:master Jul 15, 2020
@BenLewis-Seequent BenLewis-Seequent deleted the class-assign branch July 15, 2020 18:58
@youknowone
Copy link
Member

#1568

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

Successfully merging this pull request may close these issues.

4 participants