上周五下班前,在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 = "[email protected]"
};
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