В заметке "Оптимизация DataTable по памяти" я упоминал, что класс DataTable из BCL содержит довольно увесистую внутреннюю структуру для хранения данных. Нельзя сказать, что она избыточна, однако, для некоторых сценариев использования многие её возможности просто излишни. Вообще проблема с объёмом DataTable обычно не возникает. Класс сильно связан с получением данных из БД, а т.к. чистый ADO.NET редко кто использует напрямую без ORM, то и с DataTable`ом в чистом видел мы сталкиваемся не так часто.
Проблематика
Не так давно пришлось участвовать в проекте, частью которого был мощный табличный процессор (схожий с MS Excel). Данная система позволяла производить пересчёт ячеек и их контроль по установленным пользователем формулам. Обработка данных производилась на клиенте, а не в БД в силу различных причин. В какой-то момент встал вопрос потребления памяти. Основной объём в памяти занимали данные из справочных и смежных таблиц, которые подгружались в память частично и только для чтения. Использовать для хранения таких данных абстракции до уровня ячеек - довольно расточительно. Поэтому данные хранились в чистых DataTable`ах с небольшой примесью метаинформации.
После изучения внутреннего устройства и пары часов в обнимку с профилировщиком я установил, что DataTable в зависимости от содержимого съедает ещё 15-20% памяти. Пустой DataTable в .Net 4.5 вообще занимает почти 2Kb в памяти. Возникло желание раскулачить его структуру для получения чего-то более легковесного под мою задачу.
Внутри DataTable
Для эффективного хранения данных в исходном DataTable применено несколько интересных решений.
Так, например, данные хранятся в колонках, а не в строках, как может показаться при использовании (ведь доступ происходит именно через DataRow). Это сделано для более эффективного использования памяти: значения лежат в типизированных массивах (значения в колонках одного типа). Т.е. в int колонка значения лежат в int[]. Такой подход позволяет сэкономить не только на памяти, но ещё и на скорости (вспомним, что стоимость доступа в массиве по индексу равна o(1)). DataRow при этом имеет ссылку на саму таблицу, а через неё и на нужную колонку к своему значению (свой индекс строка, как ни странно, тоже знает). Для возможности хранения null`ов в ячейках, DataColumn (а точнее Storage, на который ссылается колонка) имеет битовую маску BitArray.
Эти идеи неплохо подойдут для написания простого контейнера, позволяющего эффективно хранить табличные данные.
Сэкономить по сравнению с DataTable можно на функционале редактирования и отслеживания состояния отдельных строк.
Реализация
Начнём с того, что новый контейнер должен уметь конструироваться из DataTable. Для этого обеспечим его специальным конструктором:
public LightDataTable(DataTable table)
Далее необходимо инициализировать внутреннюю структуру контейнера: создать нужные таблицы и строки, а также переложить в себя данные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /// <summary> /// Constructor /// </summary> /// <param name="table"><see cref="DataTable"/> that should be wrapped to <see cref="LightDataTable"/></param> public LightDataTable(DataTable table) { TableName = table.TableName; RowsCount = table.Rows.Count; var columns = new List<LightDataColumn>(table.Columns.Count); foreach (var column in table.Columns) columns.Add(CreateColumn((DataColumn)column)); Columns = new LightDataColumnCollection(columns); } |
Создание колонки выглядит следующим образом:
Как видно, для всех основных типов написаны типизированные столбцы, каждый из которых внутри себя хранит данные в типизированных массивах.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /// <summary> /// Light column factory /// </summary> /// <param name="column"><see cref="DataColumn"/> that should be wrapped</param> /// <returns>Light column</returns> private LightDataColumn CreateColumn(DataColumn column) { var type = column.DataType; if (type == typeof(string)) return new LightStringDataColumn(column, this); if (type == typeof(Guid)) return new LightGuidDataColumn(column, this); if (type == typeof(Int16)) return new LightInt16DataColumn(column, this); if (type == typeof(Int32)) return new LightInt32DataColumn(column, this); if (type == typeof(Int64)) return new LightInt64DataColumn(column, this); if (type == typeof(Decimal)) return new LightDecimalDataColumn(column, this); if (type == typeof(bool)) return new LightBooleanDataColumn(column, this); if (type == typeof(DateTime)) return new LightDateTimeDataColumn(column, this); if (type == typeof(byte)) return new LightByteDataColumn(column, this); if (type == typeof(byte[])) return new LightByteArrayDataColumn(column, this); if (type == typeof(double)) return new LightDoubleDataColumn(column, this); throw new NotImplementedException($"Type {type} is not supported."); } |
Как видно, для всех основных типов написаны типизированные столбцы, каждый из которых внутри себя хранит данные в типизированных массивах.
Тут вступила в дело моя природная лень и опасение писать велосипеды. Поэтому я использовал для хранения те же storage классы, что и DataTable. Для этого пришлось немного замарать руки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /// <summary> /// Initialize <see cref="LightDataColumn"/> from <see cref="DataColumn"/> /// </summary> /// <param name="column">Source column</param> /// <param name="storage">Existing storage</param> protected override void Initialize(DataColumn column, object storage) { if (Table.RowsCount > 0) Array.Copy((string[])ValuesField.GetValue(storage), _values = new string[Table.RowsCount], Table.RowsCount); } /// <summary> /// Provides access to <see cref="System.Data.Common.StringStorage"/> /// </summary> private static readonly FieldInfo ValuesField = typeof(DataTable).Assembly.GetType("System.Data.Common.StringStorage").GetField("values", BindingFlags.Instance | BindingFlags.NonPublic); |
Что имеем?
Тесты по памяти показали, что данный контейнер позволяет сэкономить до 15% памяти по сравнению с DataTable. В купе с подходом по принудительному интернированию строк, описанному ранее, выигрыш иногда достигал 60% в зависимости от характера данных.
Хотя спору нет - задача довольно специфичная. И если вы полезли работать напрямую с DataTable в обход современных ORM, то в 95% случаев вы что-то делаете не так :P
Хотя спору нет - задача довольно специфичная. И если вы полезли работать напрямую с DataTable в обход современных ORM, то в 95% случаев вы что-то делаете не так :P
Комментариев нет:
Отправить комментарий