Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to work with Foundation objects (NSString, NSArray, NSDictionary) in Swift without bridging?

Tags:

macos

swift

cocoa

When using Swift, the Cocoa frameworks are declared to return native Swift types, even though the frameworks are actually returning Objective-C objects. Likewise, the methods takes Swift types as parameters, where that makes sense.

Suppose I want to call a Cocoa method that (in Objective-C) would give me an NSArray and then pass that to a Cocoa method that takes an NSArray. With code like this:

let a: [AnyObject] = [] // Imagine calling a method that returns a huge NSArray.
let mutable = NSMutableArray()
mutable.addObjectsFromArray(a)

It looks like the huge NSArray is going to get bridged to a Swift array when assigned to a and then bridged back to an NSArray when passed as a parameter. At least that's how it seems from profiling and looking at the disassembly.

Is there a way to avoid these potentially slow conversions when I don't need to actually work with the array in Swift? When I'm just receiving it from Cocoa and then passing it back to Cocoa?

At first, I thought that it would help to add type information for a:

let a: NSArray = [] // Imagine calling a method that returns a huge NSArray.
let mutable = NSMutableArray()
mutable.addObjectsFromArray(a as [AnyObject])

But then I have to convert the parameter to a Swift array later or the compiler will complain.

Furthermore, the disassembly for code like:

let c: NSArray = mutable.subarrayWithRange(NSMakeRange(0, 50))

shows calls to __TF10Foundation22_convertNSArrayToArrayurFGSqCSo7NSArray_GSaq__ and __TFer10FoundationSa19_bridgeToObjectiveCurfGSaq__FT_CSo7NSArray, seemingly converting the return value to Swift and then back to Objective-C. (This happens even with Release builds.) I had hoped that by typing c as NSArray there would be no bridging necessary.

I'm concerned that this could lead to inefficiencies with very large data structures, with many disparate conversions of regular ones, and with collections that are lazy/proxied because they are not necessarily large but may be expensive to compute. It would be nice to be able to receive such an array from Objective-C code and pass it back without having to realize all of the elements of the array if they are never accessed from Swift.

This is a very different performance model than with Core Foundation/Foundation where the bridging was toll-free. There are so many cases where code passes objects back and forth assuming that it will be O(1), and if these are invisibly changed to O(n) the outer algorithms could become quadratic or worse. It's not clear to me what one is supposed to do in this case. If there is no way to turn off the bridging, it seems like everything that touches those objects would need to be rewritten in Objective-C.

Here is some sample timing code based on the above example:

NSArray *getArray() {
    static NSMutableArray *result;
    if (!result) {
        NSMutableArray *array = [NSMutableArray array];
        for (NSUInteger i = 0; i < 1000000; i++) {
            [array addObjectsFromArray:@[@1, @2, @3, @"foo", @"bar", @"baz"]];
        }
        result = array;
    }
    return result;
}

@interface ObjCTests : XCTestCase
@end
@implementation ObjCTests
- (void)testObjC { // 0.27 seconds
    [self measureBlock:^{
        NSArray *a = getArray();
        NSMutableArray *m = [NSMutableArray array];
        [m addObjectsFromArray:a];
    }];
}
@end

class SwiftTests: XCTestCase {
    func testSwift() { // 0.33 seconds
        self.measureBlock() {
            let a: NSArray = getArray() as NSArray
            let m = NSMutableArray()
            m.addObjectsFromArray(a as [AnyObject])
        }
    }

    func testSwiftPure() { // 0.83 seconds
        self.measureBlock() {
            let a = getArray()
            var m = [AnyObject]()
            m.appendContentsOf(a)
        }
    }
}

In this example, testSwift() is about 22% slower than testObjC(). Just for fun, I tried doing the array append with the native Swift array, and this was much slower.

A related issue is that when Objective-C code passes Swift code an NSMutableString, the Swift String ends up with a copy of the mutable string. This is good in the sense that it won’t be unexpectedly mutated behind Swift’s back. But if all you need to do is pass a string to Swift and look at it briefly, this copy could add unexpected overhead.

like image 978
Michael Tsai Avatar asked Sep 17 '15 02:09

Michael Tsai


1 Answers

have you tried making an extension?

extension NSMutableArray
{
    func addObjectsFromNSArray(array:NSArray)
    {
         for item in array
         {
              self.addObject(item);
         }
    }
}

Now that I had time to actually play with this instead of talking in theory, I am going to revise my answer

Create an extension, but instead, do it in an objective c file

@interface NSMutableArray(Extension)
    - (void)addObjectsFromNSArray:(NSObject*) array;
@end

@implementation NSMutableArray(Extension)
    - (void)addObjectsFromNSArray:(NSObject*) array
{
   [self addObjectsFromArray:(NSArray*)array];
}
@end

I found the code to work a lot faster doing it this way. (Almost 2x from my tests)

testSwift 4.06 seconds

testSwiftPure 7.97 seconds

testSwiftExtension 2.30 seconds

like image 66
Knight0fDragon Avatar answered Oct 12 '22 23:10

Knight0fDragon