我遇到过一个我无法理解的性能问题。我知道如何解决它,但我不明白为什么会这样。这只是为了好玩!
让我们的谈话代码。我尽可能地简化了代码以重现问题
假设我们有一个泛型类。它内部有一个空列表,并在构造函数中执行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
方法的性能。从派生类型访问比从基类访问慢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);
}
}
完整列表on gist
答案 0 :(得分:31)
<强>更新强>
CLR团队在Microsoft Connect
它与共享泛型代码中的字典查找有关。运行时和JIT中的启发式方法不适用于此特定测试。我们将看看可以采取哪些措施。
与此同时,您可以通过向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方法 - 你将会像以前一样受到性能损失。
我想是有一些与堆栈对齐或方法表大小相关的东西。
P.S。你可以替换
Enumerable.Empty<T>();
// with
var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
仍然是同样的错误。
答案 1 :(得分:0)
经过一些实验,我发现当T是类类型时Enumerable.Empty<T>
总是很慢;如果它是值类型,它更快,但取决于结构大小。
我测试了对象,字符串,int,PointF,RectangleF,DateTime,Guid。
看看它是如何实现的,我尝试了不同的替代方案,并找到了一些可以快速实现的方法。
Enumerable.Empty<T>
依赖于内部课程EmptyEnumerable<TElement>
Instance
静态属性。
然后,Enumerable.Empty<T>
真正做的只是返回一个空的T数组。
尝试不同的方法,我发现缓慢是由 属性和 volatile 修饰符引起的。
采用初始化为T [0]而非Enumerable.Empty<T>
的静态字段,如
public static readonly T[] EmptyArray = new T[0];
问题消失了。
请注意,只读修饰符不是行列式。
使用 volatile 声明相同的静态字段或通过属性访问该字段会导致此问题。
此致 丹尼尔。
答案 2 :(得分:-2)
似乎存在CLR优化器问题。关闭&#34;优化代码&#34;在Build选项卡上,尝试再次运行测试。