Как я могу сделать так, чтобы моя функция работала так же быстро, как «Содержит» в ArrayList? - PullRequest
6 голосов
/ 25 февраля 2010

Я не могу понять несоответствия между временем, которое требуется для метода Contains для поиска элемента в ArrayList, и временем, которое требуется для небольшой функции, которую я написал, чтобы выполнить то же самое. В документации говорится, что Contains выполняет линейный поиск, поэтому он должен быть в O(n), а не каким-либо другим более быстрым методом. Однако, хотя точные значения могут не иметь значения, метод Contains возвращает через 00:00:00.1087087 секунд, а моя функция занимает 00:00:00.1876165. Это может быть немного, но эта разница становится более очевидной при работе с массивами еще больших размеров. Чего мне не хватает и как мне написать свою функцию, чтобы соответствовать выступлениям Contains?

Я использую C # в .NET 3.5.

public partial class Window1 : Window
{
    public bool DoesContain(ArrayList list, object element)
    {
        for (int i = 0; i < list.Count; i++)
            if (list[i].Equals(element)) return true;

        return false;
    }

    public Window1()
    {
        InitializeComponent();

        ArrayList list = new ArrayList();
        for (int i = 0; i < 10000000; i++) list.Add("zzz " + i);

        Stopwatch sw = new Stopwatch();
        sw.Start();

        //Console.Out.WriteLine(list.Contains("zzz 9000000") + " " + sw.Elapsed);
        Console.Out.WriteLine(DoesContain(list, "zzz 9000000") + " " + sw.Elapsed);
    }
}

EDIT:

Хорошо, ребята, посмотрите:

public partial class Window1 : Window
{
    public bool DoesContain(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (element.Equals(list[i])) return true;

        return false;
    }


    public bool DoesContain1(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (element.Equals(list[i])) return true;

        return false;
    }

    public Window1()
    {
        InitializeComponent();

        ArrayList list = new ArrayList();
        for (int i = 0; i < 10000000; i++) list.Add("zzz " + i);

        Stopwatch sw = new Stopwatch();
        long total = 0;
        int nr = 100;

        for (int i = 0; i < nr; i++)
        {
            sw.Reset();
            sw.Start();
            DoesContain(list,"zzz");
            total += sw.ElapsedMilliseconds;
        }
        Console.Out.WriteLine(total / nr);


        total = 0;
        for (int i = 0; i < nr; i++)
        {
            sw.Reset();
            sw.Start();
            DoesContain1(list, "zzz");
            total += sw.ElapsedMilliseconds;
        }
        Console.Out.WriteLine(total / nr);


        total = 0;
        for (int i = 0; i < nr; i++)
        {
            sw.Reset();
            sw.Start();
            list.Contains("zzz");
            total += sw.ElapsedMilliseconds;
        }
        Console.Out.WriteLine(total / nr);
    }
  }

Я сделал в среднем 100 рабочих времен для двух версий моей функции (прямая и обратная петля) и для функции Contains по умолчанию. Время у меня 136 и 133 миллисекунд для моих функций и отдаленный победитель 87 для Contains версии. Что ж, если раньше вы могли утверждать, что данных было мало, и я основал свои выводы на первом, изолированном прогоне, что вы скажете об этом тесте? Не только в среднем Contains работает лучше, но он достигает неизменно лучших результатов в каждом запуске. Итак, есть ли здесь какой-то недостаток для сторонних функций или что?

Ответы [ 11 ]

11 голосов
/ 25 февраля 2010

Во-первых, вы не запускаете его много раз и сравниваете средние значения.

Во-вторых, ваш метод не подключается, пока он на самом деле не запускается. Таким образом, точное время компиляции добавляется к времени выполнения.

Истинный тест будет запускаться каждый раз несколько раз и усреднять результаты (любое количество вещей может привести к тому, что один или другой будет медленнее для запуска X из общего Y), и ваши сборки должны быть предварительно объединены с использованием ngen.exe .

5 голосов
/ 25 февраля 2010

Когда вы используете .NET 3.5, почему вы используете ArrayList для начала, а не List<string>?

Несколько вещей, которые нужно попробовать:

  • Вы могли видеть, помогает ли использование foreach вместо цикла for
  • Вы можете кэшировать счет:

    public bool DoesContain(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = 0; i < count; i++)
        {
            if (list[i].Equals(element))
            {
                return true;
            }
            return false;
        }
    }
    
  • Вы можете отменить сравнение:

    if (element.Equals(list[i]))
    

Хотя я не ожидаю, что что-либо из этого будет иметь существенное (положительное) значение, это следующие вещи, которые я попробую.

Вам нужно сделать этот тест на содержание несколько раз? Если это так, вы можете создать HashSet<T> и использовать его повторно.

2 голосов
/ 25 февраля 2010

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

Единственное отличие, которое я вижу, заключается в том, что вызов list[i] выполняет проверку границ для i, тогда как метод Contains - нет.

1 голос
/ 26 февраля 2010

После вашего редактирования я скопировал код и сделал несколько улучшений.
Разница не воспроизводима, это проблема измерения / округления.

Чтобы увидеть это, измените свои пробеги на эту форму:

    sw.Reset();
    sw.Start();
    for (int i = 0; i < nr; i++)
    {          
        DoesContain(list,"zzz");            
    }
    total += sw.ElapsedMilliseconds;
    Console.WriteLine(total / nr);

Я только что переместил несколько строк. Проблема JIT была незначительной с таким количеством повторений.

1 голос
/ 26 февраля 2010

Используя приведенный ниже код, я смог относительно условно получить следующие значения времени (в течение нескольких мс):
1: 190мс DidContainRev
2: 198мс DoesContainRev1
3: 188мс DoesContainFwd
4: 203мс DoesContainFwd1
5: 199мс Содержит

Несколько вещей, на которые стоит обратить внимание.

  1. Это запускается с выпуском скомпилированного кода из командной строки. Многие люди делают ошибку, сравнивая тестирование кода в среде отладки Visual Studio, не говоря уже о том, что здесь кто-то сделал, но о чем-то нужно быть осторожным.

  2. list[i].Equals(element) выглядит немного медленнее, чем element.Equals(list[i]).

    using System;
    using System.Diagnostics;
    using System.Collections;
    
    
    namespace ArrayListBenchmark
    {
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            const int arrayCount = 10000000;
            ArrayList list = new ArrayList(arrayCount);
            for (int i = 0; i < arrayCount; i++) list.Add("zzz " + i);
        sw.Start();
        DoesContainRev(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("1: {0}", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainRev1(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("2: {0}", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainFwd(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("3: {0}", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainFwd1(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("4: {0}", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        list.Contains("zzz");
        sw.Stop();
        Console.WriteLine(String.Format("5: {0}", sw.ElapsedMilliseconds));
        sw.Reset();
    
        Console.ReadKey();
    }
    public static bool DoesContainRev(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (element.Equals(list[i])) return true;
    
        return false;
    }
    public static bool DoesContainFwd(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (element.Equals(list[i])) return true;
    
        return false;
    }
    public static bool DoesContainRev1(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (list[i].Equals(element)) return true;
    
        return false;
    }
    public static bool DoesContainFwd1(ArrayList list, object element)
    {
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (list[i].Equals(element)) return true;
    
        return false;
    }
             }
            }
    
1 голос
/ 25 февраля 2010

Во-первых, если вы используете типы, которые вы знаете заранее, я бы предложил использовать дженерики. Так что List вместо ArrayList. ArrayList.Contains под капотом делает немного больше, чем то, что вы делаете. Следующее от отражателя:

public virtual bool Contains(object item)
{
    if (item == null)
    {
        for (int j = 0; j < this._size; j++)
        {
            if (this._items[j] == null)
            {
                return true;
            }
        }
        return false;
    }
    for (int i = 0; i < this._size; i++)
    {
        if ((this._items[i] != null) && this._items[i].Equals(item))
        {
            return true;
        }
    }
    return false;
}

Обратите внимание, что он разветвляется после передачи нулевого значения для элемента. Однако, поскольку все значения в вашем примере не равны NULL, дополнительная проверка на NULL в начале и во втором цикле теоретически должна занять больше времени.

Вы уверены, что имеете дело с полностью скомпилированным кодом? То есть, когда ваш код запускается в первый раз, он компилируется в JIT, тогда как инфраструктура, очевидно, уже скомпилирована.

1 голос
/ 25 февраля 2010

С действительно хорошим оптимизатором вообще не должно быть различий, потому что семантика кажется одинаковой. Однако существующий оптимизатор может оптимизировать вашу функцию не так хорошо, как оптимизированный с жестким кодом Contains. Некоторые из пунктов для оптимизации:

  1. сравнение со свойством каждый раз может быть медленнее, чем обратный отсчет и сравнение с 0
  2. сам вызов функции имеет снижение производительности
  3. Использование итераторов вместо явного индексирования может быть быстрее (цикл foreach вместо простого for)
0 голосов
/ 25 февраля 2010

используя структуру массива, вы не можете искать быстрее, чем O (n) без какой-либо дополнительной информации. если вы знаете, что массив отсортирован, то вы можете использовать алгоритм бинарного поиска и потратить только o (log (n)) в противном случае вы должны использовать набор.

0 голосов
/ 25 февраля 2010

Используйте SortedList<TKey,TValue>, Dictionary<TKey, TValue> или System.Collections.ObjectModel.KeyedCollection<TKey, TValue> для быстрого доступа по ключу.

var list = new List<myObject>(); // Search is sequential
var dictionary = new Dictionary<myObject, myObject>(); // key based lookup, but no sequential lookup, Contains fast
var sortedList = new SortedList<myObject, myObject>(); // key based and sequential lookup, Contains fast

KeyedCollection<TKey, TValue> также быстрый и позволяет индексированный поиск, однако его необходимо наследовать, поскольку он является абстрактным. Поэтому вам нужна конкретная коллекция. Однако с помощью следующего вы можете создать универсальный KeyedCollection.

public class GenericKeyedCollection<TKey, TValue> : KeyedCollection<TKey, TValue> {
   public GenericKeyedCollection(Func<TValue, TKey> keyExtractor) {
      this.keyExtractor = keyExtractor;
   }

   private Func<TValue, TKey> keyExtractor;

   protected override TKey GetKeyForItem(TValue value) {
      return this.keyExtractor(value);
   }
}

Преимущество использования KeyedCollection заключается в том, что метод Add не требует указания ключа.

0 голосов
/ 25 февраля 2010

Мне кажется, что ArrayList написан на C ++ и может использовать некоторые микрооптимизации (примечание: это предположение).

Например, в C ++ вы можете использовать арифметику указателей (в частности, увеличивая указатель для итерации массива), чтобы быстрее, чем при использовании индекса.

...