Performance bottlneck interacting with statement cache in native
See original GitHub issueHi! I’m using SQLDelight on iOS. SQLDelight generates queries that properly setup a unique int identifiers for use with statement caching. I’m doing some performance testing of a date serialization function that we’re using for getting dates in and out of a sql column, and I noticed that SQLDelight interacting with the statement cache was the main bottleneck on inserts. Here’s a snippet of the query being used:
fun insert(Comment: Comment) {
driver.execute(4,
"""INSERT OR REPLACE INTO Comment VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", 10) {
bindString(1, Comment.field)
/* more bind code followed by notifyQueries*/
}
We’re doing the following to bulk insert these objects:
queries.transaction {
for (object in objects) {
queries.insert(object)
}
}
Heaviest trace:
To insert 100k objects, it takes 17s on my machine. 9s of that is the above trace. Is there a more efficient way in SQLDelight to do this kind of bulk insert? Thanks!
Update: that trace is a bit misleading. That heavy stack is shared by inserts and fetches: 6s for inserts, 3s for fetches.
Issue Analytics
- State:
- Created 5 years ago
- Comments:34 (14 by maintainers)
Top GitHub Comments
To be clear, this will also make the Kotlin native sqlite/sqldelight performance way better than Android (presumably lack of jni). When multithreaded coroutines happens, I have some ideas about a super optimized mutation architecture, where all updates are sent to a single thread queue, and we take some of the sqlite thread sanity checks off, but that’s definitely not something we need to chat about soon.
Learned a lot about performance over the weekend. Here’s where we’re at.
A lot of the time is involved with the internal map and list accounting in stately. As each entry is added/removed, there’s an internal object “Node” that gets created. That’s for the linked list. Each one of those needs to be frozen, which was adding a lot of time, but also, for add/remove, you’re doing a lot of atomic reference assignment. It’s expensive.
Adding an object pool for Node sped that up dramatically. Object pool is kind of ugly, but Kotlin Native is not exactly optimized, so it is what it is.
After that, I was poking through the code some more. We remove the statement from the cache for both selects and mutations, but it only makes sense for selects. Keeping them in the cache for mutation statements means more significant performance increase. It also makes the object pool kind of pointless, at least in our case.
I’m taking one quick look now at another option. Keeping a weak ref to statements in a thread local. That’s about as fast as you’d be able to push it before sqlite itself becomes a significant portion of the proc time.
The speed diff depends largely on number of fields in table, as the slow down was a per-insert call. I ran a test with a 2-field table inserting 500k rows. Current lib is just under 40s, no thread local was about 10s, and with thread local the first shot was about 7s. 5x-ish. For tables with more cols, it’s kind of ~3x.
I just ran the original sample for 250k and the current sqldelight driver is about 17.5s and with all the tweaks it’s about 5.2s.
Making these changes does not change the sqldelight driver a whole lot, but it’s also not a huge priority to get into a release, but I’ll do a bit more poking then wrap a PR together to take a look at.