Почему этот код List‹›.IndexOf намного быстрее, чем List[i] и ручное сравнение?

Я запускаю 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();
    }
}

person Roger    schedule 07.09.2010    source источник
comment
Действительно ли IndexOf быстрее? Когда я пытался воспроизвести IndexOf, он был значительно медленнее, и я предполагаю, что предположения Джона о боксе верны.   -  person Dirk Vollmar    schedule 08.09.2010
comment
Я получаю прямо противоположный результат: IndexOf в 20 раз медленнее, когда PointD является структурой. Метод Equals() структуры недешев. Опубликуйте реальный пример, который можно проверить.   -  person Hans Passant    schedule 08.09.2010
comment
Ах, если IndexOf работает быстрее на вашей машине, но не на чьей-либо еще, это, вероятно, из-за вашего профилировщика. Многие профилировщики не одинаково наказывают код, в котором они не могут копаться.   -  person Rei Miyasaka    schedule 08.09.2010


Ответы (3)


Я сделал следующие предположения:

  • PointD это структура
  • IndexOf действительно медленнее, чем перебор списка вручную

Вы можете ускорить IndexOf, реализовав интерфейс IEquatable<T>:

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<T> IndexOf будет сравнивать два элемента PointD, используя ValueType.Equals(object other), который требует дорогостоящих операций упаковки.

В документации по интерфейсу IEquatable<T> указано:

Интерфейс IEquatable<T> используется универсальными объектами коллекций, такими как Dictionary<TKey, TValue>, List<T> и LinkedList<T>, при проверке на равенство в таких методах, как Contains, IndexOf, LastIndexOf и Remove. Его следует реализовать для любого объекта, который может храниться в универсальной коллекции.

Вот мой полный код теста:

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 я получаю следующие тайминги (приблизительно):

IndexOf:  00:00:04.8096623
For Loop: 00:00:00.0014203

При превращении PointD в class тайминги меняются на

IndexOf:  00:00:01.0703627
For Loop: 00:00:00.0014190

При использовании struct PointD, реализующего IEquatable<PointD>, я получаю более "похожие" результаты, но IndexOf все еще медленнее (теперь нет существенной разницы при использовании class):

IndexOf:  00:00:00.3904615
For Loop: 00:00:00.0015218
person Dirk Vollmar    schedule 07.09.2010
comment
@ 0xA3: Не могли бы вы вызвать List<PointD>.IndexOf один раз перед запуском секундомера, чтобы исключить затраты JIT + одноразовую инициализацию (статический конструктор EqualityComparer<PointD> для определения правильного компаратора и т. д.), которая происходит под капотом? Не то чтобы я считал, что это имеет значение, учитывая разницу в порядке величин, которую мы наблюдаем. - person Ani; 08.09.2010
comment
@Ani: Да, вы правы в том, что эталонный тест неточен в отношении накладных расходов JIT. Но я решил пренебречь эффектом. Фактически, если вы сначала разогреетесь, вы увидите, что накладные расходы JIT для цикла for более значительны, что кажется разумным, поскольку List<T>.IndexOf содержится в собственном образе mscorlib.dll. - person Dirk Vollmar; 08.09.2010
comment
это заканчивается IndexOf 8.08... и [] 10.018... это мой вопрос, почему? Спасибо за всю работу над вашим текущим ответом - person Roger; 08.09.2010
comment
Когда я перешел от структуры к классу, это изменило его. Понятно. еще раз спасибо - person Roger; 08.09.2010

Обычно, прежде чем вы получите доступ к элементу массива, он проверяет, что индекс >= 0 и длина ‹, чтобы вы не читали и не перезаписывали память, которая вам не принадлежит. Помимо прочего, он устраняет множество серьезных недостатков безопасности, называемых переполнением буфера.

Излишне говорить, что это снижает производительность, если в вашем цикле очень мало кода. Чтобы уменьшить эту проблему, JIT-компилятор оптимизирует циклы for формы for (i = 0; i < array.Length; i++) { array[i]; }, то есть любой цикл, который обращается ко всем индексам массива от 0 до длины - 1. В этом случае он опускает проверку границ. (Оптимизация терпит неудачу, если вы обращаетесь к таким вещам, как массив [i + 1], по той причине, что вы можете выйти за границы.)

К сожалению, это работает только с массивами, а не со списками. Таким образом, ваш последний код не будет оптимизирован.

Но поскольку List‹> внутри содержит массив, а List.IndexOf() использует цикл для прямого доступа к каждому значению в массиве, его можно оптимизировать.

Кстати, лучше сказать for (int i = 0; i < array.Length; i++) { }, чем int length = array.Length; for(int i = 0; i < length; i++) { }, потому что нельзя быть уверенным, что length действительно является длиной массива.

Изменить: глядя на код IndexOf с использованием Reflector, цикл действительно оптимизируется, но, как уже упоминали другие люди, он вызывает Equals(), для чего требуется поиск vtable и различные проверки работоспособности. Так что в этом случае 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;
}
person Rei Miyasaka    schedule 07.09.2010
comment
Сумасшедшие случайные знания .NET есть! - person Chris Marisic; 08.09.2010

Какой тип _pCoord3Points? Если тип элемента является типом значения, который переопределяет только Equals(object), то возможно, что IndexOf многократно упаковывает значения для проверки на равенство. Это может объяснить. Однако на данный момент это просто догадки... если бы вы могли предоставить короткую, но полную программу, демонстрирующую проблему, вам было бы намного легче помочь.

person Jon Skeet    schedule 07.09.2010
comment
Но если IndexOf включает бокс, то он должен быть медленнее, а не быстрее, как утверждает OP, не так ли? - person Dirk Vollmar; 08.09.2010
comment
Действительно, Джон, похоже, ты прав. Я измерил, и IndexOf значительно медленнее (в моем измерении около 1000 раз) для типов значений и все еще примерно в 100 раз медленнее для ссылочных типов. - person Dirk Vollmar; 08.09.2010