Самый эффективный способ извлечь все элементы справочника из списка ключей?


У меня есть экземпляр c# Dictionary<DateTime,SomeObject>.

У меня есть следующий код:

private Dictionary<DateTime, SomeObject> _containedObjects = ...;//Let's imagine you have ~4000 items in it

public IEnumerable<SomeObject> GetItemsList(HashSet<DateTime> requiredTimestamps){
    //How to return the list of SomeObject contained in _containedObjects
    //Knowing that rarely(~<5% of the call), one or several DateTime of "requiredTimestamps" may not be in _containedObjects
}

Я ищу, как вернуть IEnumerable<SomeObject>, содержащий все элементы, на которые ссылается один из предоставленных ключей. Единственная проблема заключается в том, что этот метод будет вызываться очень часто, и мы не всегда можем иметь каждый заданный ключ в параметре.

Так есть ли что-то более эффективное, чем это:

private Dictionary<DateTime, SomeObject> _containedObjects = ...;//Let's imagine you have ~4000 items in it

public IEnumerable<SomeObject> GetItemsList(HashSet<DateTime> requiredTimestamps){
    List<SomeObject> toReturn = new List<SomeObject>();
    foreach(DateTime dateTime in requiredTimestamps){
        SomeObject found;
        if(_containedObjects.TryGetValue(dateTime, out found)){
            toReturn.Add(found);
        }
    }
    return toReturn;
}
4 2

4 ответа:

Метод 1: Чтобы сделать это значительно быстрее - это не путем изменения алгоритма, а путем создания локальной копии _containedObjects в вашем методе и ссылки на локальную копию для поиска.

Пример:

public static IEnumerable<int> GetItemsList3(HashSet<DateTime> requiredTimestamps)
{
    var tmp = _containedObjects;

    List<int> toReturn = new List<int>();
    foreach (DateTime dateTime in requiredTimestamps)
    {
        int found;

        if (tmp.TryGetValue(dateTime, out found))
        {
            toReturn.Add(found);
        }
    }
    return toReturn;
}

тестовые данные и время (на наборе из 5000 элементов С 125 найденными ключами):
Ваш исходный метод (миллисекунды): 2,06032186895335
Способ 1 (миллисекунды): 0,53549626223609

Метод 2: Один из способов сделать это незначительно быстрее перебирать меньшее множество и выполнять поиск по большему набору. В зависимости от разницы в размерах вы получите некоторую скорость.

Вы используете словарь и хэш-набор, поэтому ваш поиск по любому из них будет O(1).

Пример: если _containedObjects содержит меньше элементов, чем requiredTimestamps, мы проходим через _containedObjects (в противном случае используйте свой метод для обратного)

public static IEnumerable<int> GetItemsList2(HashSet<DateTime> requiredTimestamps)
{
    List<int> toReturn = new List<int>();
    foreach (var dateTime in _containedObjects)
    {
        int found;

        if (requiredTimestamps.Contains(dateTime.Key))
        {
            toReturn.Add(dateTime.Value);
        }
    }
    return toReturn;
}

тестовые данные и время (на множестве 5000 для _containedObjects и множестве 10000 элементов для requiredTimestamps с Найдено 125 ключей):
Ваш исходный метод (миллисекунды): 3,88056291367086
Способ 2 (миллисекунды): 3,31025939438943

В общем, есть два способа сделать это:

  1. последовательно просмотрите requiredTimestamps и найдите каждую отметку даты/времени в словаре. Поиск по словарю - это O(1), поэтому если есть элементы k, которые нужно искать, это займет O (k) времени.
  2. последовательно просмотрите словарь и извлеките те, которые имеют соответствующие ключи в наборе хэшей requiredTimestamps. Это займет O (n) времени, где n - количество элементов в словаре.

в теории , первый вариант-который у вас в настоящее время есть-будет самым быстрым способом сделать это.

На практике, скорее всего, первый вариант будет более эффективным, когда количество элементов, которые вы ищете, меньше некоторого процента от общего количества элементов в словаре. То есть, если вы ищете 100 ключей в словаре из миллиона, первый вариант почти наверняка будет быстрее. Если вы ищете 500 000 ключей в словаре из миллиона, второй метод может быть быстрее, потому что перейти к следующему ключу намного быстрее, чем выполнить поиск.

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

Одна из оптимизаций, которую вы можете рассмотреть, - это предварительный размер выходного списка. Это позволит избежать перераспределения ресурсов. Итак, когда вы создаете свой список toReturn:

List<SomeObject> toReturn = new List<SomeObject>(requiredTimestamps.Count);

Вы можете использовать LINQ, но я сомневаюсь, что он увеличит производительность, даже если есть какая-то разница, она будет незначительной.

Ваш метод может быть:

public IEnumerable<SomeObject> GetItemsList(HashSet<DateTime> requiredTimestamps)
{
    return _containedObjects.Where(r => requiredTimestamps.Contains(r.Key))
                            .Select(d => d.Value);
}

Один положительный момент в этом-ленивая оценка, так как вы не заполняете список и не возвращаете его.

Вот несколько различных способов сделать это-производительность все в значительной степени одинакова, так что вы можете выбрать на основе читабельности.

Вставьте это в LinqPad, если вы хотите протестировать его - в противном случае просто соберите любой код, который вам нужен.

Я думаю, что мой личный фаворит с точки зрения читаемости-это метод 3. Метод 4, безусловно, читаем, но имеет неприятную особенность, что он делает два поиска в словаре для каждой требуемой метки времени.

void Main()
{
    var obj = new TestClass<string>(i => string.Format("Element {0}", i));

    var sampleDateTimes = new HashSet<DateTime>();
    for(int i = 0; i < 4000 / 20; i++)
    {
        sampleDateTimes.Add(DateTime.Today.AddDays(i * -5));
    }
    var result = obj.GetItemsList_3(sampleDateTimes);
    foreach (var item in result)
    {
        Console.WriteLine(item);
    }
}

class TestClass<SomeObject>
{
    private Dictionary<DateTime, SomeObject> _containedObjects;

    public TestClass(Func<int, SomeObject> converter)
    {
        _containedObjects = new Dictionary<DateTime, SomeObject>();
        for(int i = 0; i < 4000; i++)
        {
            _containedObjects.Add(DateTime.Today.AddDays(-i), converter(i));
        }
    }

    public IEnumerable<SomeObject> GetItemsList_1(HashSet<DateTime> requiredTimestamps)
    {
        List<SomeObject> toReturn = new List<SomeObject>();
        foreach(DateTime dateTime in requiredTimestamps)
        {
            SomeObject found;
            if(_containedObjects.TryGetValue(dateTime, out found))
            {
                toReturn.Add(found);
            }
        }
        return toReturn;
    }

    public IEnumerable<SomeObject> GetItemsList_2(HashSet<DateTime> requiredTimestamps)
    {
        foreach(DateTime dateTime in requiredTimestamps)
        {
            SomeObject found;
            if(_containedObjects.TryGetValue(dateTime, out found))
            {
                yield return found;
            }
        }
    }    

    public IEnumerable<SomeObject> GetItemsList_3(HashSet<DateTime> requiredTimestamps)
    {
        return requiredTimestamps
            .Intersect(_containedObjects.Keys)
            .Select (k => _containedObjects[k]);
    }

    public IEnumerable<SomeObject> GetItemsList_4(HashSet<DateTime> requiredTimestamps)
    {
        return requiredTimestamps
            .Where(dt => _containedObjects.ContainsKey(dt))
            .Select (dt => _containedObjects[dt]);
    }
}