Подтвердить что ты не робот

Реализация цикла List.Contains() появляется быстрее, чем встроенная. Это? Если да, то почему?

(Этот вопрос возникает из обсуждения, которое началось здесь)

Я сравнивал тайминги для поиска значения true в List<bool> с помощью List.Contains() с параметрами для ручного проката.

Я вижу разные результаты от сообщений других людей. Я попробовал это на нескольких системах, и цикл кажется более быстрым на 2 и 3,5 раза на всех системах, в которых я его пробовал. Эти системы варьируются от 5-летних ноутбуков с XP с .Net 4 до недавних ПК под управлением Windows 8 и .Net 4.5.

Другие люди сообщают о разных результатах, а именно: List.Contains() примерно такая же скорость, как или немного быстрее, чем цикл.

Вот мой тестовый код.

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

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main()
        {
            int size = 10000000;
            int count = 10;
            List<bool> data = new List<bool>(size);

            for (int i = 0; i < size; ++i)
                data.Add(false);

            var sw = new Stopwatch();

            for (int trial = 0; trial < 5; ++trial)
            {
                sw.Restart();

                for (int i = 0; i < count; ++i)
                    TestViaLoop(data);

                sw.Stop();
                Console.WriteLine(sw.ElapsedMilliseconds + " TestViaLoop()");
                sw.Restart();

                for (int i = 0; i < count; ++i)
                    TestViaListContains(data);

                sw.Stop();
                Console.WriteLine(sw.ElapsedMilliseconds + " TestViaListContains()");
                Console.WriteLine();
            }
        }

        static bool TestViaLoop(List<bool> data)
        {
            for (int i = 0; i < data.Count; ++i)
                if (data[i])
                    return true;

            return false;
        }

        static bool TestViaListContains(List<bool> data)
        {
            return data.Contains(true);
        }
    }
}

Чтобы протестировать этот код, вы должны скомпилировать его как сборку x86 RELEASE и запустить ее извне отладчика.

Вот мои результаты с моего ПК с Windows 8 x64 с использованием среды .NET 4.5 (хотя я получаю аналогичные результаты с .Net 4):

Times are in milliseconds

126 TestViaLoop()
441 TestViaListContains()

122 TestViaLoop()
428 TestViaListContains()

131 TestViaLoop()
431 TestViaListContains()

138 TestViaLoop()
426 TestViaListContains()

122 TestViaLoop()
439 TestViaListContains()

Как вы можете видеть, цикл занимает около 1/3 времени в моей системе.

Теперь, если мы используем Resharper для просмотра реализации List.Contains(), он выглядит так:

bool Contains(T item)
{
    if (item == null)
    {
        for (int j = 0x0; j < this._size; j++)
        {
            if (this._items[j] == null)
            {
                return true;
            }
        }
        return false;
    }
    EqualityComparer<T> comparer = EqualityComparer<T>.Default;
    for (int i = 0x0; i < this._size; i++)
    {
        if (comparer.Equals(this._items[i], item))
        {
            return true;
        }
    }
    return false;
}

Хотя он использует Comparer.Equals() (который должен сделать его медленнее, чем цикл), он также использует частный массив _items[], что позволяет избежать проверки диапазона индекса, который будет использоваться для моей реализации цикла.

У меня есть три вопроса:

  • Может ли кто-нибудь повторить результаты, которые я вижу? (Не забудьте запустить сборку релиза вне отладчика.)
  • Если да, может ли кто-нибудь объяснить, как мой цикл может быть намного быстрее, чем List.Contains()?
  • Если нет, может ли кто-нибудь объяснить, почему я вижу, что мой цикл будет быстрее?

Это не просто академический интерес для меня, так как я пишу код, который работает с большим количеством числовых данных и который должен быть как можно быстрее, и это то, о чем мне нужно знать. (Примечание: Да, я прорабатываю вещи и только пытаюсь оптимизировать вещи, которые нужно оптимизировать... Я знаю о проблемах преждевременной оптимизации.)

[EDIT]

Мне кажется, что это может быть связано с процессором. Все системы, на которых я это пробовал, имеют процессоры Intel, хотя и очень разные модели от Quad Core на 3,8 ГГц до одного ядра Pentium M на частоте 1,6 ГГц...

Для тех из вас, кто видит, что цикл работает медленнее, вы используете процессоры Intel?

4b9b3361

Ответ 1

Он использует GenericEqualityComparer, если мы посмотрим на реализацию метода Equals, будет выглядеть так:

public override bool Equals(T x, T y)
{
  if ((object) x != null)
  {
    if ((object) y != null)
      return x.Equals(y);
    else
      return false;
  }
  else
    return (object) y == null;
}

Когда он проверяет, не являются ли объекты равными нулю, он делает их бокс, и вы получаете две операции по боксу. Этот IL-код показывает, как он выглядит:

IL_0002: box !T
IL_0007: ldnull
IL_0008: ceq

Редактировать по 280Z28: CIL для того же метода несколько отличается в .NET 4.5.

public override bool Equals(T x, T y)
{
    if (x != null)
        return ((y != null) && x.Equals(y));

    if (y != null)
        return false;

    return true;
}

Вот IL. Для тех, кто смотрит на Reflector, обратите внимание, что brfalse.s и brnull.s являются одной и той же инструкцией.

L_0000: ldarg.1 
L_0001: box !T
L_0006: brnull.s L_0021
...

Базовый JIT-компилятор не оптимизирует работу с ящиком, но я не проверял с помощью NGen или оптимизирующего компилятора, чтобы убедиться, что они делают.

Ответ 2

Реализация цикла создает тот же результат, что и Contains, но вы не можете использовать его в общем случае. То есть Вы должны были бы использовать сравнение Equals для более сложных объектов. Реализация Contains выполняет больше работы, чем ваша реализация, поэтому я не понимаю, почему вы должны ожидать, что она будет быстрее в этом случае.

Если у вас есть список пользовательских Person объектов, и вы можете переопределить метод Equals для сравнения, скажем, с их Address Name SSNumber и DateOfBirth, циклы будут выполняться почти с одинаковыми эксплуатационные расходы.

Я ожидал бы для примитивных значений, то да, итерация цикла будет превосходить общий Contains, но это преждевременная оптимизация, вы не будете делать (существенно) лучше, чем Contains для более сложных сравнения объектов.