Как я могу заставить свою функцию работать так же быстро, как" содержит " в ArrayList?
Я не могу вычислить расхождение между временем, которое требуется для метода 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 работает лучше, но и последовательно достигает лучших результатов в каждом запуске. Итак, есть ли здесь какой-то недостаток для функций третьей стороны, или что? 11 ответов:
Используя приведенный ниже код, я смог получить следующие тайминги относительно согласованно (в течение нескольких МС):
1: 190ms DoesContainRev
2: 198ms DoesContainRev1
3: 188ms DoesContainFwd
4: 203ms DoesContainFwd1
5: 199ms содержитЗдесь следует отметить несколько моментов.
Это выполняется с помощью кода release compiled из командной строки. Многие люди делают ошибку, сравнивая код внутри среды отладки Visual Studio, а не сказать, что кто-то здесь это сделал, значит быть осторожным.
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; } } }
Во-первых, вы не запускаете его много раз и не сравниваете средние значения.
Во-вторых, ваш метод не будет jitted, пока он фактически не запустится. Таким образом, время компиляции just in time добавляется во время его выполнения.
Истинный тест будет выполняться каждый несколько раз и усреднять результаты (любое количество вещей может привести к тому, что один или другой будет медленнее для запуска X из общего числа Y), и ваши сборки должны быть предварительно скомпонованы с использованием ngen.exe .
Поскольку вы используете .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>и использовать его повторно.
Я не уверен, что вам разрешено публиковать код Reflector, но если вы откроете метод с помощью Reflector, вы увидите, что это, по сути, то же самое (есть некоторые оптимизации для нулевых значений, но ваша тестовая проводка не содержит нулей).
Единственное различие, которое я вижу, заключается в том, что вызовlist[i]делает проверку границ наi, тогда как метод Contains этого не делает.
С действительно хорошим оптимизатором вообще не должно быть разницы, потому что семантика кажется одинаковой. Однако существующий оптимизатор может оптимизировать вашу функцию не так хорошо, как оптимизирован жестко закодированный
Contains. Некоторые пункты для оптимизации:
- сравнение со свойством каждый раз может быть медленнее, чем обратный отсчет и сравнение с 0
- сам вызов функции имеет свой штраф за производительность
- использование итераторов вместо явное индексирование может быть быстрее (
foreachцикл вместо простогоfor)
Во-первых, если вы используете типы, которые вы знаете заранее, я бы предложил использовать дженерики. Так что список вместо ArrayList. Под капотом-Аррайлист.Содержит на самом деле делает немного больше, чем то, что вы делаете. Следующее от рефлектора:
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-компиляцию, где, как очевидно, уже скомпилирован фреймворк.
После вашей правки я скопировал код и внес в него несколько улучшений.
Разница не была воспроизводимой, она оказалась проблемой измерения/округления.Чтобы увидеть это, измените свои пробеги на следующую форму:
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++ можно использовать арифметику указателей (в частности, увеличение указателя для итерации массива), чтобы быть быстрее, чем использование индекса.
Используя структуру массива, вы не можете искать быстрее, чем O (n) без какой-либо дополнительной информации. если вы знаете, что массив отсортирован, то можете воспользоваться алгоритмом бинарного поиска и потратить только o (log(n)) в противном случае вы должны использовать набор.
Исправлено после прочтения комментариев:
Он не использует некоторыйхэш-аллогоритм для быстрого поиска.
Использование
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 не требует указания ключа.
Comments