性能:派生自泛型的类型
本文关键字:类型 泛型 派生 性能 | 更新日期: 2023-09-27 18:32:26
我遇到了一个我不太理解的性能问题。我知道如何解决它,但我不明白为什么会这样。这只是为了好玩!
让我们谈谈代码。我尽可能地简化了代码以重现该问题。
假设我们有一个泛型类。它内部有一个空列表,并在构造函数中对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);
}
}
要点的完整列表
更新:
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 方法 - 您将像以前一样受到性能损失。
我猜有一些与堆栈对齐或方法表大小有关的东西。
附言您可以替换
Enumerable.Empty<T>();
// with
var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
还是同样的错误。
经过一些实验,我发现当 T 是类类型时,Enumerable.Empty<T>
总是很慢;如果是值类型,它更快,但取决于结构大小。我测试了对象,字符串,整数,PointF,矩形F,日期时间,Guid。
看看它是如何实现的,我尝试了不同的替代方案,并找到了一些快速工作的替代方案。
Enumerable.Empty<T>
依赖于内部类EmptyEnumerable<TElement>
的Instance
静态属性。
该属性执行一些小操作:
- 检查私有静态易失性字段是否为空。
- 将空数组分配给字段一次(仅当为空时)。
- 返回字段的值。
然后,Enumerable.Empty<T>
真正做的只是返回一个空的 T 数组。
尝试不同的方法,我发现缓慢是由属性和挥发性修饰符引起的。
采用初始化为 T[0] 的静态字段,而不是像 Enumerable.Empty<T>
public static readonly T[] EmptyArray = new T[0];
问题消失了。请注意,只读修饰符不是行列式。使用易失性声明相同的静态字段或通过属性访问会导致问题。
问候丹尼尔。
似乎存在 CLR 优化器问题。关闭"生成"选项卡上的"优化代码",然后尝试再次运行测试。