Я не могу выяснить несоответствие между временем, которое требуется для 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);
}
}
Править:
Хорошо, теперь, парни, посмотрите:
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
выполните лучше, но это достигает последовательно лучших результатов в каждом выполнении. Так, есть ли некоторый недостаток в здесь для сторонних функций, или что?
Используя приведенный ниже код, я смог относительно стабильно (в пределах нескольких мс) получить следующие значения времени:
1: 190 мс DoesContainRev
2: 198 мс DoesContainRev1
3: 188 мс DoesContainFwd
4: 203 мс DoesContainFwd1
5: 199 мс Содержит
Здесь следует обратить внимание на несколько вещей.
Это запускается с кодом, скомпилированным для выпуска, из командной строки. Многие люди совершают ошибку при тестировании кода внутри среды отладки Visual Studio, не говоря уже о том, что кто-то здесь делал, но о чем-то, о чем следует быть осторожным.
Список list [i] .Equals (element)
выглядит немного медленнее, чем element.Equals (list [i])
.
с использованием System;
с использованием System.Diagnostics;
с использованием System.Collections;
пространство имен ArrayListBenchmark
{{ {1}} class Program
{
static void Main (string [] args)
{
Секундомер sw = new Stopwatch ();
const int arrayCount = 10000000;
ArrayList list = new ArrayList (arrayCount);
for (int i = 0; i = 0; i -)
if (element.Equals (list [i])) return true;
return false ;
}
public static bool DoesContainFwd (список ArrayList, элемент объекта)
{
int count = list.Count;
for (int i = 0; i = 0; i -)
if (list [i] .Equals (element)) return true;
return false;
}
public static bool DoesContainFwd1 (список ArrayList, элемент объекта)
{
int count = list.Count;
for (int i = 0; i
Во-первых, вы не запускаете его много раз и не сравниваете средние значения.
Во-вторых, ваш метод не будет изменен до тех пор, пока он не будет запущен. Таким образом, время своевременной компиляции добавляется ко времени его выполнения.
Настоящий тест будет запускаться каждый несколько раз и усреднять результаты (любое количество вещей может привести к тому, что один или другой будет медленнее для выполнения X из общего количества Y), и ваши сборки должны быть предварительно настроены с использованием ngen.exe .
С действительно оптимизатором не должно быть никакой разницы, потому что семантика кажется одинаковой. Однако существующий оптимизатор может оптимизировать вашу функцию не так хорошо, как оптимизированный жестко запрограммированный Contains
. Некоторые моменты для оптимизации:
цикл foreach
вместо простого for
) Используйте SortedList
, Dictionary
или System. Collections.ObjectModel.KeyedCollection
для быстрого доступа на основе ключа.
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
также работает быстро и позволяет индексированный поиск, однако его необходимо унаследовать, поскольку он является абстрактным. Следовательно, вам нужна конкретная коллекция. Однако с помощью следующего вы можете создать общий 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 не требует указания ключа.
Я не уверен, разрешено ли вам публиковать код Reflector, но если вы откроете метод с помощью Reflector, вы увидите, что это по сути то же самое (есть некоторые оптимизации для нулевых значений, но ваш тестовый набор не включает нули).
Единственное различие, которое я вижу, это то, что вызов list[i]
делает проверку границ для i
, тогда как метод Contains этого не делает.
Во-первых, если вы используете типы, которые знаете заранее, я бы посоветовал использовать дженерики. Итак, 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;
}
Обратите внимание, что он разветвляется при передаче нулевого значения для элемента. Однако, поскольку все значения в вашем примере не равны нулю, дополнительная проверка нуля в начале и во втором цикле теоретически должна занять больше времени.
Вы уверены, что имеете дело с полностью скомпилированным кодом? То есть, когда ваш код запускается в первый раз, он компилируется JIT, поскольку структура, очевидно, уже скомпилирована.
После вашего редактирования я скопировал код и сделал в нем несколько улучшений.
Разница не была воспроизведена, оказалось, что это проблема измерения/округления.
Чтобы увидеть это, измените свои прогоны на такой вид:
sw.Reset();
sw.Start();
for (int i = 0; i < nr; i++)
{
DoesContain(list,"zzz");
}
total += sw.ElapsedMilliseconds;
Console.WriteLine(total / nr);
Я просто передвинул несколько строк. Проблема JIT оказалась незначительной при таком количестве повторений.
Мое предположение заключается в том, что ArrayList написан на C++ и может использовать преимущества некоторых микрооптимизаций (примечание: это предположение).
Например, в C++ вы можете использовать арифметику указателей (в частности, инкрементирование указателя для итерации массива), чтобы быть быстрее, чем использование индекса.
Поскольку вы используете .NET 3.5, почему вы используете для начала ArrayList
, а не List
?
Несколько вещей, которые стоит попробовать:
foreach
вместо for
цикла Вы можете кэшировать счетчик:
public bool DoesContain (список ArrayList, элемент объекта) {{ 1}} {
int count = list.Count;
for (int i = 0; i
Вы можете перевернуть сравнение:
if (element.Equals (list [i]))
Хотя я не ожидаю, что что-либо из этого будет имеют существенное (положительное) значение, я бы в следующий раз попробовал это.
Нужно ли вам проводить это испытание на содержание более одного раза? Если это так, вы можете создать HashSet
и использовать его повторно.
используя структуру массива, вы не можете искать быстрее, чем O(n) без дополнительной информации. Если известно, что массив отсортирован, то можно использовать алгоритм бинарного поиска и потратить всего o(log(n)). В противном случае вы должны использовать набор.
Изменено после прочтения комментариев:
Он не использует некоторый хеш-алгоритм для обеспечения быстрого поиска.