性能:从generics派生的types
我遇到了一个我不能理解的性能问题。 我知道如何解决,但我不明白为什么会发生。 这只是为了好玩!
让我们来谈谈代码。 我尽可能简化代码来重现问题。
假设我们有一个generics类。 它里面有一个空的列表,在构造函数中用T
做一些事情。 它有Run
方法调用列表上的IEnumerable<T>
方法,例如Any()
。
public class BaseClass<T> { private List<T> _list = new List<T>(); public BaseClass() { Enumerable.Empty<T>(); // or Enumerable.Repeat(new T(), 10); // or even new T(); // or foreach (var item in _list) {} } public void Run() { for (var i = 0; i < 8000000; i++) { if (_list.Any()) // or if (_list.Count() > 0) // or if (_list.FirstOrDefault() != null) // or if (_list.SingleOrDefault() != null) // or other IEnumerable<T> method { return; } } } }
那么我们有一个空的派生类:
public class DerivedClass : BaseClass<object> { }
我们来测量从两个类运行ClassBase<T>.Run
方法的性能。 来自派生types的访问速度比基类要慢4倍。 我不明白为什么会发生这种情况。 在发行模式下编译,结果与预热相同。 它只发生在.NET 4.5上。
public class Program { public static void Main() { Measure(new DerivedClass()); Measure(new BaseClass<object>()); } private static void Measure(BaseClass<object> baseClass) { var sw = Stopwatch.StartNew(); baseClass.Run(); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); } }
完整列表的要点
更新:
Microsoft Connect上的CLR团队有一个答案
它与共享的generics代码中的字典查找有关。 运行时启发式和JIT对于这个特定的testing并不适用。 我们将看看可以做些什么。
与此同时,您可以通过向BaseClass添加两个虚拟方法(甚至不需要调用)来解决此问题。 它会导致启发式工作,如预期的那样。
原版的:
这是JIT失败。
可以通过这个疯狂的事情来修复:
public class BaseClass<T> { private List<T> _list = new List<T>(); public BaseClass() { Enumerable.Empty<T>(); // or Enumerable.Repeat(new T(), 10); // or even new T(); // or foreach (var item in _list) {} } public void Run() { for (var i = 0; i < 8000000; i++) { if (_list.Any()) { return; } } } public void Run2() { for (var i = 0; i < 8000000; i++) { if (_list.Any()) { return; } } } public void Run3() { for (var i = 0; i < 8000000; i++) { if (_list.Any()) { return; } } } }
请注意Run2()/ Run3() 不从任何地方调用。 但是如果你注释掉Run2或者Run3的方法 – 你会像以前那样得到性能上的损失。
有一些相关的堆栈alignment或方法表的大小,我猜。
PS您可以replace
Enumerable.Empty<T>(); // with var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
仍然是相同的错误。
经过一番实验,我发现Enumerable.Empty<T>
在T是类types时总是很慢; 如果它是一个值types更快,但依赖于结构的大小。 我testing了对象,string,int,PointF,RectangleF,DateTime,Guid。
看看它是如何实施的,我尝试了不同的select,并发现一些工作很快。
Enumerable.Empty<T>
依赖于内部类EmptyEnumerable<TElement>
的Instance
静态属性 。
该属性做一些事情:
- 检查私有静态易失性字段是否为空。
- 将一个空数组分配给该字段一次(仅在空的情况下)。
- 返回字段的值。
那么, Enumerable.Empty<T>
实际上只是返回一个T的空数组。
尝试不同的方法,我发现缓慢是由属性和挥发性修饰词造成的。
采用静态字段初始化为T [0]而不是Enumerable.Empty<T>
like
public static readonly T[] EmptyArray = new T[0];
问题消失了。 请注意,只读修饰符不是行列式。 使用volatile声明或通过属性访问相同的静态字段会导致此问题。
问候,Daniele。
看来有一个CLR优化器问题。 closures“生成”选项卡上的“优化代码”,然后尝试再次运行testing。
- 为什么Math.Ceiling返回double?
- 无法加载文件或程序集“System.Net.Http.Formatting”或其依赖项之一。 该系统找不到指定的path
- resharperdebugging第三方反编译的DLL
- WPF CreateBitmapSourceFromHBitmap()内存泄漏
- 在“释放”模式下的“堆栈跟踪”中显示.NET程序集的行号
- 我怎样才能使xmlserializer只序列化纯XML?
- 你如何得到一个multidimensional array的宽度和高度?
- 用Enumerable.Range与传统for循环的foreach思考
- 如何将System.Type转换为可空的版本?