-
-
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
Changes from all commits
5de790b
c31dd9b
e57c345
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2902,20 +2902,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)]; | ||
|
||
for (int i = 0; i < cols.Length; i++) { | ||
var name = SQLite3.ColumnName16 (stmt, i); | ||
cols[i] = map.FindColumn (name); | ||
if (cols[i] != null) | ||
fastColumnSetters[i] = FastColumnSetter.GetFastSetter<T> (_conn, cols[i]); | ||
} | ||
|
||
while (SQLite3.Step (stmt) == SQLite3.Result.Row) { | ||
var obj = Activator.CreateInstance (map.MappedType); | ||
for (int i = 0; i < cols.Length; i++) { | ||
if (cols[i] == null) | ||
continue; | ||
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 commentThe 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) |
||
if (fastColumnSetters[i] != null) { | ||
fastColumnSetters[i].Invoke ((T)obj, stmt, i); | ||
} | ||
else { | ||
var colType = SQLite3.ColumnType (stmt, i); | ||
var val = ReadCol (stmt, i, colType, cols[i].ColumnType); | ||
cols[i].SetValue (obj, val); | ||
} | ||
} | ||
OnInstanceCreated (obj); | ||
yield return (T)obj; | ||
|
@@ -3234,6 +3243,227 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr | |
} | ||
} | ||
|
||
internal class FastColumnSetter | ||
{ | ||
/// <summary> | ||
/// Creates a delegate that can be used to quickly set object members from query columns. | ||
/// | ||
/// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, | ||
/// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). | ||
/// </summary> | ||
/// <typeparam name="T">The type of the destination object that the query will read into</typeparam> | ||
/// <param name="conn">The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database.</param> | ||
/// <param name="column">The table mapping used to map the statement column to a member of the destination object type</param> | ||
/// <returns> | ||
/// A delegate for fast-setting of object members from statement columns. | ||
/// | ||
/// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. | ||
/// </returns> | ||
internal static Action<T, Sqlite3Statement, int> GetFastSetter<T> (SQLiteConnection conn, TableMapping.Column column) | ||
{ | ||
Action<T, Sqlite3Statement, int> fastSetter = null; | ||
|
||
Type clrType = column.PropertyInfo.PropertyType; | ||
|
||
var clrTypeInfo = clrType.GetTypeInfo (); | ||
if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { | ||
clrType = clrTypeInfo.GenericTypeArguments[0]; | ||
clrTypeInfo = clrType.GetTypeInfo (); | ||
} | ||
|
||
if (clrType == typeof (String)) { | ||
fastSetter = CreateTypedSetterDelegate<T, string> (column, (stmt, index) => { | ||
return SQLite3.ColumnString (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (Int32)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, int> (column, (stmt, index)=>{ | ||
return SQLite3.ColumnInt (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (Boolean)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, bool> (column, (stmt, index) => { | ||
return SQLite3.ColumnInt (stmt, index) == 1; | ||
}); | ||
} | ||
else if (clrType == typeof (double)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, double> (column, (stmt, index) => { | ||
return SQLite3.ColumnDouble (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (float)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, float> (column, (stmt, index) => { | ||
return (float) SQLite3.ColumnDouble (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (TimeSpan)) { | ||
if (conn.StoreTimeSpanAsTicks) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, TimeSpan> (column, (stmt, index) => { | ||
return new TimeSpan (SQLite3.ColumnInt64 (stmt, index)); | ||
}); | ||
} | ||
else { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, TimeSpan> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
TimeSpan resultTime; | ||
if (!TimeSpan.TryParseExact (text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) { | ||
resultTime = TimeSpan.Parse (text); | ||
} | ||
return resultTime; | ||
}); | ||
} | ||
} | ||
else if (clrType == typeof (DateTime)) { | ||
if (conn.StoreDateTimeAsTicks) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, DateTime> (column, (stmt, index) => { | ||
return new DateTime (SQLite3.ColumnInt64 (stmt, index)); | ||
}); | ||
} | ||
else { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, DateTime> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
DateTime resultDate; | ||
if (!DateTime.TryParseExact (text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) { | ||
resultDate = DateTime.Parse (text); | ||
} | ||
return resultDate; | ||
}); | ||
} | ||
} | ||
else if (clrType == typeof (DateTimeOffset)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, DateTimeOffset> (column, (stmt, index) => { | ||
return new DateTimeOffset (SQLite3.ColumnInt64 (stmt, index), TimeSpan.Zero); | ||
}); | ||
} | ||
else if (clrTypeInfo.IsEnum) { | ||
// NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() | ||
} | ||
else if (clrType == typeof (Int64)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, Int64> (column, (stmt, index) => { | ||
return SQLite3.ColumnInt64 (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (UInt32)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, UInt32> (column, (stmt, index) => { | ||
return (uint)SQLite3.ColumnInt64 (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (decimal)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, decimal> (column, (stmt, index) => { | ||
return (decimal)SQLite3.ColumnDouble (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (Byte)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, Byte> (column, (stmt, index) => { | ||
return (byte)SQLite3.ColumnInt (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (UInt16)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, UInt16> (column, (stmt, index) => { | ||
return (ushort)SQLite3.ColumnInt (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (Int16)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, Int16> (column, (stmt, index) => { | ||
return (short)SQLite3.ColumnInt (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (sbyte)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, sbyte> (column, (stmt, index) => { | ||
return (sbyte)SQLite3.ColumnInt (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (byte[])) { | ||
fastSetter = CreateTypedSetterDelegate<T, byte[]> (column, (stmt, index) => { | ||
return SQLite3.ColumnByteArray (stmt, index); | ||
}); | ||
} | ||
else if (clrType == typeof (Guid)) { | ||
fastSetter = CreateNullableTypedSetterDelegate<T, Guid> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
return new Guid (text); | ||
}); | ||
} | ||
else if (clrType == typeof (Uri)) { | ||
fastSetter = CreateTypedSetterDelegate<T, Uri> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
return new Uri (text); | ||
}); | ||
} | ||
else if (clrType == typeof (StringBuilder)) { | ||
fastSetter = CreateTypedSetterDelegate<T, StringBuilder> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
return new StringBuilder (text); | ||
}); | ||
} | ||
else if (clrType == typeof (UriBuilder)) { | ||
fastSetter = CreateTypedSetterDelegate<T, UriBuilder> (column, (stmt, index) => { | ||
var text = SQLite3.ColumnString (stmt, index); | ||
return new UriBuilder (text); | ||
}); | ||
} | ||
else { | ||
// NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type | ||
} | ||
return fastSetter; | ||
} | ||
|
||
/// <summary> | ||
/// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. | ||
/// | ||
/// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. | ||
/// </summary> | ||
/// <typeparam name="ObjectType">The type of the object whose member column is being set</typeparam> | ||
/// <typeparam name="ColumnMemberType">The CLR type of the member in the object which corresponds to the given SQLite columnn</typeparam> | ||
/// <param name="column">The column mapping that identifies the target member of the destination object</param> | ||
/// <param name="getColumnValue">A lambda that can be used to retrieve the column value at query-time</param> | ||
/// <returns>A strongly-typed delegate</returns> | ||
private static Action<ObjectType, Sqlite3Statement, int> CreateNullableTypedSetterDelegate<ObjectType, ColumnMemberType> (TableMapping.Column column, Func<Sqlite3Statement, int, ColumnMemberType> getColumnValue) where ColumnMemberType : struct | ||
{ | ||
var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); | ||
bool isNullable = false; | ||
|
||
if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition () == typeof (Nullable<>)) { | ||
isNullable = true; | ||
} | ||
|
||
if (isNullable) { | ||
var setProperty = (Action<ObjectType, ColumnMemberType?>)Delegate.CreateDelegate ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
typeof (Action<ObjectType, ColumnMemberType?>), null, | ||
column.PropertyInfo.GetSetMethod ()); | ||
|
||
return (o, stmt, i) => { | ||
var colType = SQLite3.ColumnType (stmt, i); | ||
if (colType != SQLite3.ColType.Null) | ||
setProperty.Invoke (o, getColumnValue.Invoke (stmt, i)); | ||
}; | ||
} | ||
|
||
return CreateTypedSetterDelegate<ObjectType, ColumnMemberType> (column, getColumnValue); | ||
} | ||
|
||
/// <summary> | ||
/// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. | ||
/// </summary> | ||
/// <typeparam name="ObjectType">The type of the object whose member column is being set</typeparam> | ||
/// <typeparam name="ColumnMemberType">The CLR type of the member in the object which corresponds to the given SQLite columnn</typeparam> | ||
/// <param name="column">The column mapping that identifies the target member of the destination object</param> | ||
/// <param name="getColumnValue">A lambda that can be used to retrieve the column value at query-time</param> | ||
/// <returns>A strongly-typed delegate</returns> | ||
private static Action<ObjectType, Sqlite3Statement, int> CreateTypedSetterDelegate<ObjectType, ColumnMemberType> (TableMapping.Column column, Func<Sqlite3Statement, int, ColumnMemberType> getColumnValue) | ||
{ | ||
var setProperty = (Action<ObjectType, ColumnMemberType>)Delegate.CreateDelegate ( | ||
typeof (Action<ObjectType, ColumnMemberType>), null, | ||
column.PropertyInfo.GetSetMethod ()); | ||
|
||
return (o, stmt, i) => { | ||
var colType = SQLite3.ColumnType (stmt, i); | ||
if (colType != SQLite3.ColType.Null) | ||
setProperty.Invoke (o, getColumnValue.Invoke (stmt, i)); | ||
}; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Since the insert never changed, we only need to prepare once. | ||
/// </summary> | ||
|
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.