-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Performance Optimization: Fast Column Setters #902
Conversation
Note: If anyone else would like to try reproducing my results, the program I used to time performance is here: https://gist.github.com/cherron-aptera/a31f1de0fd489ffd74db248b62e5a6e5 |
@@ -2855,20 +2855,29 @@ public IEnumerable<T> ExecuteDeferredQuery<T> (TableMapping map) | |||
var stmt = Prepare (); | |||
try { | |||
var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; | |||
var fastColumnSetters = new Action<T, Sqlite3Statement, int>[SQLite3.ColumnCount (stmt)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we cache a list of fast column setters at the beginning of the loop.
var colType = SQLite3.ColumnType (stmt, i); | ||
var val = ReadCol (stmt, i, colType, cols[i].ColumnType); | ||
cols[i].SetValue (obj, val); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a FastColumnSetter is ever not available for this column, then we don't worry about it and we just fall back to the (slow) PropertyInfo.SetValue()
method.
} | ||
|
||
if (isNullable) { | ||
var setProperty = (Action<ObjectType, ColumnMemberType?>)Delegate.CreateDelegate ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's frustrating to me how much of this code is almost identically copy-pasted between CreateNullableTypedSetterDelegate
and CreateTypedSetterDelegate
, but I couldn't find a clean way around it. I don't know if / how ProtoBuf deals with this, but I wonder if they don't support nullable types the way SQLite-net does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I like your code, my biggest hesitation is how much dupe code the reader layers are accumulating. I think that cleanup work will be for another time though.
…om being called on every row of every query. This greatly improves large query performance because it frontloads the heavy Reflection checks to only be called once per query.
…k for nullable types.
c7e7546
to
e57c345
Compare
Rebased and re-uploaded the branch to trigger the automatic build checks again. This is a significant performance optimization, and I would still love to see this get merged into |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this. I've been meaning to implement Strongly-Typed Delegates forever now. It's certainly faster on iOS.
I really appreciate all your effort and research into this! 🤠
} | ||
|
||
if (isNullable) { | ||
var setProperty = (Action<ObjectType, ColumnMemberType?>)Delegate.CreateDelegate ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I like your code, my biggest hesitation is how much dupe code the reader layers are accumulating. I think that cleanup work will be for another time though.
Seems to cause trouble with SqliteNetExtensions
|
@tranb3r It looks like it was attempting to persist a List<> with SqliteNetExtensions -- what type of object was stored in the list, do you know? If you could possibly set a breakpoint and see what the expected method arguments are, I could look into why this is happening. It looks like you're doing this on Android? I'm curious to know if it happens on other platforms as well. If we could build an isolated test project, that would help me with reproducing -- but I don't know how much trouble that would be. |
A bit more context:
I can definitely set a breakpoint, but could you please tell me where exactly (I guess when calling the CreateDelegate in CreateTypedSetterDelegate method ?) and what info you need ? I can also try to build a repro project, but I guess it's only relevant to do it on android since the exception is in mono ? |
Hmm, good question. Maybe the first step is to see if we can reproduce it in a desktop environment? |
@tranb3r If you are able to give me a code snippet for how you're using sqlitenetextensions, I can see about building a sample application from my end? |
I've created a simple repro project, with 2 applications:
With sqlite-net-pcl 1.8.0-beta, there is an exception on both environments:
No error with 1.7.335. The code for the test is in TestClass.cs in the shared lib (TestSqliteNet). @cherron-aptera Let me know if you need more information. |
I haven't yet taken time to try to fix this yet, but wanted to confirm that your test project is excellent -- I am able to run it locally and it fails for me with the same exception that you reported. Well done on this -- thank you! |
@cherron-aptera Did you have the time to take a look ? Maybe I should open a new issue ? |
@cherron-aptera Sorry to insist: should I open a new issue ? Maybe somebody else will have the time to take a look. Thanks. |
@tranb3r I'm sorry, I haven't taken time to take a look yet. I will try to do so, but feel free to open an issue. If anyone else has time to look at it also, that would be very welcome! |
This PR changes the way that we do column lookups to be once-per-query rather than once-per row. By doing this, we can speed up large query retrieval by ~500%.
We have to do some funky stuff re: creating dynamic strongly-typed setter delegates, but other libraries have already gone this way (such as Protobuf), so we can follow in their footsteps a bit.
Detailed explanation: Simple Syntax vs. Fast Speed
SQLite.net is very easy to use, and has great facility for mapping from SQLite tables into object properties. However, for very large queries, response times can be a bit on the slow side:
If OrderDetail is 1 million records or so, then performance is very poor:
Retrieved 1000000 records in 14852 ms
Why does it take 15 seconds to query from a local database? This should be FAST!
Traditionally this can be overcome by doing some low-level work and stepping through a SQLite query on your own:
uuuugh. It's much more cumbersome syntax, but the performance boost is undeniable:
Retrieved 1000000 records in 1350 ms.
That's more like it! It runs in a tenth of the time! Both are using SQLite.net with the same query on the same database -- so why is Method 1 so slow?
In-Loop Reflection as the Root of Evil
The core of the problem lies in the fact that the inside of SQLite.net's
ExecuteDeferredQuery
loop makes a call toPropertyInfo.SetValue ()
. This is a rather heavy-weight call that uses Reflection every time it's called to make sure that the genericobject
being passed into it is compatible with the type held by that particular PropertyInfo.So how do we fix this? Is there a way to have the syntactic sugar of Method 1 with the speed of Method 2?
The good news is yes -- there are two alternatives that people have used to approach this problem.
Fast-Member
The fastest method is to emit dynamic IL and link into that at runtime. This is the method used by the excellent Fast-Member library.
However, there are .NET platforms (such as Xamarin.iOS and Unity IL2CPP) that don't support such shenanigans, so I'd rather not go down that road.
Strongly-Typed Delegates
The second method isn't quite as fast as dynamic IL, but it's still reasonably fast. It involves doing all of the type-checking reflection outside of the query loop, and creating strongly-typed delegates to set property values.
Jon Skeet wrote a blog post that explains this technique, as well as a Stack Overflow answer that summarizes nicely. This is the same technique used to add this same speed-boost to Google's Protobuf library.
I'm not doing everything quite the same way as he is (to be honest, I had a hard time following all of it), but I think the version I created is hopefully easy enough to read, and similarly zippy at runtime.
Note that we avoid quite a bit of confusion by simply skipping the Enum case, and falling back to the original method of simply calling Column.Set() on every row. For enumerated types, then my pull request will simply fall back on the old (and slow) method of calling
PropertyInfo.SetValue()
on every row. Yes, it's slow -- but at least it's not going to be any slower than it was before this change (and maybe someone else can help figure out the black-magic voodoo to make strongly-typed delegates for enums function).Measuring Performance
So what does this all boil down to? Well, let's check the performance.
Prior to my pull request:
After FastColumnSet:
Still not nearly as good as the hand-created mappings, but 15 seconds down to 3 seconds is still an impressive boost!
There's a lot of junk I needed to put in there to make Nullable types work properly (ugh). If I skip Nullable checks and defer nullable types back to the legacy method, then it makes things a tad bit simpler, but I don't think the speed increase is that significant -- it shaves off maybe 500ms or so.
Feedback?
What do you all think? Is this clean enough / general-purpose enough to make it into the main trunk?
I'm certainly open to input on how to make this all cleaner -- it was a bear getting this all to work properly, but I'm very very thankful for a comprehensive unit test suite in SQLite-net. Kudos for that. :)