Perfilen un servicio Delphi mientras guarda un libro de 400,000 filas y la sorpresa rara vez es el bucle que llena celdas: es la llamada SaveAs. Con el escritor predeterminado, cada hoja se serializa en una cadena XML en memoria antes de comprimirse dentro del zip OOXML, y en una hoja ancha esa cadena transitoria puede superar ampliamente al modelo de celdas del que salió. El trabajo que construía cómodamente sus datos en una meseta de 800 MB se dispara después por encima del límite de 2 GB del contenedor durante el guardado, y el OOM killer redacta el reporte de bug a las 03:00. HotXLS, la biblioteca nativa de hojas de cálculo de losLab para Delphi y C++Builder, atiende exactamente esto con su modo StreamingWrite, y lo combina con callbacks de escritura por fila y un modelo de pool de estilos que premia un poco de disciplina en bucles ajustados.
Qué almacena en búfer la ruta de guardado predeterminada, y qué cambia StreamingWrite
El escritor XLSX predeterminado favorece la simplicidad: renderiza por completo el XML de la hoja y después entrega la cadena terminada al compresor zip. Es la decisión correcta para la inmensa mayoría de libros, donde el XML de toda la hoja cabe en unos pocos megabytes. Deja de ser correcta cuando la forma serializada de una hoja llega a cientos de megabytes: el XML de hoja de cálculo es verboso, cada celda numérica cuesta decenas de caracteres de marcado, y la cadena que lo contiene debe ser contigua. La firma de diagnóstico es inconfundible en una gráfica de memoria: una larga meseta plana mientras se llenan filas, luego un pico triangular agudo durante SaveAs, y después caída.
Configurar Book.StreamingWrite := True cambia SaveAs a un escritor de hojas en streaming que emite el XML de la hoja directamente al stream zip a medida que se genera, así que la cadena intermedia nunca existe y el pico se aplana hasta perderse en el ruido.
Sean precisos sobre lo que esto compra, porque sobredimensionarlo lleva a planes de capacidad equivocados. El indicador cambia solo la ruta de guardado. Construir el libro todavía asigna el modelo completo de celdas en memoria, de modo que la meseta de memoria durante la fase de llenado no cambia; lo que desaparece es el pico de serialización apilado encima de esa meseta al guardar. Para un trabajo que llena 400k filas, ese pico suele ser la diferencia entre entrar o no en el presupuesto de memoria. La propiedad usa False como valor predeterminado para preservar el comportamiento histórico, así que optar por ella es una línea explícita en el código.
Una exportación masiva con el indicador activado
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // pool index, 0-based
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // 1-based at the cell
end;
Book.StreamingWrite := True; // stream sheet XML straight into the zip
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C] crea celdas bajo demanda, lo que mantiene limpio el cuerpo del bucle, y conviene saberse de memoria los límites de la cuadrícula: 1,048,576 filas por 16,384 columnas (XlsxMaxRow y XlsxMaxCol). Un feed de datos que excede el límite de filas necesita dividirse en hojas desde su código; nada posterior lo hará por ustedes.
Llenar filas sin sobrecosto Variant por celda
Cada asignación Cells[R, C].Value paga una búsqueda de celda y una conversión Variant. Con diez mil filas nadie lo nota; con un millón de filas de veinte columnas, el costo por llamada se convierte en el costo dominante de la fase de llenado. Las interfaces por lotes entregan al escritor una fila completa por vez: WriteRows maneja un callback que suministra una fila por invocación:
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // data source drained: stop cleanly
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// fill rows 2..100001, columns A..C, pulling from the reader
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
El indicador Cancel del callback convierte un rango fijo de filas en "hasta N filas", que es la forma natural cuando el conteo viene de una consulta que todavía no terminaron de ejecutar; Skip deja una fila individual vacía sin detener la corrida. El callback también es el lugar natural para preocupaciones operativas que de otra manera se atornillan mal: incrementar un contador de progreso cada mil filas, revisar un token de cancelación del programador de trabajos o limitar la tasa de lectura desde una base de datos fuente viven en un solo punto en vez de cruzar todo el bucle de escritura de celdas. Los equivalentes en espejo ForEachRow y ForEachCell existen del lado de lectura por las mismas razones, y importan cuando un trabajo por lotes consume y produce archivos grandes.
Los pools de estilos premian izar definiciones
El modelo de estilos XLSX es un conjunto de pools compartidos: Fonts.Add, Fills.AddSolid y Borders.Add devuelven índices de pool basados en 0, y una celda referencia una fuente almacenando ese índice más uno en FontIndex, con cero como valor predeterminado del libro. El +1 se ve en el ejemplo masivo anterior, y olvidarlo aplica silenciosamente el estilo equivocado: un off-by-one en un pool de estilos no lanza excepción.
La disciplina del bucle: creen cada objeto de estilo antes del bucle de filas y reutilicen el índice dentro. Fonts.Add deduplica definiciones idénticas, así que llamarlo por fila es apenas CPU desperdiciada, pero Alignments.Add devuelve una entrada nueva en cada llamada; dentro de un bucle de 100k filas, eso infla styles.xml con cien mil registros duplicados, agranda el archivo y ralentiza cada apertura posterior en Excel. Definan una vez, indexen muchas.
Streams, directorios temporales y el bucle batch alrededor de todo
Todo lo anterior también funciona sin sistema de archivos. Ambas fachadas exponen sobrecargas TStream en toda su superficie de IO: Open, SaveAs, SaveAsCSV, SaveAsHTML, SaveAsODS, de modo que un worker por lotes puede renderizar directamente a un TMemoryStream destinado a almacenamiento blob o a una respuesta HTTP. Un borde filoso: SaveAs(Stream) escribe desde la posición actual del stream y no rebobina, así que reinicien Position := 0 antes de entregar el stream a lo que lo distribuya. Los trabajos en la fachada XLS tienen sus propios ajustes: SetTempDir apunta los archivos temporales del escritor BIFF a un volumen con el espacio y el presupuesto de IO para absorberlos, algo que importa en servidores donde la ruta temporal predeterminada vive en un disco de sistema pequeño; y UseSharedFormulas comprime cuerpos de fórmula repetidos en grupos compartidos, una reducción de tamaño significativa para la forma clásica de reporte donde la misma fórmula llena una columna completa.
El bucle batch en sí se mantiene aburrido a propósito:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // fresh instance: no state bleed
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // one bad input must not kill the batch
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
Una instancia fresca de libro por archivo cuesta microsegundos y elimina toda una categoría de bugs de contaminación entre archivos: estilos, nombres definidos y propiedades de documento del archivo 17 nunca pueden filtrarse al archivo 18. El saltar y continuar ante un Open fallido importa igual: una carga truncada en un lote de 600 archivos debe producir una línea de log, no un lote muerto. Noten también lo que no hace el tramo CSV: SaveAsCSV escribe las fórmulas como texto literal sin evaluarlas, así que un lote de conversión cuyos consumidores esperan valores calculados debe ejecutar Calculate en las celdas relevantes primero u operar sobre libros que ya traen resultados en caché.
Concurrencia: las instancias son baratas, compartir está prohibido
Los objetos de ninguna de las dos fachadas son thread-safe, y el diseño no les pide que lo sean: no hay estado global compartido entre instancias, así que el modelo de escalamiento es un libro por hilo worker, punto. Un pool de N workers, cada uno dueño de su propio TXLSXWorkbook, escala linealmente hasta que la memoria se vuelve el techo; y el cálculo del techo es concreto: el modelo de celdas concurrente más grande multiplicado por el conteo de workers, más el sobrecosto de guardado que StreamingWrite con suerte ya aplanó. Apliquen back-pressure en la cola de trabajos en lugar de dentro del escritor; un libro escrito a medias por un hilo sin recursos vale menos que un trabajo que esperó su turno.
Para el panorama más amplio de ajuste: fórmulas compartidas, omisión de gráficos en lectura y palancas específicas de XLS, vean la guía de rendimiento para libros grandes; los trabajos por lotes cuyas filas salen directamente de una base de datos están cubiertos en los patrones de exportación de base de datos para reportes Delphi.
HotXLS compila dentro de su servicio Delphi o C++Builder como Object Pascal nativo sin dependencias externas; las ediciones y licencias están en la página de producto de HotXLS Component.