性能:派生自通用的类型

Ale*_*tin 63 .net c# generics clr performance

我遇到过一个我无法理解的性能问题.我知道如何解决它,但我不明白为什么会这样.这只是为了好玩!
我们来谈谈代码.我尽可能地简化了代码以重现问题.
假设我们有一个泛型类.它内部有一个空列表,并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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们有一个空的派生类:

public class DerivedClass : BaseClass<object>
{
}
Run Code Online (Sandbox Code Playgroud)

让我们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);
    }
}
Run Code Online (Sandbox Code Playgroud)

完整列出的要点

Sin*_*nix 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;
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

请注意,不会从任何地方调用Run2()/ Run3().但是如果你注释掉Run2或Run3方法 - 你会像以前一样受到性能损失.

我想,有一些与堆栈对齐或方法表大小有关的东西.

PS你可以更换

 Enumerable.Empty<T>();
 // with
 var x = new Func<IEnumerable<T>>(Enumerable.Empty<T>);
Run Code Online (Sandbox Code Playgroud)

还是一样的bug.

  • 虽然这没有回答这个问题,但它确实导致了某个地方出现错误的路径.有趣的是,如果`Run2`和`Run3`包含任何代码也没关系. (5认同)
  • 再一次,我不是在问'如何'来修复它.我知道该怎么做.我问'为什么'它发生了. (4认同)