Artículo técnico

Almacenamiento de celdas por bloques de filas en HotXLS

En SpreadsheetML una columna que lleva un formato de moneda necesita exactamente un elemento: <col min="2" max="2" style="3"/>. ECMA-376 Part 1 adjunta un estilo de celda solo en tres lugares: una columna (§18.3.1.13), una fila (§18.3.1.73) y una celda (§18.3.1.4). Restrinja el formato a B2:D500000 y SpreadsheetML no tiene un solo elemento para esa forma: un estilo de columna correría más allá de la fila 500000 y un estilo de fila cubriría cada columna, así que un escritor acaba emitiéndolo como celdas de solo estilo, 1.499.997 de ellas

Un modelo de hoja de cálculo en memoria choca con la misma pared un paso antes. Si formatear significa poner una propiedad en cada objeto de celda, un clic en un encabezado de columna asigna un millón de objetos para cargar un pequeño entero, y un proceso Delphi que solo quería un formato de número se queda sin espacio de direcciones en un libro que en disco son unos pocos kilobytes. La pregunta real de diseño es dónde vive el formato mientras usted edita, y cuándo se convierte en datos por celda

Cómo HotXLS formatea un rango sin crear celdas

HotXLS conserva el formato de rangos como intervalos. Cada setter de estilo en TXLSXRange — SetNumberFormat, SetNumberFormatIndex, SetFontIndex, SetFillIndex, SetBorderIndex, SetAlignmentIndex — agrega un registro a la lista StyleOverlays de la hoja: cuatro coordenadas, las partes de estilo que la llamada posee y los índices de pool para esas partes. Las celdas que ya existen dentro del rectángulo levantan las partes nuevas de inmediato. Las coordenadas vacías se quedan vacías, y no se asigna nada para ellas

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Edge: TXLSXBorder;
  BorderIdx, Row: Integer;
  Stats: TXLSXCellStorageStatistics;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Sheet := Workbook.Sheets.Add('Ledger');
    Sheet.WriteStringRowValues(1, 2, ['Net', 'Tax', 'Gross']);
    for Row := 2 to 1001 do
      Sheet.WriteFloatRowValues(Row, 2, [Row * 1.5, Row * 0.3, Row * 1.8]);

    // Columnas B..D sobre las 1.048.576 filas: un intervalo por llamada
    Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetNumberFormat('#,##0.00 "EUR"');

    // Construya el borde una vez, y aplíquelo por índice
    Edge := TXLSXBorder.Create;
    try
      Edge.SetAll(xlsxBorderThin);
      BorderIdx := Workbook.Borders.LookupOrAdd(Edge); // 1-based
    finally
      Edge.Free;
    end;
    Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetBorderIndex(BorderIdx);

    Stats := Sheet.Cells.GetStorageStatistics;
    // Espere 3.003: tres celdas de encabezado más 3.000 valores
    WriteLn('Cells: ', Stats.CellCount);
    if Workbook.SaveAs('ledger.xlsx') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Workbook.Free;
  end;
end;

Las dos llamadas de formato cubrieron 3.145.728 coordenadas y no agregaron nada a ese conteo. Los intervalos empiezan en la fila 1 a propósito: un intervalo de columna completa se guarda como un elemento <col>, y los encabezados de texto en la fila 1 de todas formas no se ven afectados por un formato de número. Los rangos devueltos por RCRange, Range['A1:C5'], EntireColumn y EntireRow son propiedad de la hoja, así que el ejemplo jamás los libera

TXLSXFormatParts es lo que evita que varios intervalos se peleen. Sus miembros son xfpFont, xfpFill, xfpBorder, xfpNumberFormat, xfpAlignment, xfpProtection y xfpCellControl, y cada setter de TXLSXRange reclama exactamente uno de ellos. Cuando una celda se resuelve, los intervalos que la cubren se aplican en el orden en que se agregaron, cada uno escribiendo solo sus propias partes, y el formato explícito en la propia celda les gana a todos. Un intervalo de columna que fija el formato de número y un intervalo de fila que fija un relleno ambos llegan intactos a la celda B5

Cuando un registro debe cargar varias partes, Sheet.StyleOverlays.Add(Row1, Col1, Row2, Col2, Parts, State) toma un TXLSXCellStyleState con los índices de pool rellenados. Add acota las coordenadas a la cuadrícula de 1.048.576 × 16.384, intercambia esquinas invertidas, y devuelve nil sin registrar nada cuando Parts está vacío. Las ediciones estructurales mantienen los registros al día: InsertRows dentro de un bloque formateado ensancha el intervalo por el conteo insertado, y insertar encima desplaza el registro completo hacia abajo

Diagrama de superposición de estilos por intervalo para HotXLS mostrando un rango de formato de columna almacenado como un registro con partes de formato reclamadas en lugar de un millón de objetos de celda vacía en Delphi
Un formato de columna, fila o rectángulo vive una vez como un registro de superposición por intervalo que reclama solo las partes que posee. El estilo efectivo se ensambla perezosamente la primera vez que una celda dentro del rango se toca de verdad

¿Por qué SetBorders crea celdas cuando SetBorderIndex no?

TXLSXRange.SetBorders(Kind, Style, Color) tiene que mirar cada celda por su cuenta, porque la respuesta depende de la posición. Con xlsxEdgeOutline la fila superior recibe un borde superior, la columna izquierda un borde izquierdo, y el interior nada; con xlsxEdgeInside es más o menos al revés. La llamada también fusiona los bordes nuevos en cualquier borde que la celda ya tenga, así que Outline seguido de Inside construye una cuadrícula completa. Esa composición por celda pasa por Cells[R, C], que crea una celda cuando no existe. En un bloque de reporte de 10 × 40 esto es exactamente correcto. En EntireColumn(3) con xlsxEdgeAll son 1.048.576 objetos de celda nuevos

Así que la división es simple. Para cualquier cosa más alta que una pantalla de datos, construya el borde una vez y aplíquelo por índice, como hace el primer ejemplo: TXLSXBorder.SetAll, luego Workbook.Borders.LookupOrAdd para un índice 1-based, luego SetBorderIndex. Reserve SetBorders para bloques cuyo contorno y líneas internas difieren, como el bloque de reporte de 10 × 40 de arriba

Leer los formatos de vuelta sin deshacer el trabajo

La misma trampa atrapa al código que solo lee. Cells[R, C] es un accesor que crea al fallar: pregúntele el formato de número de B500000 y obtiene la respuesta correcta más una celda nueva, con estilo y vacía en B500000 que ahora se almacenará y guardará. Ponga ese bucle sobre una columna para auditar el formato y habrá reconstruido el millón de objetos que el intervalo quería evitar, una lectura de apariencia inocente a la vez

TXLSXWorksheet.GetCellVisibleFormat resuelve el estilo efectivo de una coordenada sin agregar una celda. Fusiona el predeterminado de la hoja, los intervalos de fila y columna, los intervalos de rectángulo, los estilos de tabla y cualquier formato explícito en la celda. El overload con un parámetro out ABuiltinNumberFormat reporta un formato integrado de Excel como 14 (fecha corta) por separado de NumberFormatIndex, que apunta al pool NumberFormats personalizado del libro. Cells.HasCell(R, C) responde si existe en absoluto una celda física

var
  Sheet: TXLSXWorksheet;
  Before, After: TXLSXCellStorageStatistics;
  State: TXLSXCellStyleState;
  Row, Builtin, Unformatted, Physical: Integer;
begin
  Sheet := Workbook.Sheets[1];  // Sheets es 1-based
  Before := Sheet.Cells.GetStorageStatistics;
  Unformatted := 0;
  Physical := 0;
  for Row := 1 to 200000 do
  begin
    // Estilo efectivo, resuelto sin agregar una celda a la hoja
    State := Sheet.GetCellVisibleFormat(Row, 2, Builtin);
    if (State.NumberFormatIndex = 0) and (Builtin = 0) then
      Inc(Unformatted);
    if Sheet.Cells.HasCell(Row, 2) then
      Inc(Physical);
    // Sheet.Cells[Row, 2].NumberFormatIndex responde la misma pregunta,
    // pero deja un objeto de celda nuevo detrás por cada fila que visita
  end;
  After := Sheet.Cells.GetStorageStatistics;
  // Ambos conteos deberían ser idénticos antes y después del bucle
  WriteLn(Format('cells %d -> %d, styled blanks %d -> %d',
    [Before.CellCount, After.CellCount,
     Before.StyledBlankCellCount, After.StyledBlankCellCount]));
end;

Corra una verificación así una vez contra su carga de trabajo real. StyledBlankCellCount cuenta celdas que tienen estilo y ningún valor, fórmula o texto enriquecido. Si sube durante una pasada que se suponía de solo lectura, algún helper está creando celdas puramente para cargar estilo, típicamente llamando a Cells[R, C] para decidir si una celda necesita formato

Qué significan los bloques de 256 filas para su código

Las celdas físicas viven en bloques de 256 filas. Dentro de un bloque, cada fila poblada guarda sus celdas en una lista ordenada por columna; una fila sin celdas es solo una ranura vacía, y un tramo de 256 filas sin celdas no tiene ningún bloque. Una hoja dispersa con datos en las filas 3, 7 y 512 por tanto sostiene dos bloques. El guardado recorre la misma estructura en orden fila-mayor, que es el orden que SpreadsheetML exige para los elementos <row> y sus hijos <c>

Diagrama de almacenamiento por bloques de filas mostrando las celdas de hoja de HotXLS sostenidas en bloques fijos de 256 filas con handles Delphi estables y serialización fila-mayor al stream deflate del XLSX
Las celdas se sientan en bloques fijos de 256 filas que asignan solo filas ocupadas y se serializan en el mismo orden en que los bytes salen del proceso. Los handles públicos sobreviven a la reorganización interna, así que la distribución de bloques queda invisible para los llamadores

La referencia TXLSXCell que usted sostiene sigue siendo el mismo objeto mientras otras celdas se agregan o quitan a su alrededor; los bloques reorganizan su propia contabilidad, no sus handles. Los escritores en masa como WriteFloatRowValues y WriteStringRowValues toman una ruta más rápida en una hoja fresca y añaden valores fila-mayor a un almacén empaquetado. El primer acceso por celda vía Cells[R, C] convierte ese almacén en objetos de celda una sola vez. GetStorageStatistics muestra en qué estado está a través de PackedStorageActive y PackedCellCount, junto a RowBlockCount y PopulatedRowCount

Los pools de estilo se podan al momento de guardar. Antes de que styles.xml se escriba, HotXLS reconstruye los pools de fuente, relleno, borde, formato de número, alineación y protección para que contengan solo entradas aún en uso, y un libro que un usuario re-estilizó durante una hora no distribuye cada relleno abandonado. Las celdas ancladas a un FormatIndex absoluto conservan su ranura intacta

Guardados con streaming por filas y los números que lo muestran

TXLSXWorkbook.StreamingWrite vale True por defecto. Con él activo, SaveAs genera cada <row> de hoja y lo escribe directo al stream deflate de su entrada ZIP, así que el pedazo más grande de XML de hoja en memoria en cualquier momento es una sola fila. Con él apagado, el XML de cada hoja se construye primero como una cadena completa y se comprime después, y la memoria pico sigue a la hoja más grande. Las celdas de solo estilo en que se convierten los intervalos de rectángulo, el caso B2:D500000 de la apertura, se generan dentro de ese bucle por fila también, así que agregan al archivo pero jamás se sientan en memoria como un bloque de XML

Las shared strings y los registros de pivot cache siguen el mismo patrón. Cada entrada <si> o <r> se genera y se escribe por su cuenta, así que en un libro analítico ancho la tabla de shared strings, frecuentemente más grande que cualquier hoja individual, jamás tiene que existir como un bloque de XML

Diagrama de StreamingWrite comparando el streaming deflate por fila de HotXLS contra ensamblar el XML completo de la hoja en memoria antes de comprimir en Delphi
Con StreamingWrite activo, cada fila fluye directo al stream deflate del paquete y la memoria pico sigue a una fila. Apagar la bandera primero ensambla el XML completo de la hoja, lo que escala la memoria pico con la hoja entera

Nada de esto hay que tomarlo a la ciega. Después de cada guardado el libro expone cuatro propiedades de solo lectura: LastSaveUsedStreaming, LastSavePeakRowXmlChars (la fila única más larga, en caracteres), LastSaveBufferedWorksheetXmlChars (el XML total de hoja sostenido en memoria, cero cuando hay streaming) y LastSavePeakAuxiliaryXmlChars (la entrada de shared-string o pivot más larga). LastSaveUsedStreaming es True para salida .xlsx, .xltx y .xltm y para el escritor binario .xlsb; la salida OpenDocument va por su propio escritor y reporta False

procedure ReportSave(Workbook: TXLSXWorkbook; const FileName: string;
  Streaming: Boolean);
begin
  Workbook.StreamingWrite := Streaming;
  if Workbook.SaveAs(FileName) <> 1 then
    raise Exception.CreateFmt('Save failed: %s', [FileName]);
  WriteLn(Format('streaming=%s peakRow=%d buffered=%d peakAux=%d',
    [BoolToStr(Workbook.LastSaveUsedStreaming, True),
     Workbook.LastSavePeakRowXmlChars,
     Workbook.LastSaveBufferedWorksheetXmlChars,
     Workbook.LastSavePeakAuxiliaryXmlChars]));
end;

// El mismo libro, ambos modos; observe la memoria del proceso en su profiler
ReportSave(Workbook, 'ledger-stream.xlsx', True);
ReportSave(Workbook, 'ledger-buffered.xlsx', False);

Sobre sus propios datos, la línea buffered muestra una cifra buffered del orden del XML de la hoja completa, mientras la línea streaming muestra cero y un peakRow no mayor que su fila más ancha. Deje StreamingWrite activo. La bandera decide solo si el XML de hoja se ensambla por completo antes de comprimir, y LastSaveBufferedWorksheetXmlChars reporta el tamaño de ese ensamblado en caracteres

A dónde va la memoria restante

Una vez que celdas y estilos están compactos, los siguientes consumidores más grandes en un libro grande son la tabla de shared strings y las partes satélite que el archivo cargue: pivot caches, dibujos y XML preservado de partes que el modelo de objetos no modela. Esas tienen sus propias palancas, y ninguna configuración única las cubre a todas

Si abrir es el cuello de botella en lugar de guardar, la carga selectiva es el lugar para empezar: el recorrido de carga de hojas solo con metadatos y selectiva cubre leer un libro sin pagar por hojas que usted no tocará. Para throughput de lectura en archivos muy grandes, vea las notas sobre parseo paralelo de XLSX y el allocador de memoria, y para trabajos de solo salida que nunca necesitan un modelo de objetos, escrituras con streaming para trabajos por lotes de servidor encaja mejor que cualquier cantidad de ajuste aquí

HotXLS lee y escribe XLS y XLSX desde código Delphi y C++Builder nativo sin Excel ni automatización OLE, así que cada número de arriba viene de su propio proceso y puede verificarse ahí. Los formatos soportados y las versiones de RAD Studio están listados en la página del componente de hojas de cálculo HotXLS para Delphi