【问题标题】:Why is this List<>.IndexOf code so much faster than the List[i] and manual compare?为什么这个 List<>.IndexOf 代码比 List[i] 和手动比较快得多?
【发布时间】:2010-09-07 21:46:07
【问题描述】:

我在这段代码上运行 AQTime,我发现 .IndexOf 占用了 16% 的时间,而另一段则接近 80%...它们似乎使用相同的 IsEqual 和其他例程。调用 116,000 次,插入 30,000 个项目。所有 List 对象都没有超过 200 个元素。 (我可能用错了 AQTime,我正在研究这个)

class PointD : IEquatable<PointD>
{
    public double X, Y, Z;

    bool IEquatable<PointD>.Equals(PointD other)
    {
        return ((X == other.X) && (Y == other.Y) && (Z == other.Z));
    }
}

class PerfTest
{
    readonly List<PointD> _pCoord3Points = new List<PointD>();
    public int NewPoints;
    public int TotalPoints;

    public PerfTest()
    {
        NewPoints = 0;
        TotalPoints = 0;
    }
    public int CheckPointIndexOf(PointD pt)
    {
        int retIndex = _pCoord3Points.IndexOf(pt);
        if (retIndex < 0)
        {
            _pCoord3Points.Add(pt);
            NewPoints++;
        }
        TotalPoints++;
        return retIndex;    
    }

    public int CheckPointForBreak(PointD pt)
    {
        int retIndex = -1;
        for (int i = 0; i < _pCoord3Points.Count; i++)
        {
            PointD otherPt = _pCoord3Points[i];
            if ((pt.X == otherPt.X) &&
                (pt.Y == otherPt.Y) &&
                (pt.Z == otherPt.Z))
            {
                retIndex = i;
                break;
            }
        }
        if (retIndex == -1)
        {
            NewPoints++;
            _pCoord3Points.Add(pt);
        }
        TotalPoints++;
        return retIndex;
    }

    static void Main()
    {
        const int xmax = 300;
        const int ymax = 10;
        const int zmax = 10;
        const int imax = 4;

        var test = new PerfTest();
        //test.Init();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < imax; i++)
        {
            for (int x = 0; x < xmax; x++)
            {
                for (int y = 0; y < ymax; y++)
                {
                    for (int z = 0; z < zmax; z++)
                    {
                        var pt = new PointD { X = x, Y = y, Z = z };
                        test.CheckPointIndexOf(pt);
                    }
                }
            }

        } 
        sw.Stop();
        string output = string.Format("Total: {0:0} New: {1:0} IndexOf: ", test.TotalPoints, test.NewPoints);
        Console.Write(output);
        Console.WriteLine(sw.Elapsed);

        test = new PerfTest();
        sw = Stopwatch.StartNew();
        for (int i = 0; i < imax; i++)
        {
            for (int x = 0; x < xmax; x++)
            {
                for (int y = 0; y < ymax; y++)
                {
                    for (int z = 0; z < zmax; z++)
                    {
                        var pt = new PointD { X = x, Y = y, Z = z };
                        test.CheckPointForBreak(pt);
                    }
                }
            }

        } 
        sw.Stop();
        output = string.Format("Total: {0:0} New: {1:0} PointD[] ", test.TotalPoints, test.NewPoints);
        Console.Write(output);
        Console.WriteLine(sw.Elapsed);
        Console.ReadLine();
    }
}

【问题讨论】:

  • 真的IndexOf 更快吗?当我试图重现 IndexOf 时速度明显变慢,我认为 Jon 关于拳击的推测是正确的。
  • 我得到了完全相反的结果,当 PointD 是一个结构时,IndexOf 慢了 20 倍。结构的 Equals() 方法并不便宜。发布一个真实可验证的例子。
  • 啊,如果 IndexOf 在你的机器上更快,但在其他人的机器上却没有,那可能是因为你的分析器。许多分析器不会同样惩罚他们无法插入的代码。

标签: c# performance indexof


【解决方案1】:

我做了以下假设:

  • PointD 是一个结构体
  • IndexOf 确实比手动迭代列表要慢

您可以通过实现IEquatable&lt;T&gt; 接口来加速IndexOf

struct PointD : IEquatable<PointD>
{
    public int X;
    public int Y;
    public int Z;

    public bool Equals(PointD other)
    {
        return (this.X == other.X) &&
                (this.Y == other.Y) &&
                (this.Z == other.Z);
    }
}

在不实现IEquatable&lt;T&gt; 接口的情况下,IndexOf 将使用ValueType.Equals(object other) 比较两个PointD 元素,这涉及昂贵的装箱操作。

IEquatable&lt;T&gt; 接口的文档说明:

IEquatable&lt;T&gt; 接口由泛型集合对象使用,例如 Dictionary&lt;TKey, TValue&gt;List&lt;T&gt;LinkedList&lt;T&gt;,在 ContainsIndexOfLastIndexOf 和 @ 等方法中测试相等性时987654338@。 应该为任何可能存储在通用集合中的对象实现它。

这是我的完整基准代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;

struct PointD 
{
    public int X;
    public int Y;
    public int Z;
}

class PerfTest
{
    List<PointD> _pCoord3Points = new List<PointD>();

    int checkPointIndexOf(PointD pt)
    {
        return _pCoord3Points.IndexOf(pt);  
    }

    int checkPointForBreak(PointD pt)
    {
        int retIndex = -1;
        for (int i = 0; i < _pCoord3Points.Count; i++)
        {
            PointD otherPt = _pCoord3Points[i];
            if ((pt.X == otherPt.X) &&
                (pt.Y == otherPt.Y) &&
                (pt.Z == otherPt.Z))
                retIndex = i;
            break;
        }
        return retIndex;
    }

    void init()
    {
        for (int x = 0; x < 100; x++)
        {
            for (int y = 0; y < 10; y++)
            {
                for (int z = 0; z < 10; z++)
                {
                    PointD pt = new PointD() { X = x, Y = y, Z = z };
                    _pCoord3Points.Add(pt);
                }
            }
        }
    }

    static void Main(string[] args)
    {
        PerfTest test = new PerfTest();
        test.init();
        Stopwatch sw = Stopwatch.StartNew();
        for (int x = 0; x < 100; x++)
        {
            for (int y = 0; y < 10; y++)
            {
                for (int z = 0; z < 10; z++)
                {
                    PointD pt = new PointD() { X = x, Y = y, Z = z };
                    test.checkPointIndexOf(pt);
                }
            }
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed);
        sw = Stopwatch.StartNew();
        for (int x = 0; x < 100; x++)
        {
            for (int y = 0; y < 10; y++)
            {
                for (int z = 0; z < 10; z++)
                {
                    PointD pt = new PointD() { X = x, Y = y, Z = z };
                    test.checkPointForBreak(pt);
                }
            }
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed);
    }
}

在 Windows 7、x64、.NET 4.0 x64 版本上,我得到以下时间(大约):

索引:00:00:04.8096623 循环:00:00:00.0014203

PointD 转换为class 时,时间会更改为

索引:00:00:01.0703627 循环:00:00:00.0014190

当使用struct PointD 实现IEquatable&lt;PointD&gt; 时,我得到更多“相似”的结果,但IndexOf 仍然较慢(现在使用class 没有显着差异):

索引:00:00:00.3904615 循环:00:00:00.0015218

【讨论】:

  • @0xA3:您能否在开始秒表之前调用List&lt;PointD&gt;.IndexOf 一次以消除 JIT 成本 + 一次性初始化(EqualityComparer&lt;PointD&gt; 的静态构造函数以确定正确的比较器等)发生在引擎盖下?考虑到我们看到的数量级差异,我认为这并不重要。
  • @Ani:是的,你说得对,基准在 JIT 开销方面并不准确。但我决定忽略效果。事实上,如果你先热身,你会发现 for 循环的 JIT 开销更大,这似乎是合理的,因为 List&lt;T&gt;.IndexOf 包含在 mscorlib.dll 的本机映像中。
  • 它最终是 IndexOf 8.08... 和 [] 10.018... 这是我的问题,为什么?感谢您为当前答案所做的所有工作
  • 当我从 struct 更改为更改它的 class 时。知道了。再次感谢
【解决方案2】:

通常,在您访问数组元素之前,它会检查以确保索引 >= 0 且 buffer overflows 的严重安全漏洞。

不用说,如果循环中的代码非常少,这会影响性能。为了减轻这个问题,JIT 编译器优化了 for (i = 0; i &lt; array.Length; i++) { array[i]; } 形式的 for 循环——即任何访问数组从 0 到长度 - 1 的所有索引的循环。它省略了这种情况下的边界检查。 (如果你访问像 array[i + 1] 这样的东西,优化会失败,因为你可能会越界。)

不幸的是,这只适用于数组,不适用于 Lists。所以你后面的代码不会被优化。

但由于List内部包含一个数组,而List.IndexOf()使用循环直接访问数组中的每个值,所以可以优化。

顺便说一句,说for (int i = 0; i &lt; array.Length; i++) { } 比说int length = array.Length; for(int i = 0; i &lt; length; i++) { } 好——因为它不能确定length 真的是数组的长度。

编辑:使用 Reflector 查看 IndexOf 代码,循环确实会优化,但正如这里其他人提到的,它调用 Equals() - 这需要 vtable lookup 和各种健全性检查。因此,在这种情况下,如果您不使用分析器运行 IndexOf,它实际上可能会更慢。

反汇编代码:

internal virtual int IndexOf(T[] array, T value, int startIndex, int count)
{
    int num = startIndex + count;
    for (int i = startIndex; i < num; i++)
    {
        if (this.Equals(array[i], value))
        {
            return i;
        }
    }
    return -1;
}

【讨论】:

  • 那里疯狂的.NET知识!
【解决方案3】:

_pCoord3Points 的类型是什么?如果元素类型是只覆盖Equals(object) 的值类型,那么IndexOf 可能会重复装箱值以检查是否相等。这可能解释它。不过,这实际上只是猜测……如果您可以提供一个简短但完整的程序来演示该问题,那将更容易为您提供帮助。

【讨论】:

  • 但是如果IndexOf 涉及拳击,那么它应该更慢,而不是像 OP 声称的那样更快,不是吗?
  • 的确,乔恩,你似乎是对的。我测量了 IndexOf 对于值类型的速度明显慢(在我的测量中大约为 1000 倍),而对于引用类型来说仍然慢了大约 100 倍。
猜你喜欢
  • 2011-05-01
  • 2015-07-24
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 2016-12-26
相关资源
最近更新 更多