性能:派生自泛型的类型

本文关键字:类型 泛型 派生 性能 | 更新日期: 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 优化器问题。关闭"生成"选项卡上的"优化代码",然后尝试再次运行测试。