性能:派生自通用的类型

时间:2014-11-27 17:27:15

标签: c# .net performance generics clr

我遇到过一个我无法理解的性能问题。我知道如何解决它,但我不明白为什么会这样。这只是为了好玩! 让我们的谈话代码。我尽可能地简化了代码以重现问题 假设我们有一个泛型类。它内部有一个空列表,并在构造函数中执行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

3 个答案:

答案 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选项卡上,尝试再次运行测试。