Skip to content

Conversation

johnhaddon
Copy link
Member

We have a few issues with the way we have wrapped classes in Python to date, using the IECorePython::Wrapper class :

  • We pay the overhead of wrapping a class even when we don't end up deriving from it in Python. Many classes (nearly all the Parameters) are wrapped so they can be derived from in Python, but this is a very rare use case - most Parameters are just used directly and most Parameter instances are not of Python derived types.
  • When a class is wrapped, all its wrapped methods must acquire the GIL and enter Python to see if there is an override there. In many cases there is not. Entering Python is slow, and acquiring the GIL destroys parallelism.
  • When a class is wrapped, the C++ class must hold a reference to the Python half of its implementation. This creates a circular reference which must be broken by garbage collection. This has overheads in both memory and runtime.
  • Wrapping classes must use ugly macros like IECOREPYTHON_RUNTIMETYPEDWRAPPERFNS to repeat the same old overrides time and time again.

This pull request implements a new style of wrapping which solves all these problems. In its constructor, it determines whether the instance is of a Python subclass or not, and provides this information via an isSubclassed() method. This can be called without acquiring the GIL, and is used to avoid looking for method overrides when that is not necessary. When an instance isn't subclassed we also avoid making a circular reference, and avoid all garbage collection overhead. The new wrappers also automatically provide the appropriate virtual overrides, with no need for macros.

There are two potential downsides to this change :

  • We can no longer inject overrides for methods into non-subclassed Python objects. To override a method you must derive from the base class as you would in C++. This isn't terribly Pythonic, but it is necessary to enabled this optimisation, and this optimisation is very necessary.
  • We can no longer guarantee a one-to-one relationship between a C++ object and a Python object for non-subclassed objects. We've always had a problem whereby two non-wrapped Python objects could refer to the same C++ object, and we use the "a.isSame( b )" workaround where normally one would use "a is b". Because wrapped classes hold onto their Python half, we can guarantee a one-to-one mapping, but now we only guarantee that when a wrapped object is subclassed in Python. Otherwise we would have to pay the garbage collection penalty. I have some ideas as to how this could be solved, and solved for non-wrapped objects too, but that will be something for the future.

Right now, Cortex has both styles of wrapping available, and all existing classes are wrapped using the old style - nothing has changed for Cortex users. I'm about to put in a pull request for Gaffer which uses the new style, as it's critical for performance there. In that pull request you'll see some of the gotchas with the new approach, and how to deal with them. Once we've transitioned over to Gaffer using the new style, and got any problems ironed out we can move Cortex to the new style too, and remove all the old deprecated code - but I suggest we don't do that till the next major version.

There is significant overhead in acquiring the GIL and entering python looking for virtual method overrides, and this has been showing up in several profiles recently, and also badly affecting thread scaling. Often the overhead was being incurred for classes which were wrapped but for instanced which weren't actually subclassed in python or defining any overrides.

This new mechanism detects whether or not an object is subclassed in Python at construction, and avoids ever entering Python looking for overrides for non-subclassed objects. It also avoids creating reference cycles and the associated garbage collection in this case.

Additionally, the provision of wrapper classes specific to different base classes makes wrapping a large class hierarchy much easier and less error prone. The RunTimeTypedWrapper automatically adds the appropriate virtual method forwarders whereas before every piece of wrapping code had to manually insert the IECOREPYTHON_RUNTIMETYPEDWRAPPERFNS macro. This style of wrapping has been in use in Gaffer for a while and has worked well there.

Currently, none of the existing Cortex bindings are using the new wrapping mechanism - for now it is intended to be used in Gaffer first where the performance benefits will be felt the most.
Intrusive pointers come at a cost, and there's no need to use them here.
Prior to this we were relying on the built in Python defaults, which compared equal only between the exact same python objects. Since we can't guarantee that a single RefCounted object is always represented by only one Python object, this default doesn't always give us the right answer. This has always been a problem for non-wrapped objects, but is now a problem even for wrapped classes if they use the new style wrappers and haven't been subclassed in Python. Ideally in the future we'll fix the object identity problem once and for all, for both wrapped and non-wrapped objects, but for now this is a good improvement.
The default hash is based on the address of the Python object, basing it on the address of the underlying C++ object prevents issues caused by two python objects referencing the same underlying RefCounted object.
@ldmoser
Copy link

ldmoser commented Mar 10, 2014

Great work John! I'm looking forward to apply the new wrappers in Cortex 9!
Andrew, do you want to look at it as well?

Fixed a duplicate method name in RefCountedTest, fixed a typo in RunTimeTypedBinding.h and deprecated the old constructor for WrapperGarbageCollector.
@johnhaddon
Copy link
Member Author

Thanks for spotting that error Lucio - I've fixed it along with a couple of other small tidy ups.

johnhaddon added a commit that referenced this pull request Mar 11, 2014
@johnhaddon johnhaddon merged commit 8213257 into ImageEngine:master Mar 11, 2014
@johnhaddon johnhaddon deleted the improvedWrappers branch March 11, 2014 12:01
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.

2 participants