ComparerFactory - 运行时实现编译时性能






4.19/5 (8投票s)
一个关于如何在运行时使用反射来实现编译时性能的例子。

引言
反射速度慢已经变成了一种常识。为了证明这个事实,本文使用了WPF ListCollectionView
的默认排序行为。在对数千个条目进行排序之前,它工作良好。本文描述的技术展示了如何使用反射在运行时生成类,同时避免性能损失。
背景
我的最初任务是在ComboBox
中对项目列表进行排序(都在WPF中完成,并且有一些优秀的文章展示了如何做到这一点)。我很容易地想到,使用CollectionView
并在其上使用SortDescription
private void Sort(string sortBy, ListSortDirection direction){
ICollectionView dataView = CollectionViewSource.GetDefaultView(lv.ItemsSource);
dataView.SortDescriptions.Clear();
var sd = new SortDescription("Name", direction);
dataView.SortDescriptions.Add(sd);
dataView.Refresh();
}
没那么快。字面上说,不是快,而是相当慢。该列表包含 2 万个项目。是的,我知道两万条记录在ComboBox
中看起来不太好,无论是否排序,但这不是我的问题。我的问题是排序,我必须发明一些东西。反射是造成速度慢的原因,以下解决方案效果很好。创建一个比较器类
public class CustomComparer: IComparer {
public int Compare(object x, object y) {
return ((ComparedClass)x).Name.CompareTo(
((ComparedClass)y).Name);
}
}
然后使用ListCollectionView
的CustomSort
属性,而不是上面的SortDescriptions
dataView.CustomSort = new CustomComparer();
这解决了性能问题,但我的真正任务是让它适用于任何ComboBox
以及列表中任何类型的对象。我太懒了,每次都写一个自定义的比较器类,所以我写了一个工厂来为我生成它们。
运行时编译
常见的反射用法是获取类定义的元数据并在运行时使用它。但真正的魔力始于你在运行时获取该元数据并在那里构建类。这些类的行为将像在编译时创建的一样快。诀窍是在运行时使用 Reflection.Emit 生成代码。
这是可能的,因为 .NET 程序集是 IL(中间语言)指令的二进制代码。您可以将 IL 视为 .NET 的一种汇编语言。当程序集被执行 JIT(即时编译)时,编译器将 IL 转换为特定于机器的代码。通过System.Reflection.Emit
命名空间,我们可以使用 IL 代码在运行时生成动态程序集,JIT 编译器不会注意到差异。让我们动态地创建程序集
dynamicName = new AssemblyName("dynamicAssemblyName");
dynamicAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
dynamicName,
AssemblyBuilderAccess.Run);
dynamicModuleBuilder = dynamicAssemblyBuilder.DefineDynamicModule(dynamicName.Name);
这是创建类的先决条件。然后我们需要一个TypeBuilder
实例
TypeBuilder tb = dynamicModuleBuilder.DefineType(
typeName,
TypeAttributes.Public | TypeAttributes.Class,
null, new Type[] { typeof(IComparer) });
表明我们将要实现IComparer
接口
tb.AddInterfaceImplementation(typeof(IComparer));
MethodBuilder methodBuilder = tb.DefineMethod("Compare",
MethodAttributes.Public | MethodAttributes.Virtual,
typeof(int),
new Type[] { typeof(object), typeof(object) });
ILGenerator methodILGenerator = methodBuilder.GetILGenerator();
现在我们可以使用methodILGenerator
来生成方法体。但是该方法在 IL 中会是什么样子呢?好问题,很高兴你问了。有一个标准和来自微软的进一步阅读,将帮助我们完成这项任务。
但是等等,还有更简单的方法。C# 编译器为我们完成了翻译成 IL 的任务。我们所需要的只是一个反汇编器来将该代码翻译回人类可读的形式。ildasm(作为免费 Windows SDK 下载的一部分提供)就是这样做的。以下是 ildasm 解读的方法定义的样子
.method public hidebysig newslot virtual final
instance int32 Compare(object x,
object y) cil managed
{
// Code size 28 (0x1c)
.maxstack 8
IL_0000: ldarg.1
IL_0001: castclass tevton.ComparerFactoryTests.ComparerFactoryTest/CustomComparer
IL_0006: callvirt instance string
tevton.ComparerFactoryTests.ComparerFactoryTest/CustomComparer::get_Name()
IL_000b: ldarg.2
IL_000c: castclass tevton.ComparerFactoryTests.ComparerFactoryTest/CustomComparer
IL_0011: callvirt instance string
tevton.ComparerFactoryTests.ComparerFactoryTest/CustomComparer::get_Name()
IL_0016: callvirt instance int32 [mscorlib]System.IComparable::CompareTo(object)
IL_001b: ret
} // end of method CustomComparer::Compare
经过一点努力和一些实验(值类型属性比较器看起来会有点不同),这可以转化为以下 C# 代码
var getAccessor = propertyInfo.GetAccessors()
.Where(mi => mi.Name.StartsWith("get_")).SingleOrDefault();
methodILGenerator.Emit(OpCodes.Ldarg_1);
methodILGenerator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
methodILGenerator.Emit(OpCodes.Callvirt, getAccessor);
if(propertyInfo.PropertyType.IsValueType) {
methodILGenerator.Emit(OpCodes.Box, propertyInfo.PropertyType);
}
methodILGenerator.Emit(OpCodes.Ldarg_2);
methodILGenerator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
methodILGenerator.Emit(OpCodes.Callvirt, getAccessor);
if(propertyInfo.PropertyType.IsValueType) {
methodILGenerator.Emit(OpCodes.Box, propertyInfo.PropertyType);
}
methodILGenerator.Emit(OpCodes.Callvirt, typeof(IComparable).GetMethod("CompareTo"));
methodILGenerator.Emit(OpCodes.Ret);
这处理了方法体。只需一个最后的润色 - 用构建的方法覆盖IComparer.Compare()
MethodInfo compareInfo = typeof(IComparer).GetMethod("Compare");
tb.DefineMethodOverride(methodBuilder, compareInfo);
Type t = tb.CreateType();
IComparer res = Activator.CreateInstance(t) as IComparer;
类实例就绪!我之前提到的工厂将所有这些动态比较器包装成一个方便的包。
Using the Code
每当您需要像上面所示的IComparer
类时,您可以从工厂获取一个实例
dataView.CustomSort =
ComparerFactory.Instance.GetComparer(typeof(ComparedClass), "Name");
就是这样,具有强类型类性能的所有反射(一种“后期绑定”)的好处。源代码带有一个演示项目和 NUnit 测试库,以使用法清晰明了。
请在家尝试。反射比我以前想象的要强大,但请注意存在一些缺点,例如调试动态代码的难度,使 GC 处置动态创建的类型以及如果您 IL 代码错误时出现零星异常的麻烦。
历史
- 2010 年 2 月 - 初始版本