Open samschott opened 4 years ago
Hi, sorry for not replying earlier. Thanks for the report - in the past we haven't focused that much on performance in rubicon-objc, so it's quite likely that some parts of the code just aren't very well optimized. PyObjC will always have a slight performance advantage because it uses native code for many parts of the Objective-C interface (whereas rubicon-objc uses only ctypes
), but the overhead of ctypes
shouldn't be that big, and certainly doesn't explain why rubicon-objc is 60 times as slow as PyObjC.
For testing, I ran for _ in range(1000): NSObject.new().autorelease()
with cProfile, to see if there were any obvious slow calls that happen often. Here are the most significant things I found:
Whenever an Objective-C method returns an Objective-C object (rather than a primitive C value), the returned object is passed through py_from_ns
, to convert the object into a more Python-friendly form. To determine if and how the object should be converted, py_from_ns
checks if the object is an instance of certain classes that can be converted (like NSString
, NSArray
, etc.) using the Objective-C isKindOfClass
method.
At the moment, py_from_ns
has six isKindOfClass
checks. However, when py_from_ns
is called to convert a method result, an extra _auto=True
parameter is passed, which disables all of these checks except for the first two (NSDecimalNumber
and NSNumber
). Still, most objects aren't instances of either of these two classes, which means that almost every method call that returns an Objective-C object results in two extra isKindOfClass
calls on the returned object. This has a noticeable performance impact - as a test, I disabled the NSDecimalNumber
and NSNumber
conversions when _auto=True
is passed, and (on my machine) it reduced the average execution time of NSObject.new().autorelease()
from 600 µs to 300 µs.
I don't think the implicit NSDecimalNumber
and NSNumber
conversions are used very often, so it's worth considering if we should disable those automatic conversions on objects returned from methods. Callers that actually need these conversions can call py_from_ns
explicitly (which will try all available conversions, because _auto
is not set by default).
Whenever an Objective-C method is accessed on an object (e. g. NSObject.new
or obj.autorelease
), a new ObjCMethod
is created from the Objective-C method pointer. ObjCMethod.__init__
makes some Objective-C runtime calls to look up the method's name and type encoding, and then parses and converts the type encoding string into a list of ctypes
type objects. I haven't tested this in detail, but according to the profiling statistics, each ObjCMethod.__init__
call takes about 70 µs.
Objective-C methods never change their name or type encoding after creation, so it should be safe to cache the ObjCMethod
objects for each method pointer instead of reconstructing them every time. This would save 70 µs on every method call after the first one (for each method).
These are just the most obvious things I found - there might be other possible optimizations that I haven't seen. (If you find any other slow parts in the method calling code that could be optimized, let us know!) When I have the time I'll try implementing these optimizations properly - or if you like you can submit a PR yourself.
Thanks for looking into this!
The speedup from from 600 µs to 300 µs when removing py_from_ns
conversions seems worth the tradeoff of not automatically converting numbers.
Of course, there is still more than an order of magnitude difference in performance to PyObjc which may not entirely be due to using ctypes. I think PyObjc may have methods and return types of most Frameworks hard-coded but I am not sure about that.
For now, it would be great if you could implement the optimisations. I still not familiar with most parts of rubicon.objc
.
NSObject.alloc().init()
from about 600 µs to about 200 µs. This is much faster than before, but can probably still be improved further (see #185 for example), so I'll keep this issue open to track that.Wow, I have just tested this and it makes a large difference for UIs with many widgets. The user interface feels a lot smoother, especially when widgets are initialised on-demand, and startup times are significantly shorter.
Thank you!
Is your feature request related to a problem? Please describe.
Instantiating an ObjC class with rubicon-objc takes about 600 - 700 μs which is relatively slow compared to PyObjc with 10 - 15 μs. This can become problematic for instance when showing a table with 30 rows and 5 columns which are visible (see https://github.com/beeware/toga/issues/1030): each of the 150 cells is a
NSTableCellView
which has aNSTextField
and aNSImageView
and 6 layout constraints. Creating these 1350 class instances then takes almost one second which is a very noticeable lag for a "simple" table. A similar task in PyObjc takes about 16 ms.Describe the solution you'd like
Speed up the creation of ObjCClass instances. I am not sure about how rubicon-objc handles creating class instances but can try to look through the code to see if there are obvious slowdowns.