четверг, 31 марта 2011 г.

Медленный DataTable?!

Вроде бы, как же так, DataTable, специальный класс для работы с данными. Причем, с большими объемами данных. И все-таки, проблемы с производительностью могут возникнуть даже с ним!

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

Простой тест!

Создадим широкую таблицу, 100 колонок (типа String). Добавим в неё пустяковый, в общем-то, объем данных - 3000 строк. Пробежимся по всей таблице, и проставим всем ячейкам значение "test".

foreach (DataRow row in table.Rows)
  foreach (DataColumn column in table.Columns)
    row[column] = "test";

Замерим время выполнения...
00:00:01.7110978

Ого! Нехило, для такой, вроде бы простой операции!? 2 секунды на каких-то жалких 3000 строк!?

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

Вот и у нас, в одном из совершенно реальных проектов, обнаружилась широкая таблица на 70 колонок. И с ней возникла похожая проблема. Пришлось немало повозиться, чтобы локализовать эту проблему и понять, в чем дело. И еще полчаса на то, чтобы выяснить, как это исправить.

Решение оказалось довольно неожиданным. Оказывается, в случае широких таблиц, быстрее удалить целую строку и добавить новую (фактически пересоздав таблицу), чем заполнять строки стандартным способом.

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

Measure("Standard method", 1, () =>
{
    foreach (DataRow row in table.Rows)
        foreach (DataColumn column in table.Columns)
        {
            int value = Convert.ToInt32(row[column]);
            if (value > 100)
                row[column] = value * 2;
            else
                row[column] = value * 3;
        }
});

Ничего сложного. Если значение больше 100, умножаем его на 2, иначе на 3.
А теперь то же самое, но с пересозданием строк:

Measure("Row recreate method", 1, () =>
{
    for (int i = 0; i < tempTable.Rows.Count; i++)
    {
        List<object> values = new List<object>();
        foreach (DataColumn column in tempTable.Columns)
        {
            int value = Convert.ToInt32(tempTable.Rows[0][column]);
            if (value > 100)
                values.Add(value * 2);
            else
                values.Add(value * 3);
        }

        tempTable.Rows.RemoveAt(0);
        tempTable.Rows.Add(values.ToArray());
    }
});

Да уж! Выглядит похуже, а уж читается совсем плохо.
Но давайте потестируем производительность этих двух вариантов кода на таблицах различной конфигурации...

100 колонок, 3000 строк:
Standard method => 00:00:02.3351335
Row recreate method => 00:00:00.1030059


10 колонок, 30000 строк:
Standard method => 00:00:00.2970170
Row recreate method => 00:00:00.1980114


1 колонка, 300000 строк:
Standard method => 00:00:00.1740099
Row recreate method => 00:00:01.2810733


Результаты налицо: для большинства таблиц вариант с пересозданием строк будет работать очень быстро.

P. S.  Исходники метода Measure можно взять из статьи про производительность Lua Interface.

Комментариев нет:

Отправить комментарий

Внимание! Реклама и прочий спам будут беспощадно удаляться.