重复造轮子感悟 – XLinq性能提升心得
曾经的两座大山
1、EF
刚接触linq那段时间,感觉这家伙好神奇,语法好优美,好厉害。后来经历了EF一些不如意的地方,就想去弥补,既然想弥补,就必须去了解原理。最开始甚至很长一段时间都搞不懂IQueryProvider(小声说,其实现在也没完全搞懂),好不容易把IQueryProvider搞懂了,然后才发现好戏才刚刚开始,这个时候尝试写了第一个ORM。那个时候不明白表达式树的原理,自然一开始的思路就是走一点算一点,走到后面就没法走了,因为思路太乱了。这个时候就感觉EF太牛了,那么复杂的linq都能翻译出来,虽然翻译的sql的质量不行,而且还有坑,不过至少翻译出来了,感觉自己永远都没办法做到。
2、Dapper
这框架是大名鼎鼎的,性能是相当高的,底层是用EMIT写的。然而我这人就是有技术洁癖,如果是我自己一个人开发,那么如果这个工具让我感觉到了不爽,我就会尝试自己开发。所以我也没用dapper,原因只是它直接操作了Connection,而且要手写sql代码。但是我自己写的在性能上始终是个硬伤,跟dapper比不了。之前我业余用表达式树实现了DataSet到List的转换,然后拿到公司装逼,同事来了一句"来来咱跟dapper比比",我说"得得你就别虐我了成不",比的结果当然是比较惨的,那个时候我觉得我不可能达到dapper的转换速度。
性能提升测试
测试代码
-
static void Main(string[] args)
-
{
-
EFDbContext db = new EFDbContext();
-
db.Configuration.AutoDetectChangesEnabled = false;
-
db.Configuration.LazyLoadingEnabled = false;
-
db.Configuration.ValidateOnSaveEnabled = false;
-
XLinqDataContext xlinq = new XLinqDataContext();
-
db.Users.Where(x => false).ToList();//让EF完成初始化
-
-
-
ExecuteTimer("EF20万数据查询", () =>
-
{
-
db.LargUsers.Take(200000).ToList();
-
});
-
-
GC.Collect();
-
ExecuteTimer("XLinq20万数据查询", () =>
-
{
-
var a = xlinq.Set<LargeUser>().Take(200000).ToList();
-
});
-
GC.Collect();
-
ExecuteTimer("Dapper20万数据查询", () =>
-
{
-
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["test"].ConnectionString);
-
var a = conn.Query<LargeUser>("SELECT top 200000 * from dbo.largeusers");
-
});
-
GC.Collect();
-
ExecuteTimer("XLinq50万数据查询", () =>
-
{
-
var a = xlinq.Set<LargeUser>().Take(500000).ToList();
-
});
-
GC.Collect();
-
ExecuteTimer("Dapper50万数据查询", () =>
-
{
-
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["test"].ConnectionString);
-
var a = conn.Query<LargeUser>("SELECT top 500000 * from dbo.largeUsers");
-
});
-
Console.ReadKey();
-
}
-
-
static void ExecuteTimer(string name, Action action)
-
{
-
var watch = Stopwatch.StartNew();
-
action();
-
watch.Stop();
-
Console.WriteLine(string.Format("{0}:{1}毫秒", name, watch.ElapsedMilliseconds));
-
}
测试结果
秒EF是妥妥的,但dapper基本上性能一样,那点差距就直接忽略吧。
之前也进行过一次,被dapper秒,和EF性能一样,原因是因为字典缓存用的有问题。
而这次这个最终取数据全部用的数组,一开始其实把dapper都给秒了,快了dapper接近一半,不过后来发现情况没考虑全,考虑全了就差不多了。
感悟和心得
-
Expression Tree与EMIT
应该有不少人都认为后者比前者快,我一开始也这么认为的。
但园子一位大神说,Expression Tree与EMIT最终生成的代码是一样的,所以性能是一样的。说实在的,我还是不太信。
然后老赵大神又说,Expression Tree最终也是操作的EMIT来实现的。好吧,其实我有点信了,但还是不太信。
直到我在写XLinq的时候用纯Expression Tree超越了用纯EMIT的Dapper,我才相信。(不信?可以在评论里要源码)
-
基础类型转换的坑
之前造轮子的时候,一直觉得Convert.ChangeType是尚方宝剑,一剑在手,天下我有。
然而在写XLinq的时候,被这个方法坑的不轻。
当我要long、int、short、int?、long?、short?这几个类型相互转换的时候,总会冒出来异常。
然后谷歌一下,园子大神的博文说:"这个方法一遇到Nullable<T>类型的时候就会挂掉"。
好吧,我认了(说错了的话请指正)。然后不得不自己处理类型转换,同时也算是知道了这个坑。
-
LINQ并不能完美实现无缝切换数据库
一直认为只要linq provider写得足够好,就能支持无缝切换数据库,注意是切换不是支持。
然而现在看来,想要做到直接改一下配置文件就能切换这是不可能的。
举一个例子,假如说要将sql server数据库切换到sqlite,这个时候切换过去之后看起来其实不会有什么问题,因为sqlite即使语法不对也可能不会报错。
在sql server中,2015/1/1这是合法的日期型数据,但这样的数据在sqlite中是无法识别的。
也就是说,如果你之前用的sql server,并且使用了这样的数据格式,那么切换到sqlite后可能所有关于日期的判断会全部出错,并不是指报异常,而是说计算结果不正确。
-
将IDataReader转换到List的功能整个直接生成代码,不再生成每一个属性的委托
之前在写类似于ORM的工具时,总会将先生成每一个属性的Setter和Getter委托,然后再单独调用这些委托来完成转换。
这样就几乎无法避免装箱拆箱的问题,但装箱拆箱其实还是会浪费一点性能的。
后来想到了另一种办法,不再生成Setter和Getter的委托,转而生成包装了一个循环的委托。
就是说,我传给委托一个类型和一个Reader对象,它就直接返回给我一个List,我不用去获取每一个属性的委托。
委托中直接生成了访问该类型的每一个属性的代码,这样就直接避免了装箱拆箱,最终性能跟dapper差不多了。
不过这样做可能会占用多一点缓存,不过现在来说内存应该不是问题
IDataReader转List关键代码
1 public static Func<IDataReader, IList> GetDataReaderMapeer(Type type,IDataReader reader) 2 { 3 Func<IDataReader, IList> func = null; 4 if (!_dataReader2ListCahce.TryGetValue(type, out func)) 5 { 6 lock (_dataReader2ListCahce) 7 { 8 if (!_dataReader2ListCahce.TryGetValue(type, out func)) 9 { 10 var readerExp = Expression.Parameter(ReflectorConsts.IDataReaderType, "reader"); 11 var properties = ExpressionReflector.GetProperties(type); 12 var fieldCount = reader.FieldCount; 13 var expressions = new List<Expression>(); 14 var objVar = Expression.Variable(type, "entity"); 15 var fieldCountVar = Expression.Variable(ReflectorConsts.Int32Type, "fieldCount"); 16 var readerVar = Expression.Variable(ReflectorConsts.IDataReaderType, "readerVar"); 17 var propertyNameArr = Expression.Variable(ReflectorConsts.StringArrayType, "pis"); 18 var indexArrVar = Expression.Variable(ReflectorConsts.Int32ArrayType, "indexes"); 19 var readIndexVar = Expression.Variable(ReflectorConsts.Int32Type, "readIndex"); 20 var indexVar = Expression.Variable(ReflectorConsts.Int32Type, "index"); 21 var forBreakLabel = Expression.Label("forBreak"); 22 var assignIndexVar = Expression.Assign(indexVar, Expression.Constant(0)); 23 var listType = ReflectorConsts.ListType.MakeGenericType(type); 24 var listVar = Expression.Variable(listType, "list"); 25 expressions.Add(Expression.Assign(listVar, Expression.New(listType))); 26 expressions.Add(Expression.Assign(readerVar, readerExp)); 27 expressions.Add(assignIndexVar); 28 var assignFieldCountVar = Expression.Assign(fieldCountVar, 29 Expression.MakeMemberAccess(readerVar, ReflectorConsts.FieldCountOfIDataReader) 30 ); 31 expressions.Add(assignFieldCountVar); 32 var readNameExp = Expression.Call(readerVar, ReflectorConsts.GetOrdinalOfIDataReader, Expression.ArrayIndex(propertyNameArr, indexVar)); 33 var initIndexArray = Expression.Assign(indexArrVar, Expression.NewArrayBounds(ReflectorConsts.Int32Type, Expression.Constant(fieldCount))); 34 var initPropertyArrayExpressions = new List<Expression>(); 35 for (int i = 0; i < fieldCount; i++) 36 { 37 initPropertyArrayExpressions.Add(Expression.Constant(reader.GetName(i))); 38 } 39 var initPropertyArray = Expression.Assign(propertyNameArr, Expression.NewArrayInit(ReflectorConsts.StringType, initPropertyArrayExpressions)); 40 var assignIndexArrayVar = Expression.Assign(Expression.ArrayAccess(indexArrVar, indexVar), readNameExp); 41 expressions.Add(initPropertyArray); 42 expressions.Add(initIndexArray); 43 expressions.Add(Expression.Loop( 44 Expression.IfThenElse( 45 Expression.LessThan(indexVar, fieldCountVar), 46 Expression.Block( 47 assignIndexArrayVar, 48 Expression.Assign( 49 indexVar, 50 Expression.Add(indexVar, Expression.Constant(1)) 51 ) 52 ), 53 Expression.Break(forBreakLabel) 54 ), 55 forBreakLabel 56 )); 57 Expression body = null; 58 DataReaderGetMethodSwitcher switcher = null; 59 var labelTarget = Expression.Label(type, "return"); 60 var paramterExpressions = new List<ParameterExpression>(); 61 var setEntityExpressions = new List<Expression>(); 62 if (TypeHelper.IsCompilerGenerated(type)) 63 { 64 var constructor = type.GetConstructors().FirstOrDefault(); 65 if (constructor == null) 66 { 67 throw new ArgumentException("类型" + type.FullName + "未找到构造方法"); 68 } 69 var parameters = constructor.GetParameters(); 70 var expressionParams = new List<ParameterExpression>(); 71 for (int i = 0; i < fieldCount; i++) 72 { 73 var parameter = parameters[i]; 74 var parameterVar = Expression.Variable(parameter.ParameterType, parameter.Name); 75 var parameterType = TypeHelper.GetUnderlyingType(parameter.ParameterType); 76 switcher = new DataReaderGetMethodSwitcher(parameterType, readIndexVar, readerVar); 77 switcher.Process(); 78 var rightExp = (Expression)switcher.Result; 79 if (TypeHelper.IsNullableType(parameter.ParameterType)) 80 { 81 rightExp = Expression.Convert(rightExp, parameter.ParameterType); 82 } 83 var isNullExp = Expression.Call(readerExp, ReflectorConsts.IsDBNullfIDataReader, readIndexVar); 84 var ifExp = Expression.IfThenElse(isNullExp, Expression.Assign(parameterVar, Expression.Default(parameter.ParameterType)), Expression.Assign(parameterVar, rightExp)); 85 var exps = new List<Expression>(); 86 setEntityExpressions.Add( 87 Expression.Assign( 88 readIndexVar, 89 Expression.ArrayIndex(indexArrVar, Expression.Constant(i)) 90 ) 91 ); 92 setEntityExpressions.Add(ifExp); 93 expressionParams.Add(parameterVar); 94 } 95 setEntityExpressions.Add(Expression.Assign(objVar, Expression.New(constructor, expressionParams))); 96 paramterExpressions.AddRange(expressionParams); 97 paramterExpressions.Add(readerVar); 98 paramterExpressions.Add(listVar); 99 } 100 else 101 { 102 var newExp = Expression.New(type); 103 setEntityExpressions.Add(Expression.Assign(objVar, newExp)); 104 for (int i = 0; i < fieldCount; i++) 105 { 106 var propertyName = reader.GetName(i); 107 var property = properties.Get(propertyName); 108 if (property == null) 109 { 110 continue; 111 } 112 var propertyAssignExpressions = new List<Expression>(); 113 var propertyExp = Expression.Property(objVar, property); 114 var propertyType = TypeHelper.GetUnderlyingType(property.PropertyType); 115 Expression rightExp = null; 116 switcher = new DataReaderGetMethodSwitcher(propertyType, readIndexVar, readerVar); 117 switcher.Process(); 118 rightExp = (Expression)switcher.Result; 119 if (TypeHelper.IsNullableType(property.PropertyType)) 120 { 121 rightExp = Expression.Convert(rightExp, property.PropertyType); 122 } 123 setEntityExpressions.Add( 124 Expression.Assign( 125 readIndexVar, 126 Expression.ArrayIndex(indexArrVar, Expression.Constant(i)) 127 ) 128 ); 129 var ifExp = Expression.IfThen( 130 Expression.Not( 131 Expression.Call(readerExp, ReflectorConsts.IsDBNullfIDataReader, readIndexVar) 132 ), 133 Expression.Assign(propertyExp, rightExp) 134 ); 135 setEntityExpressions.Add(ifExp); 136 } 137 paramterExpressions.Add(listVar); 138 paramterExpressions.Add(readerVar); 139 } 140 paramterExpressions.Add(indexVar); 141 paramterExpressions.Add(propertyNameArr); 142 paramterExpressions.Add(fieldCountVar); 143 paramterExpressions.Add(indexArrVar); 144 paramterExpressions.Add(readIndexVar); 145 //expressions.Add(Expression.Call( 146 // null, 147 // typeof(MessageBox).GetMethod("Show", new Type[] { ReflectorConsts.StringType }), 148 // Expression.Call( 149 // null, 150 // ReflectorConsts.ConvertToStringMethod, 151 // Expression.Convert( 152 // Expression.ArrayIndex(indexArrVar, Expression.Constant(1)), 153 // ReflectorConsts.ObjectType) 154 // ))); 155 setEntityExpressions.Add(Expression.Call(listVar, listType.GetMethods().FirstOrDefault(x => x.Name == "Add"), objVar)); 156 expressions.Add( 157 Expression.Loop( 158 Expression.Block( 159 Expression.IfThenElse( 160 Expression.Call(readerVar, ReflectorConsts.ReadOfIDataReader), 161 Expression.Block(new[] { objVar }, setEntityExpressions), 162 Expression.Break(labelTarget, Expression.Default(type)) 163 ) 164 ), 165 labelTarget 166 )); 167 expressions.Add(listVar); 168 body = Expression.Block( 169 paramterExpressions, 170 expressions 171 ); 172 func = Expression.Lambda<Func<IDataReader, IList>>(body, readerExp).Compile(); 173 _dataReader2ListCahce.Add(type, func); 174 } 175 } 176 } 177 return func; 178 }