【问题标题】:c# Generics - unexpected performance resultsc# 泛型 - 意外的性能结果
【发布时间】:2013-12-31 13:05:37
【问题描述】:

我相信微软声称泛型在处理引用类型时比使用普通多态更快。但是,以下简单测试(64 位 VS2012)将表明并非如此。使用多态性,我通常会加快 10% 的秒表时间。我是否误解了结果?

public interface Base { Int64 Size { get; } }
public class Derived : Base { public Int64 Size { get { return 10; } } }

public class GenericProcessor<TT> where TT : Base
{
    private Int64 sum;
    public GenericProcessor(){ sum = 0; }
    public void process(TT o){ sum += o.Size; }
    public Int64 Sum { get { return sum; } }
}
public class PolymorphicProcessor
{
    private Int64 sum;
    public PolymorphicProcessor(){ sum = 0; }
    public void process(Base o){ sum += o.Size; }
    public Int64 Sum { get { return sum; } }
}
static void Main(string[] args)
{
    var generic_processor = new GenericProcessor<Derived>();
    var polymorphic_processor = new PolymorphicProcessor();
    Stopwatch sw = new Stopwatch();
    int N = 100000000;
    var derived = new Derived();

    sw.Start();
    for (int i = 0; i < N; ++i) generic_processor.process(derived);
    sw.Stop();
    Console.WriteLine("Sum ="+generic_processor.Sum + " Generic performance = " + sw.ElapsedMilliseconds + " millisec");

    sw.Restart();
    sw.Start();
    for (int i = 0; i < N; ++i) polymorphic_processor.process(derived);
    sw.Stop();
    Console.WriteLine("Sum ="+polymorphic_processor.Sum+ " Poly performance = " + sw.ElapsedMilliseconds + " millisec");

更令人惊讶(和令人困惑)的是,如果我将类型转换添加到处理器的多态版本,如下所示,它的运行速度始终比泛型版本快约 20%。

        public void process(Base trade)
        {
            sum += ((Derived)trade).Size; // cast not needed - just an experiment
        }

这里发生了什么?我知道泛型在处理原始类型时可以帮助避免昂贵的装箱和拆箱,但我在这里严格处理引用类型。

【问题讨论】:

  • 你能发布一个关于泛型对于引用类型更快的说法的引用吗?
  • 使用structs 的泛型更快,因为它们允许您避免装箱/拆箱(您必须将参数声明为object 以使其与int 和@ 一起使用987654326@ 例如)。我对泛型使用引用类型更快一无所知。你能提供任何有关这方面的资料吗?
  • 对微优化进行基准测试是非常困难的。在尝试对此类操作进行基准测试时,您可能会遇到 很多 的陷阱。通常,除非您对此非常有经验,否则您的结果几乎完全代表您对代码进行基准测试的缺陷的可能性非常高,而不是您正在测量的代码的性能差异。当您正在测量的代码耗时足以显着超过基准测试框架时,此类错误不会使您的结果无效。
  • 愚蠢的基准是愚蠢的。
  • 我不熟悉这种说法,测试一个您无法清楚识别的说法似乎很奇怪。我的怀疑是,在历史上的某个地方,实际的主张被误记或抄写了。例如,最初的主张可能是,如果您有struct S&lt;T&gt;{public T item;},那么访问数组S&lt;C&gt;[] 的成员比访问C[] 的数组(如果C 是未密封的类类型)更快。这是真实的;如果元素类型是结构,抖动可以消除对不安全数组协方差的检查。

标签: c# performance generics


【解决方案1】:

使用 Ctrl-F5(不带调试器)在 .NET 4.5 x64 下执行测试。 N也增加了10倍。这样,无论测试的顺序如何,结果都能可靠地重现。


使用 ref 类型的泛型,您仍然可以获得相同的 vtable/interface 查找,因为所有 ref 类型只有一个编译方法。 Derived 没有专业化。在此基础上执行callvirt的性能应该是一样的。

此外,泛型方法有一个隐藏的方法参数typeof(T)(因为这允许您在泛型代码中实际编写typeof(T)!)。这是解释为什么通用版本更慢的额外开销。

为什么强制转换比接口调用快?演员表只是一个指针比较和一个完全可预测的分支。转换后,对象的具体类型已知,从而可以更快地调用。

if (trade.GetType() != typeof(Derived)) throw;
Derived.Size(trade); //calling directly the concrete method, potentially inlining it

所有这些都是有根据的猜测。通过查看反汇编来验证。

如果你添加演员,你会得到以下程序集:

我的组装技能不足以完全解码。然而:

  1. 16加载Derived的vtable ptr

  2. 22 和 #25 是测试 vtable 的分支。这样就完成了演员表。

  3. 在 #32 处,演员表已完成。请注意,在这一点之后没有调用。 Size 已内联。
  4. 35 a lea 实现加法

  5. 39 存储回this.sum

同样的技巧适用于通用版本 (((Derived)(Base)o).Size)。

【讨论】:

  • 我怀疑但未验证的内容。调用是完全内联的。不过,您似乎也在 x64 上运行.. 这也可能是我们都看到差异的原因.. 鉴于 x86 和 x64 的抖动不一样。
【解决方案2】:

我相信 Servy 是正确的,这是您的测试有问题。我颠倒了测试的顺序(只是预感):

internal class Program
{
    public interface Base
    {
        Int64 Size { get; }
    }

    public class Derived : Base
    {
        public Int64 Size
        {
            get
            {
                return 10;
            }
        }
    }

    public class GenericProcessor<TT>
        where TT : Base
    {
        private Int64 sum;

        public GenericProcessor()
        {
            sum = 0;
        }

        public void process(TT o)
        {
            sum += o.Size;
        }

        public Int64 Sum
        {
            get
            {
                return sum;
            }
        }
    }

    public class PolymorphicProcessor
    {
        private Int64 sum;

        public PolymorphicProcessor()
        {
            sum = 0;
        }

        public void process(Base o)
        {
            sum += o.Size;
        }

        public Int64 Sum
        {
            get
            {
                return sum;
            }
        }
    }

    private static void Main(string[] args)
    {
        var generic_processor = new GenericProcessor<Derived>();
        var polymorphic_processor = new PolymorphicProcessor();
        Stopwatch sw = new Stopwatch();
        int N = 100000000;
        var derived = new Derived();
        sw.Start();
        for (int i = 0; i < N; ++i) polymorphic_processor.process(derived);
        sw.Stop();
        Console.WriteLine(
            "Sum =" + polymorphic_processor.Sum + " Poly performance = " + sw.ElapsedMilliseconds + " millisec");


        sw.Restart();
        sw.Start();
        for (int i = 0; i < N; ++i) generic_processor.process(derived);
        sw.Stop();
        Console.WriteLine(
            "Sum =" + generic_processor.Sum + " Generic performance = " + sw.ElapsedMilliseconds + " millisec");

        Console.Read();
    }
    }

在这种情况下,多态在我的测试中比较慢。这表明第一次测试明显慢于第二次测试。它可能是第一次加载类,抢占,谁知道......

我只想指出,我并不是说泛型更快或一样快。我只是想证明这些类型的测试不会以某种方式证明一个案例。

【讨论】:

  • 你能总结一下你改变了什么吗?
  • 以相反的顺序运行测试。
  • 在 .NET 4.5、x64 上将迭代次数增加 10 倍泛型对我来说总是更慢。
  • 我刚才增加了10倍,得到的结果是多态变慢了。我并不是要争辩说泛型更快。我只是在证明这个测试并不能证明多态更快。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-10
  • 1970-01-01
  • 2021-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多