Для гуру производительности LINQ — поиск в неуниверсальной коллекции ICollection

Мне нужно выполнить поиск в коллекции объектов "Item", которые содержат свойство времени. У меня есть быстрое решение, но оно очень грязное (опубликую, если ничего лучше не появится). Ниже приведены мои попытки выполнить поиск самостоятельно, используя LINQ и еще много чего.

В моем конкретном случае я знаю, что элементы упорядочены по возрастанию в зависимости от времени. Когда я перебираю их, это 9/12, 9/13, 9/14. Я хотел бы найти быстрое решение, даже если это не заказано, но сейчас это не важно.

//ICollection c = GetCollection(); //25,000+ items
DateTime TIME = DateTime.Now.AddDays(-1);

EventLog scan = new EventLog("Application", "Server", "N/A");
EventLogCollection c = scan.Entries;
Console.WriteLine(logs.Count); // All entries already in list here

// 64 sec - SLOW
List<Item> l1 = new List<Item>();
foreach (Item i in c) { 
  if (i.time > TIME) { 
    l1.Add(i); }
}

// 93 sec - SLOWER
var l2 = c.Cast<Item>().AsParallel().Select(n => n.time > TIME);
var i = l2.Count();

// 98 sec - EVEN SLOWER!
List<Item> l3 = new List<Item>();
Parallel.ForEach(c.Cast<Item>(), n => {
    if (n.time > TIME) {
        l3.add(n);
    }
});

Мое текущее решение - выполнить BinarySearch для времени начала и окончания и прокрутить ICollection на основе этих индексов. Это очень быстро (1-2 секунды), но очень грязно. Мне не нужно другое решение, но я решил предложить его вам, экспертам по производительности.

Есть ли более быстрый и элегантный способ поиска в ICollection? Кстати, у меня нет контроля над предоставленной мне коллекцией и я не могу изменить ее структуру. .NET 4.0

И нет, я не могу использовать System.Diagnostics.Eventing.Reader, потому что я застрял в Windows XP.


person user1002479    schedule 01.10.2012    source источник
comment
возможный дубликат Может ли LINQ использовать бинарный поиск, когда коллекция заказал?   -  person Mike Perrenoud    schedule 01.10.2012
comment
Честно говоря, я удивлен, что для перебора 25 000 элементов требуется 64 секунды, только выполняя приведение, проверку значения и добавление. Ваш GetCollection на самом деле лениво извлекает записи из базы данных или что-то в этом роде? Или выполнять дополнительную работу при повторении или вычислении его time?   -  person Chris Sinclair    schedule 01.10.2012
comment
Из любопытства, как вы выполняете произвольный доступ к ICollection, чтобы запустить на нем двоичный поиск?   -  person Sergey Kalinichenko    schedule 01.10.2012
comment
На самом деле я не ищу ICollection... это действительно EventLogEntryCollection, который содержит this[int index] {get;}. Мне любопытно, есть ли лучший подход. Извините за путаницу Крис- нет. Я получаю заполненный список ... Я думаю, что это Cast, который занимает вечность, но еще не проверял это. Все еще работаю над этим   -  person user1002479    schedule 01.10.2012
comment
@ user1002479: тогда посмотри, как реализован индексатор! Это О (1)? Это делает ленивую загрузку?   -  person TDaver    schedule 01.10.2012
comment
TDaver: я не хочу использовать индексатор. В списке уже есть все значения   -  person user1002479    schedule 01.10.2012
comment
без индексатора, как вы делаете BinarySearch? Произвольный доступ (т.е. доступ по индексу) необходим для BinarySearch.   -  person TDaver    schedule 01.10.2012
comment
В моем текущем решении используется indexer. Благодаря этому у меня теперь есть класс IComparer и схематичный метод BinarySearch. Я пытаюсь найти решение, которое не требует дополнительных вещей. Сделай вид, что его нет :)   -  person user1002479    schedule 01.10.2012
comment
EventLogEntryCollection поддерживает индексатор, несмотря на отсутствие реализации IList msdn.microsoft .com/en-us/library/   -  person Jodrell    schedule 01.10.2012
comment
Я расширил свой ответ опцией бинарного поиска, которая должна быть быстрее, если вы просматриваете более 50 или около того событий.   -  person Jodrell    schedule 01.10.2012


Ответы (3)


ИЗМЕНИТЬ

На утверждение, что ответ dasblinkenlight в целом правильный, я написал быструю общую вспомогательную функцию.

public static int BinaryFirstIndexOf<T>(
    Func<int, T> indexer,
    Func<T, boo> predicate,
    int count)
{
    var low = 0;
    var high = count - 1;

    while (low < (high - 1))
    {
        var mid = low + ((high - low) / 2);

        if (predicate(indexer(mid))
        {
            high = mid;
        }
        else
        {
            low = mid;
        }
    }

    if (predicate(indexer(low)))
    {
        return low;
    }

    if (low != high && predicate(indexer(high)))
    {
        return high;
    }

    return -1;
}

Который я бы использовал так,

var time = DateTime.Now.AddDays(-1);
var c = scan.Entries;

var first = BinaryFirstIndexOf(
     i => c[i], 
     e => e.TimeGenerated > time,
     c.Count);

if (first >= 0)
{
    var result = new List<Item>(c.Count - first);
    Enumerable.Range(first, c.Count - first).AsParallel()
    .ForAll(i => 
        {
            var j = i - first;
            result[j] = (Item)c[i];
        });
}

Разве ты не хочешь сделать что-то подобное

var time = DateTime.Now.AddDays(-1);
var c = scan.Entries;

var cutOff = 0;
for (var i = c.Count - 1; i > - 1; i--)
{
    if (c[i].TimeGenerated < time)
    {
        cutOff = i;
        break;
    }
}

var result = new List<Item>(c.Count - cutOff - 1);
var j = 0;
for (var i = cutOff + 1; i < c.Count; i ++)
{
    result[j] = (Item)c[i];
    j++;
}

Я предполагаю, что нужные вам данные находятся в конце и занимают менее половины коллекции.

Или, возможно, используя linq для параллельного литья,

var time = DateTime.Now.AddDays(-1);
var c = scan.Entries;

var cutOff = 0;
for (var i = c.Count - 1; i > - 1; i--)
{
    if (c[i].TimeGenerated < time)
    {
        cutOff = i;
        break;
    }
}

var result = new List<Item>(c.Count - cutOff - 1);
Enumerable.Range(cutOff + 1, c.Count - cutOff - 1).AsParallel()
    .ForAll(i => 
        {
            var j = i - cutOff - 1;
            result[j] = (Item)c[i];
        });
person Jodrell    schedule 01.10.2012
comment
Вау, я не думал о зацикливании назад! Дуррр! Спасибо, я посмотрю на это - person user1002479; 01.10.2012
comment
Привет, Джодрелл, я только что заметил твое последнее обновление. Я попробовал это, потому что это выглядело законным, но это не сработало для меня. Я думаю, что скобка для цикла while отключена? - person user1002479; 03.10.2012
comment
@ user1002479, я сделал пару очевидных исправлений. Я не могу скомпилировать на машине, где я печатаю в веб-браузере, что раздражает. - person Jodrell; 03.10.2012
comment
Еще раз спасибо. Работал отлично - person user1002479; 03.10.2012

Ваш "запрос" неверен. На самом деле вы выбираете кучу логических значений, но не фильтруете. Вы хотите:

var l2 = c.Cast<EventLogEntry>().AsParallel().Where(n => n.time > TIME);

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

Фактически, Count имеет перегрузку, которая принимает предикат, поэтому вы можете использовать это:

var count = c.Cast<EventLogEntry>().Count(n => n.time > TIME);

Или распараллеленная версия:

var count = c.Cast<EventLogEntry>().AsParallel().Count(n => n.time > TIME);

Как и Крис, я потрясен тем, что на любую реализацию потребуется больше минуты...

person Jon Skeet    schedule 01.10.2012
comment
@KirillBestemyanov: Что ж, это может быть более быстрый и элегантный способ поиска в ICollection. Поскольку OP не проверил правильный запрос, он вполне может быть быстрым, и в этом случае это правильный подход. - person Jon Skeet; 01.10.2012
comment
Это потенциально лучшая иллюстрация того, что оптимизация является преждевременной.... - person sehe; 01.10.2012
comment
Спасибо, Джон. Я не знал, что выбираю логические значения. До 36 секунд - person user1002479; 01.10.2012
comment
Подсчет занимает 19 секунд, но мне нужны настоящие объекты - person user1002479; 01.10.2012

Это первое утверждение:

List<Item> l1 = new List<Item>();
foreach (Item i in c) { 
  if (i.time > TIME) { 
    l1.Add(i); }
}

Улучшает ли это изменение производительности (будет фильтровать список перед запуском foreach):

List<Item> l1 = new List<Item>();
foreach (Item i in c.Where(a => a.time > TIME)) { 
    l1.Add(i);
}
person Chris Dixon    schedule 01.10.2012
comment
Да, но лишь незначительно. До 40 секунд. На самом деле я не могу вызвать .Where, пока не применю его. c.Cast‹Item›.Where(...) - person user1002479; 01.10.2012