Skip to content
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

Merged
merged 3 commits into from
Dec 6, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 233 additions & 3 deletions src/SQLite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
Copy link
Contributor Author

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.


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);

Copy link
Contributor Author

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 (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;
Expand Down Expand Up @@ -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 (
Copy link
Contributor Author

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.

Copy link
Owner

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.

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>
Expand Down