性能:从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。