Как оптимизировать код с помощью DataTable и Linq?

У меня есть 2 таблицы данных. Имеется около 17000 (таблица1) и 100000 (таблица2) записей.

Необходимо проверить, содержит ли поле "FooName" "ItemName". Также необходимо взять "FooId", а затем добавить "ItemId" и "FooId" в ConcurrentDictionary.

У меня есть этот код.

DataTable table1;
DataTable table2;           
var table1Select = table1.Select();

ConcurrentDictionary<double, double> compareDictionary = new ConcurrentDictionary<double, double>();

foreach (var item in table1)
{
         var fooItem = from foo in table2.AsEnumerable()
                  where foo.Field<string>("FooName").Contains(item.Field<string>("ItemName"))
                  select foo.Field<double>("FooId");
         if(fooItem != null && fooItem.FirstOrDefault() != 0)
                {
                    compareDictionary.TryAdd(item.Field<double>("ItemId"), fooItem.FirstOrDefault());
                }
}

Работает медленно (на выполнение задачи уходит около 10 минут).

Я хочу сделать это быстрее. Как я могу его оптимизировать?


person Yulia Koval    schedule 05.03.2020    source источник
comment
ItemName это одно слово или фраза?   -  person mtkachenko    schedule 05.03.2020
comment
@mtkachenko это фраза.   -  person Yulia Koval    schedule 05.03.2020


Ответы (2)


Я вижу три точки, которые вы можете атаковать:

  1. откажитесь от строгой типизации в методах доступа к полям в пользу прямого приведения: это вызывает распаковку, которой вы можете полностью избежать, если doubles является типом значения. upd, как указано в комментариях, вы не избежите распаковки в любом случае, но потенциально можете сэкономить некоторые накладные расходы на вызов метода (что опять же спорно). Этот момент, вероятно, можно игнорировать
  2. кешировать ссылочную строку, чтобы вы обращались к ней только один раз за внешний цикл
  3. (я думаю, что это самый большой выигрыш), поскольку вы, кажется, всегда берете первый результат - выберите FirstOrDefault() прямо в LINQ - не позволяйте ему перечислять все это, когда совпадение найдено
ConcurrentDictionary<double, double> compareDictionary = new ConcurrentDictionary<double, double>();

foreach (var item in table1)
    {
        var sample = (string)item["ItemName"]; // cache the value before looping through inner collection
        var fooItem = table2.AsEnumerable()
                            .FirstOrDefault(foo => ((string)foo["FooName"]).Contains(sample)); // you seem to always take First item, so you could instruct LINQ to stop after a match is found
        if (fooItem != null && (double)fooItem["FooId"] != 0)
        {
            compareDictionary.TryAdd((double)item["ItemId"], (double)fooItem["FooId"]);
        }
    }

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

person timur    schedule 05.03.2020
comment
Я не верю, что это правда. DataRow.Field<T> для типа значения в конечном итоге вернет (T)object, который вызовет IL unbox.any !T точно так же, как (T)DataRow[fieldname]. Однако это устраняет некоторые накладные расходы на вызов метода - хотя, конечно, JIT может также встроить их. - person NetMage; 06.03.2020
comment
@NetMage Я склонен согласиться. Сначала я быстро пробежался по декомпилированному коду, и первым вызовом в очереди был Unbox‹T›, который как бы подсказал вывод. Затем я еще раз посмотрел, и кажется, что распаковка неизбежна. Кажется, что выполнение приведения по-прежнему устранит несколько нулевых и DBNull-проверок. Чуть позже обновлю ответ, чтобы не вводить в заблуждение - person timur; 06.03.2020

Если вы готовы пожертвовать памятью ради скорости, преобразование из DataTable для нужных вам полей дает примерно 6-кратное ускорение по сравнению с повторным извлечением данных столбца из table2. (Это в дополнение к ускорению от использования FirstOrDefault.)

var compareDictionary = new ConcurrentDictionary<double, double>();

var t2e = table2.AsEnumerable().Select(r => (FooName: r.Field<string>("FooName"), FooId: r.Field<double>("FooId"))).ToList();
foreach (var item in table1.AsEnumerable().Select(r => (ItemName: r.Field<string>("ItemName"), ItemId: r.Field<double>("ItemId")))) {
    var firstFooId = t2e.FirstOrDefault(foo => foo.FooName.Contains(item.ItemName)).FooId;

    if (firstFooId != 0.0) {
        compareDictionary.TryAdd(item.ItemId, firstFooId);
    }
}

Я использую С# ValueTuples, чтобы избежать накладных расходов на ссылочные объекты из анонимных классов.

person NetMage    schedule 06.03.2020