How did we Achieve this Speed-Up?
When we created ObjectBox’s Swift binding, we did not have our C API yet. So, to bridge between the C++ database core and Swift, we implemented a number of classes using Objective-C++. Objective-C++ is neat: it looks to Swift like Objective-C (so like a Swift class
), but will happily call into C++ code.
Objective-C is also a very different language from Swift, and particularly generics and struct
s don’t have a direct equivalent in Objective-C. So when we realized that many reactive frameworks in Swift were built around struct
s, we decided to change gears. We rewrote a number of core Objective-C classes in the Swift binding as Swift classes on top of the C API that we now use in all our newer language bindings, eliminating a lot of Objective-C’s dynamic dispatch, and generally improving upon algorithms here and there.
The main goal was to give our users struct support and lay the groundwork for taking advantage of Swift 5.1’s upcoming UTF8 strings by eliminating use of the UTF16-based NSString
. We also wanted to bring ObjectBox’s Objective-C-beholden error handling in line with Swift conventions. So we expected modest speed-ups here and there, but a speed increase this noticeable even before Swift 5.1 was a pleasant surprise, and we wanted to get these improvements into our users’ hands as quickly as possible.
Using structs with ObjectBox
One rather unique aspect of Swift compared to other languages is how Swift defines the term struct
as value type and class
as reference type. That makes structs incredibly handy for cases where you want to guarantee an object can never change.
Thread-safety, for instance: if you know an object is unchangeable, there is no chance of race conditions while editing. You need no locks, no copies; your threads can all safely operate on the same memory.
However, when you put an object into your ObjectBox database, put(_:)updates
the object’s id property to establish the association between the database entry and your object in memory. ObjectBox can’t do that with unchangeable structs, so we needed to make a slight adjustment to ObjectBox’s usual simple put(_:)
flow:
1 2 3 4 5 6 7 8 | struct User { let id: Id<user> let name: String let admin: Bool } let myUser = User(id: 0, name: "Kathryn Drennan", admin: true) let savedUser = box.put(struct: myUser)</user> |
Missed the difference? It’s tiny: You use put(struct:)
instead of the regular put(_:)
. put(struct:)
will create a copy of the struct you gave it with the ID changed to whatever ID the object was assigned in the database (the copy is what we store in savedUser in the above example).
So, what if you want to make changes? The way you change immutable structs is to make a copy with the one thing you wanted to change set to a different value. So while you could save it to the database using put(struct:)
, you already made a copy of the object, and it will not change after being saved, because it already has an ID. Won’t that second copy be wasteful?
That’s why Box now also offers putImmutable(_:)
. If you know that your object has already been saved, and you don’t need a copy. Just call putImmutable(_:)
. instead of put(struct:)
.
1 2 3 | let myUser = users.first let savedUserID = box.putImmutable(myUser) // assert(myUser.id == savedUserID) |
This will return the ID for your convenience, but you can always ignore it, if you do not need it.
What else has changed?
While we’re always improving things under the hood, not much should change for your existing ObjectBox code. Apart from the new ObjectBoxError
enum replacing the janky old Objective-C-style OBXError...
classes, your existing code should just compile. All the changes are in the generated code.
Go give it a try, and let us know how we’re doing.
It would be interesting to see a performance comparison between ObjectBox, Realm and Core Data.
Just FYI, you don’t actually need to make a structs fields all be constant in order to make it immutable. Semantically, changing a field in a struct is exactly the same as replacing the entire struct with a new one that has a different value in that field. The in-place update of fields is merely an optimization possible because of Swift’s memory exclusivity rule.