Техническая статья

Блочное хранение ячеек по строкам и потоковое сохранение XLSX в HotXLS

HotXLS хранит ячейки листа в компактных блоках по 256 строк, разрешает форматирование строк, столбцов и прямоугольников через отложенные интервальные накладки вместо создания объектов ячеек и стримит каждую строку прямо в deflate-поток пакета при сохранении. Вместе эти три изменения определяют профиль памяти большой книги: пиковое потребление следует за крупнейшей отдельной строкой, а не за размером полного XML листа

Причина, по которой это важно, — форма, с которой рано или поздно сталкивается каждый разработчик таблиц. Пользователь форматирует весь столбец — один клик, один миллион ячеек — и наивная объектная модель отвечает выделением миллиона объектов ячеек, чтобы хранить один индекс числового формата. Файл на диске остаётся крошечным, потому что формат XLSX выражает это одной записью <col>. Процесс крошечным вовсе не остаётся

Почему форматирование столбца стоит больше памяти, чем его заполнение?

Потому что у форматирования нет данных, оправдывающих объект. Ячейка со значением обязана существовать где-то. Ячейка, что пуста, но стилизована, существует лишь ради переноса индекса стиля, а материализовать миллионы таких — классический способ, которым Delphi-приложение таблиц выбегает из адресного пространства на файле, что Excel открывает мгновенно

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

Диаграмма наложения интервальных стилей для HotXLS: диапазон форматирования столбца хранится одной записью с заявленными частями формата вместо миллиона пустых объектов ячеек в Delphi
Формат столбца, строки или прямоугольника живёт один раз как запись-наложение интервала, заявляющая только принадлежащие ей части. Эффективный стиль собирается лениво, когда ячейка внутри диапазона реально тронута впервые
var
  Sheet: TXLSXWorksheet;
  State: TXLSXCellStyleState;
begin
  Sheet := Workbook.Sheets[1];
  // Индексы стилей берутся из пулов стилей книги, например из ячейки,
  // которую вы уже отформатировали так, как хотите, чтобы выглядел диапазон
  State := BuildStateFromTemplateCell(Sheet.Cells.Item[1, 1]);
  // Форматируйте колонки B..D без создания ни одного пустого объекта ячейки
  Sheet.StyleOverlays.Add(1, 2, MaxRowIndex, 4,
    [xfpNumberFormat, xfpAlignment], State);
end;

TXLSXFormatParts — то множество, что решает, что вносит накладка: xfpFont, xfpFill, xfpBorder, xfpNumberFormat, xfpAlignment и xfpProtection. Назвать лишь те части, что вы имеете в виду, и позволяет накладкам разумно наслаиваться: столбцовая накладка, поставляющая числовой формат, не спорит со строковой, поставляющей заливку, потому что ни одна не претендует на часть другой

Что даёт блок в 256 строк

Локальность. Ячейки держатся в блоках по 256 строк со стабильными публичными дескрипторами, сериализуются построчно, поэтому запись листа обходит память в том порядке, в котором будет выпускать байты, а не гоняется по указателям сквозь кучу. Стабильные дескрипторы важны для API: дескриптор, который держит вызывающий, остаётся валидным сквозь внутреннюю реорганизацию, выполняемую блочной раскладкой, и именно это делает компактное представление деталью реализации, а не ломающим изменением

Диаграмма блочного хранения строк: ячейки листа HotXLS держатся в фиксированных блоках по 256 строк со стабильными дескрипторами Delphi и сериализацией по строкам в deflate-поток XLSX
Ячейки сидят в фиксированных блоках по 256 строк, выделяющих только занятые строки и сериализующихся в том же порядке, каким байты покидают процесс. Публичные дескрипторы переживают внутреннюю реорганизацию, поэтому блочная раскладка остаётся невидимой для вызывающих

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

Строковое потоковое сохранение и когда оно не применяется

С включённым StreamingWrite — по умолчанию — каждая строка листа пишется прямо в deflate-поток пакета. Альтернатива, именно то, что отключает флаг, сначала строит полный XML листа и лишь затем сжимает, поэтому пиковая память масштабируется размером всего листа. Стриминг делает её масштабируемой одной строкой

Общие строки и вспомогательные части следуют той же дисциплине через один переиспользуемый UTF-8-сериализатор, выпускающий записи по одной, ограничивая пиковую память крупнейшей отдельной записью, а не целой частью. Это покрывает таблицу общих строк и записи сводных таблиц, которые на широком аналитической книге часто больше любого отдельного листа

var
  Workbook: TXLSXWorkbook;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.Open('ledger-2026.xlsx');
    // StreamingWrite по умолчанию равен True; отключайте его только когда
    // нижестоящему шагу требуется, чтобы весь XML листа существовал до сжатия
    Workbook.StreamingWrite := True;
    Workbook.SaveAs('ledger-2026-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Оставьте его включённым, если нет конкретной причины выключить. Не-стриминговый путь существует для случаев, когда что-то ещё в конвейере нуждается в собранном XML, а платить за это по умолчанию — платить за случай, в который большинство приложений никогда не попадает

Как узнать, что накладки на деле применяются

Смотрите на счётчик ячеек, а не на график памяти. Если лист сообщает правдоподобное число физических ячеек после того, как вы применили широкое форматирование, накладки свою работу делают. Если счётчик подпрыгивает на размер форматированного диапазона, что-то в пути кода материализовало ячейки — обычно цикл, читающий каждую ячейку в диапазоне для проверки её стиля, что принуждает разрешение по одной ячейке и ломает всё построение

Диаграмма StreamingWrite: построчный deflate-стриминг в HotXLS против сборки всего XML листа в памяти перед сжатием в Delphi
При включённом StreamingWrite каждая строка течёт прямо в deflate-поток пакета, а пиковая память следит за одной строкой. Выключение флага сперва собирает весь XML листа целиком, что масштабирует пиковую память с полным листом

Разрешайте стиль, когда нужен эффективный формат одной ячейки. Не разрешайте стиль для миллиона ячеек, чтобы узнать, что у столбца числовой формат; спросите накладку. То же правило применимо к записи: назначайте значения тем ячейкам, у которых они есть, а форматирование пусть остаётся интервалом

Куда уходит оставшаяся память

Когда ячейки и стили компактны, следующие крупнейшие потребители большой книги — таблица общих строк и те сопутствующие части, что несёт файл: кэши сводных таблиц, графика, сохранённый XML из частей, не моделированных объектной моделью. Для каждой из них есть свои стратегии, и честный ответ: ни одна настройка не решает их все разом

Если ваше узкое место — открытие, а не сохранение, рычаг в избирательной загрузке: разбор загрузки только по метаданным и с выбором листов описывает чтение книги без платы за листы, которых вы не коснётесь. Для пропускной способности пути чтения на очень больших файлах см. заметки о параллельном разборе XLSX и распределителе памяти, а для рабочих процессов только на вывод, которым объектная модель не нужна вовсе, потоковая запись для серверных пакетных заданий обычно подходит лучше любой настройки здесь

HotXLS читает и пишет XLS и XLSX из нативного кода Delphi и C++Builder без установленного Excel и без OLE-автоматизации, что и делает эти характеристики памяти наблюдаемыми и управляемыми — на странице компонента HotXLS перечислены поддерживаемые форматы и версии RAD Studio