一次性能优化最佳实践
上周五下班前,在Repository兄测试NLiteMapper和EmitMapper的文章中,发现了令我跌破眼镜的性能悬殊对比12283ms : 7ms
。真不可思议,与是便把EmitMapper的源代码和OOMapper 的源代码一起下载下来,以Release模式的方式做一个公平对比。测试代码
仍然沿用Repository兄的,代码如下:
public class SimpleClassFrom { public long ID { get; set; } public string Name { get; set; } public int Age { get; set; } public string Email { get; set; } } public class SimpleClassTo { public long ID { get; set; } public string Name { get; set; } public int Age { get; set; } public string Email { get; set; } } [Test] public void NLiteMapper_EmitMapper_Compare() { //consturct data List<SimpleClassFrom> fromCollection = new List<SimpleClassFrom>(); int count = 10000; for (int i = 0; i < count; i++) { SimpleClassFrom from = new SimpleClassFrom { ID = 123456, Name = "test", Age = 30, Email = "hhhhhhhhhhhhhhh@hotmail.com" }; fromCollection.Add(from); } //test nlite mapper var a = NLite.Mapper.CreateMapper<SimpleClassFrom, SimpleClassTo>(); Stopwatch sw = Stopwatch.StartNew(); sw.Start(); for (int i = 0; i < count; i++) { a.Map(fromCollection[i]); } sw.Stop(); Console.WriteLine("nlitemapper elapsed:{0}ms", sw.ElapsedMilliseconds); //test emit mapper var mapper = ObjectMapperManager.DefaultInstance .GetMapper<SimpleClassFrom, SimpleClassTo>(); sw = Stopwatch.StartNew(); for (int i = 0; i < count; i++) { mapper.Map(fromCollection[i]); } sw.Stop(); Console.WriteLine("emitmapper elapsed:{0}ms", sw.ElapsedMilliseconds); Console.Read(); }
然后进行了2次测试,取第二次的结果:nlitemapper elapsed:15475ms,emitmapper elapsed:1ms。这个结果还不如Repository兄的测试结果,但是也是意料之中。于是开始分析代码。EmitMapper是在Create 对象映射器的时候已经把对象映射的元数据通过Emit的方式进行了构建,而NLiteMapper创建对象映射器的时候并没有立即创建对象映射元数据,而是在第一次调用映射的时候开始创建,代码如下:
Lazy<List<MemberMapping>> mappings; internal Lazy<List<MemberMapping>> Mappings { get { if (mappings != null) return mappings; mappings = new Lazy<List<MemberMapping>>(() => { var fromMembers = SourceMembers.Value; var items = new List<MemberMapping>(); foreach (var toMember in DestinationMembers.Value) { string fromMemberPath = null; MemberInfo fromMember = fromMembers.FirstOrDefault(m => IsMatchMember(m, toMember, ref fromMemberPath)); if (fromMember == null) continue; if (!string.IsNullOrEmpty(fromMemberPath)) fromMemberPath = fromMemberPath.Remove(0, 1); items.Add(new MemberMapping { FromMemberPath = fromMemberPath, From = GetMappingItem(fromMember, true), To = GetMappingItem(toMember, false) }); } return items; }); return mappings; } }
于是为了公平性,先把Lazy创建元数据的地方都去掉。然后再进行测试:nlitemapper elapsed:15290ms,emitmapper elapsed:1ms,性能稍微提供了一丁点。
进入NLiteMapper的ClassMapper的Map源代码:
public override void Map(ref object from, ref object to) { var mappings = _Info.Mappings; var mappingCount = mappings.Count; if (_Info.memberMappings.Count == 0 && mappingCount == 0) return; if(to == null) to = ObjectCreator.Create(_Info.To); for(int i=0;i<mappingCount;i++) { var item = mappings[i]; try { object value = GetSourceMemberValue(ref from, ref item);//通过映射项元数据获取source 对象的一个属性值 var key = item.From.Type.FullName + "->" + item.To.Type.FullName; if (_Info.CanUsingConverter(key))//是否自定义了转换器 value = _Info.converters[key].DynamicInvoke(value); else //否则把Source对象的一个属性值转化为和Target对象的属性类型相匹配的值 value = Mapper.Map(value, item.From.Type, item.To.Type); item.To.SetMember(to, value);// 设置Target对象的成员值 } catch (Exception ex) { State.AddError( string.Format("{0}.{1} -> {2}.{3}" , item.From.Member.DeclaringType.Name , item.From.Name , item.To.Member.DeclaringType.Name , item.To.Name), ex.Message); } } FilterMembers(ref from, ref to); }
第一步:移除Try..Catch块的测试结果:nlitemapper elapsed:15489ms,emitmapper elapsed:1ms,几乎没有影响,于是恢复Try...Catch块。
第二步:定位核心代码
object value = GetSourceMemberValue(ref from, ref item);//通过映射项元数据获取source 对象的一个属性值 var key = item.From.Type.FullName + "->" + item.To.Type.FullName; if (_Info.CanUsingConverter(key))//是否自定义了转换器 value = _Info.converters[key].DynamicInvoke(value); else //否则把Source对象的一个属性值转化为和Target对象的属性类型相匹配的值 value = Mapper.Map(value, item.From.Type, item.To.Type); item.To.SetMember(to, value);// 设置Target对象的成员值
注释掉核心代码,并做测试:nlitemapper elapsed:39ms,emitmapper elapsed:1ms。测试结果相当激动人心,说明性能的
关键代码段就在上面那几句代码中,于是恢复掉注释,继续研究。
第三步:一眼找出这句代码的问题 var key = item.From.Type.FullName + "->" + item.To.Type.FullName,这个Key的获取应
该可以被缓存起来保存到元数据中去,不应该每次都重新计算,于是修改代码并测试:nlitemapper elapsed:15462ms,emitmapper
elapsed:1ms,几乎没有影响
第四步:定位到item.To.SetMember 源代码:
[DebuggerDisplay("{Name}")] struct MappingItem { public Type Type; public MemberInfo Member; private Setter setMember; public Setter SetMember { get { if (setMember == null) setMember = Member.GetSetter(); return setMember; } } private Getter getMember; public Getter GetMember { get { if (getMember == null) getMember = Member.GetGetter(); return getMember; } } public string Name { get { return string.Format("[{0} {1}]", Type.FullName, Member.Name); } } public override string ToString() { return Name; } }
Setter/Getter Delegate( 通过Emit创建的)是Lazy创建的,把它也改为立即创建为了和EmitMapper更公平比较,于是进行相关改造,并测试:nlitemapper elapsed:2011ms,emitmapper elapsed:1ms。 性能一下子从15000ms多一下缩减到2000ms多一点,等于性能提升了7倍,但是离39ms 还差很多。
第五步:分析 这句 代码 object value = GetSourceMemberValue(ref from, ref item); 获取 发现两个参数都用了关键字ref, 第一个参数是source 对象,不管它,第二个参数item 的类型是一个结构MemberMapping,所以为了提升性能就用了 ref 关键字来减少调用函数栈的开销。随即又想,我直接用Class就可以不用ref关键子了,于是改成Class类型顺手测试了一下,结果非常吃惊:nlitemapper elapsed:763ms,emitmapper elapsed:3ms。关于ref struct 体参数变量和Class参数变量的性能更详细的测试,准备以后再详细研究。
第六步: 把最后一句代码属性赋值代码注释掉:item.To.SetMember(to, value);看看测试结果:nlitemapper elapsed:773ms,emitmapper elapsed:1ms 结果也在意料之中,NLiteMapper的 Emit也不是盖的,
几乎没有任何反射开销,呵呵。
第七部: 根据测试路径,性能瓶颈应该是value = Mapper.Map(value, item.From.Type, item.To.Type) 这句代码了,把这句代码注释掉
,看看具体的验证结果:nlitemapper elapsed:42ms,emitmapper elapsed:1ms,果不其然就是这里。这句格式化属性值的代码,是否真的有
必要格式化?仔细想了想,在大部分属性映射中,属性类型都是一样的,不需要格式化,这符合典型的2:8原则,这里为了代码的一致性而忽视了2:8
原则,这是很多爱洁癖程序员(爱代码重构的程序员)容易犯的错误,随后加了一句简单的ifelse判断。性能测试结果如预期的一样:nlitemapper elapsed:42ms,emitmapper elapsed:1ms。
最后又优化了其它的几个小方面代码性能提升到:20ms左右,这个结果和emitMapper 的结果相差不多了,但是我会继续优化使之差距更小,当
然NLiteMapper中有很多其它的Mapper,也需要进行详细的性能检测,过一段时间再发布一个版本。
总结:
1. 函数参数中尽量不要用Struct参数或者Ref Struct参数
2. 在代码整洁和简洁化的同时,不要忘记了2:8原则
最后附加上AutoMapper,NLiteMapper,EmitMapper的测试结果:
nlitemapper elapsed:18ms emitmapper elapsed:1ms autoMapper elapsed:842ms