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

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

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

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

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

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

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

var
  Sheet: TXLSXWorksheet;
  State: TXLSXCellStyleState;
begin
  Sheet := Workbook.Sheets[1];
  // Style indexes come from the workbook style pools, e.g. from a cell
  // you have already formatted the way you want the range to look
  State := BuildStateFromTemplateCell(Sheet.Cells.Item[1, 1]);
  // Format columns B..D without creating a single empty cell object
  Sheet.StyleOverlays.Add(1, 2, MaxRowIndex, 4,
    [xfpNumberFormat, xfpAlignment], State);
end;

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

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

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

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

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

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

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

var
  Workbook: TXLSXWorkbook;
begin
  Workbook := TXLSXWorkbook.Create(nil);
  try
    Workbook.Open('ledger-2026.xlsx');
    // StreamingWrite defaults to True; turn it off only when a downstream
    // step requires the whole worksheet XML to exist before compression
    Workbook.StreamingWrite := True;
    Workbook.SaveAs('ledger-2026-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

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

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

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

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

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

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

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

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