【问题标题】:Performance: type derived from generic性能:从泛型派生的类型
【发布时间】:2015-01-26 09:00:19
【问题描述】:

我遇到了一个我不太理解的性能问题。我知道如何解决它,但我不明白为什么会这样。这只是为了好玩!
让我们谈谈代码。我尽可能地简化了代码以重现该问题。
假设我们有一个泛型类。它内部有一个空列表,并在构造函数中使用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&lt;T&gt;.Run 方法的性能。从派生类型访问比从基类访问慢 4 倍。我不明白为什么会这样。在 Release 模式下编译,结果与 warm up 相同。它仅发生在 .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

【问题讨论】:

  • 各位,你们试过运行代码吗?它处于释放模式,结果与热身相同。即使你互换线路。
  • 如果你从构造函数中移除Enumerable.Empty&lt;T&gt;,这两个类的表现是一样的。我不知道为什么,但是...
  • @DavidLibido 你错了。我们在非常实际的应用中注意到了这一点。真正的代码要复杂得多,并且实现了 Aho Corasick 算法。而且我不是在问测试代码的商业价值。我在问为什么会这样?!
  • 如果确实(正如下面的一些答案所暗示的那样)您在 JIT 中遇到了错误,那么 Microsoft Connect 就是打开错误的地方。您不会在 StackOverflow 上得到答案,告诉您为什么存在错误或错误的确切形状。如果您认为存在代码生成问题,请在 Connect 上打开一个错误。
  • 向 Microsoft Connect connect.microsoft.com/VisualStudio/feedback/details/1041830/…提交了一个错误

标签: c# .net performance generics clr


【解决方案1】:

更新:
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>);

还是同样的错误。

【讨论】:

  • 虽然这不能回答问题,但它确实会引出路径,暗示某处存在错误。有趣的是,Run2Run3 是否包含任何代码也没关系。
  • 再来一次,我不是要求how 修复它。我知道该怎么做。我问why 它发生了。
【解决方案2】:

经过一番实验,发现Enumerable.Empty&lt;T&gt;在T为class类型时总是很慢;如果它是一个值类型,它会更快,但取决于结构大小。 我测试了对象、字符串、int、PointF、RectangleF、DateTime、Guid。

看看它是如何实现的,我尝试了不同的替代方案,并找到了一些运行速度很快的替代方案。

Enumerable.Empty&lt;T&gt; 依赖于内部类EmptyEnumerable&lt;TElement&gt;Instance 静态属性

该属性只做一些小事:

  • 检查私有静态 volatile 字段是否为空。
  • 将一个空数组分配给该字段一次(仅当为空时)。
  • 返回字段的值。

那么,Enumerable.Empty&lt;T&gt; 真正要做的只是返回一个空的 T 数组。

尝试不同的方法,我发现缓慢是由 both propertyvolatile 修饰符引起的。

采用初始化为 T[0] 的静态字段,而不是 Enumerable.Empty&lt;T&gt; 之类的

public static readonly T[] EmptyArray = new T[0];

问题消失了。 请注意,只读修饰符不是决定性的。 使用 volatile 声明或通过 property 访问相同的静态字段会导致问题。

问候, 丹尼尔。

【讨论】:

  • BaseClass ctr 中的 new T(); 而不是 Enumerable.Empty&lt;T&gt; 怎么样?也很慢!
  • 不,它很快:在 BaseClass 构造函数中直接实例化空数组,它(几乎)与使用 EmptyArray 字段一样快。
【解决方案3】:

似乎存在 CLR 优化器问题。关闭“构建”选项卡上的“优化代码”并尝试再次运行测试。

【讨论】:

  • 结果是一样的!
猜你喜欢
  • 1970-01-01
  • 2014-03-22
  • 2011-10-09
  • 2011-05-24
  • 2010-10-22
  • 2010-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多