30만 행짜리 내보내기가 메모리 예산을 뚫고 나갈 때 대개 행 수가 탓을 뒤집어씁니다. 행 수는 대개 무죄입니다. 큰 통합 문서에서 비싼 부분은 부수 효과로 생겨난 것들입니다. 서식을 루프 안에서 넣는 바람에 셀 하나당 항목 하나씩 불어난 스타일 풀, 저장 시점에 하나의 거대한 문자열로 조립되는 워크시트 XML, 하나씩 저장된 똑같은 수식 본문 백만 개 같은 것들입니다. XLS와 XLSX 파일을 위한 losLab의 네이티브 델파이 라이브러리 HotXLS는 이 비용 하나하나에 딱 맞는 지렛대를 줍니다. 그중 어느 것도 기본으로 켜져 있지 않은데, 저마다 절충을 바꾸기 때문입니다. 그러니 어느 지렛대가 어느 증상에 맞는지 아는 것이 실제 성능 기술입니다
대용량 통합 문서는 어디에 메모리를 쓰는가
따져 볼 메모리 국면은 뚜렷이 둘입니다. 생성 중에는 여러분이 건드리는 셀마다 메모리 안 셀 모델이 자랍니다. 값, 서식, 수식이 모두 객체나 풀 항목이 됩니다. 저장 중에는 기본 XLSX 경로가 여기에 더해 각 워크시트의 XML을 넓은 문자열로 렌더링한 뒤 zip 컨테이너로 압축하므로, 최대 사용량은 모델 더하기 가장 큰 시트의 직렬화된 형태입니다. 빌드 루프는 살아남았는데 SaveAs 안에서 죽는 작업은 첫 번째가 아니라 두 번째 국면에 부딪히는 것이고, 한쪽의 해법은 다른 쪽에 아무 효과가 없습니다
파일 크기도 비슷한 규칙을 따릅니다. 셀은 스타일, 공유 문자열, 수식, 이미지, 메모와 나란히 놓인 기여자 하나일 뿐입니다. ForEachCell과 시트별 컬렉션 개수로 감사 한 번 돌려 보면, 엉뚱한 것을 최적화하기 전에 문제 파일에서 실제로 어느 자원이 지배적인지 알 수 있습니다. 측정에는 미묘한 점이 하나 있습니다. XLSX 쪽의 Sheet.Cells.Count는 사용된 범위의 넓이가 아니라 희소 저장소에 실제로 만들어진 셀의 개수를 보고합니다. 데이터가 1000 곱하기 50 직사각형을 차지하되 절반이 빈 시트는 5만이 아니라 대략 2만 5천으로 셉니다. 고객의 "거대한" 파일을 여러분의 고정 파일과 비교할 때 이 구분이 중요합니다. 희소한 재무 배치에서는 사용된 범위 넓이와 실제 셀 개체 수가 자릿수만큼 다를 수 있기 때문입니다
StreamingWrite는 저장 경로를 고치지 빌드 경로를 고치지 않습니다
TXLSXWorkbook.StreamingWrite := True로 설정하면 SaveAs가 워크시트 XML을 zip 스트림에 곧장 쓰는 스트리밍 직렬화기로 바뀌어, 시트별 문자열 중간 산물이 사라집니다. 동작 호환성 때문에 기본값은 False이며, 켜는 것은 한 줄짜리 변경입니다:
Book := TXLSXWorkbook.Create;
try
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;
end;
Book.StreamingWrite := True; // 시트 XML이 zip 컨테이너로 흘러 들어갑니다
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
이것이 무엇을 사 주는지는 정확히 짚어야 합니다. 루프가 지은 셀 모델은 예전과 똑같은 만큼의 메모리를 차지합니다. StreamingWrite는 저장 시점의 봉우리를 눌러 주는데, 그것이 끝까지 마치는 배치 작업과 95% 지점에서 실패하는 작업의 차이입니다. 빌드 루프 자체가 메모리를 다 써 버린다면, 필요한 지렛대는 다음 두 가지입니다
스타일 풀: 한 번 추가하고 인덱스를 재사용하십시오
HotXLS의 XLSX 서식은 풀 기반입니다. Book.Fonts.Add(...), Fills.AddSolid(...), Borders.Add(...)는 셀이 참조하는 0 기반 풀 인덱스를 돌려줍니다. 루프 안에서 같은 매개변수로 Fonts.Add를 부르면 중복이 제거되므로, 공간이 아니라 시간을 낭비합니다. Alignments.Add는 다르게 행동합니다. 호출마다 새 객체를 돌려주므로, 셀마다 맞춤을 만들면 풀이 행 수에 비례해 자랍니다. 습관 하나가 두 경우를 모두 덮습니다. 모든 풀 인덱스를 루프 밖에서 한 번 해결하고, 안에서는 인덱스만 대입하십시오
// 풀 조회를 뜨거운 루프 밖으로 끌어올립니다
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False); // 0 기반 풀 인덱스
for C := 1 to 24 do
Sheet.Cells[1, C].FontIndex := HeaderFont + 1; // 셀은 1 기반 저장; 0 = 기본값
+ 1은 오타가 아니며, 이것을 잊는 것이 여기서 증상을 만들어 내는 고전적인 버그입니다. 풀은 0 기반 인덱스를 내주는데 셀 쪽 속성은 0을 "기본값"으로 다루므로, 대입할 때 모든 풀 인덱스를 하나 밀어야 합니다. 빠뜨려서 틀리면 머리글이 조용히 통합 문서의 기본 글꼴로 그려지는데, 브랜딩 검토 때까지 아무도 알아채지 못하는 결함입니다
셀마다 오가는 Variant를 행 콜백으로 바꾸십시오
Sheet.Cells[R, C].Value := X는 매번 셀 조회 또는 생성에 Variant 대입이 딸립니다. 셀이 수십만 개가 되면 그 접근당 부담이 프로파일에 잡힐 만큼 커집니다. HotXLS는 양쪽 파사드에 대량 콜백 API를 제공하는데(읽기에는 ForEachCell과 ForEachRow, 쓰기에는 WriteCells와 WriteRows), 반복을 엔진 안으로 옮기고 여러분의 코드에는 한 번에 행 전체를 건네줍니다:
procedure TLedgerExport.FillRow(Sender: TObject;
SheetIndex, Row, FirstCol, LastCol: Integer;
var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
if Row > FCount then
begin
Cancel := True; // 쓰기 전체를 멈춥니다
Exit;
end;
Values := VarArrayOf([FRows[Row - 1].Account,
FRows[Row - 1].PostedOn,
FRows[Row - 1].Amount]);
end;
// 수십만 번의 속성 접근 대신 엔진 호출 한 번
Sheet.WriteRows(1, 1, FCount, 3, FillRow);
콜백의 Skip 플래그는 중단 없이 행 하나를 손대지 않고 넘어가게 하고, Cancel은 작업을 일찍 끝냅니다. 길이를 진행하면서야 알게 되는 리더가 원본일 때 쓸모가 있습니다. 빌드에는 WriteRows를, 저장에는 StreamingWrite를 짝지으면 생성 경로에 셀 단위 병목이 남지 않습니다
XLS 파사드의 읽기 쪽 지렛대
대용량 예전 .xls 파일에는 그 나름의 연장통이 있습니다. Open 전에 _DisableGraphics := True로 두면 그리기 계층 구문 분석을 통째로 건너뛰므로, 여러 해 쌓인 도형과 포함된 그림을 지고 있는 통합 문서의 적재가 빨라집니다. 제약은 단호합니다. 그러면 그리기 계층이 모델에 없으므로, 그런 통합 문서를 저장하면 그림이 빠진 파일이 써집니다. 이 플래그는 읽기 전용 분석 작업에만 쓰십시오. SetTempDir은 BIFF 기록기의 임시 파일 위치를 바꾸는데, 기본 임시 위치에 할당량이 걸려 있거나 느린 저장 장치에 놓인 서버에서 중요합니다. UseSharedFormulas는 되풀이되는 수식 본문을 공유 수식 레코드로 묶어, 수식 열 하나가 6만 행 아래로 되풀이되는 파일을 줄여 줍니다
XLS 데이터를 도는 읽기 루프에는 짚어 둘 만한 인덱싱 함정이 있습니다. 방어적으로 다루면 일이 두 배가 되고 놓치면 결과가 망가지기 때문입니다. UsedRange는 FirstRow, LastRow, FirstCol, LastCol 경계를 0 기반으로 보고하는데, Cells.Item[Row, Col]은 1 기반입니다. 사용된 범위를 훑는 스캔은 셀 접근에서 좌표마다 하나를 더해야 하며, Cells.Item[Row + 1, Col + 1]처럼 써야 합니다. 그러지 않으면 대각선으로 한 칸 밀린 격자를 읽어, 마지막 행과 열을 조용히 떨어뜨리고 유령 같은 첫 행과 열을 포함하게 됩니다. ForEachCell 콜백은 이 어긋남을 통째로 비켜 가는데, 시트 전체 스캔에 그쪽을 선호할 이유가 하나 더 늘어난 셈입니다
적재하기 전에 파일을 살펴보십시오
가장 값싼 대용량 통합 문서 작업은 하지 않는 작업입니다. 양쪽 파사드의 GetSheetNames는 셀 데이터를 적재하지 않고 파일의 워크시트를 나열합니다. XLSX 구현은 zip 안의 통합 문서 매니페스트만 읽고 통합 문서 인스턴스를 일부러 비워 두며, XLS 파사드는 첫 하위 스트림 경계에서 스캔을 멈춥니다. 그래서 "이 가져오기 작업은 어느 시트를 겨눠야 하는가"에 대한 알맞은 사전 점검이 되고, CanReadEncrypted는 실패가 예정된 Open 시도 전에 "이것이 암호화된 컨테이너인가"에 답합니다
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
raise Exception.Create('cannot enumerate sheets'); // 실패하면 목록이 비워집니다
// 대상 시트를 고른 다음, 전체 Open이 값어치가 있는지 정합니다
finally
Book.Free;
Names.Free;
end;
반환 코드 관례를 눈여겨보십시오. 이 탐색 함수들은 0 이하의 값으로 실패를 알리고 출력 목록을 비우므로, 성공을 뜻하는 특정 값 하나와 견주지 말고 <= 0을 검사하십시오
작업 규모에 접근법을 맞추기
대용량 파일 여럿을 잇달아 생성하는 무인 파이프라인에는 습관 두 가지가 그림을 마저 채웁니다. 통합 문서 객체는 공유해 쓰기에는 스레드 안전하지 않지만, 작업자 스레드마다 독립된 통합 문서를 하나씩 두는 것은 아무도 막지 않으며, 그러면 배치 변환이 깔끔하게 병렬화됩니다. 그리고 출력이 디스크가 아니라 HTTP로 갈 때는 TStream 저장 오버로드가 StreamingWrite와 어우러져, 큰 응답이 임시 파일로 실체화되는 일이 없습니다. 운영상의 각주가 하나 붙습니다. 스트림 저장은 되감지 않고 현재 위치부터 쓰므로, 스트림을 응답 프레임워크에 넘기기 전에 Position := 0으로 설정하십시오. 스트리밍 쓰기와 배치 작업 글이 그 서버 쪽 패턴을 펼쳐 보이고, 데이터베이스 내보내기 글은 이 지렛대들이 데이터셋 주도 보고서 어디에 끼워지는지 보여 줍니다
끝으로, 보고서 계열마다 최악의 경우 고정 파일을 하나씩 두고 CI에서 시간을 재십시오. 문서 생성의 성능 퇴행은 좀처럼 자기를 알리지 않습니다. 루프 안에 들어간 스타일 하나나 전체 Open으로 바뀐 탐색 호출은 기능적으로는 아무것도 바꾸지 않고, 야간 배치가 그저 사십 분 더 걸릴 뿐입니다. 대표적인 오십만 셀 고정 파일에 시간 측정 테스트를 걸어 두면 그 표류가 운영 사고 대신 빨간 빌드로 나타납니다
평가판 빌드, 대량 생성 예제가 든 데모 프로젝트, 전체 API 참조 문서는 HotXLS Delphi Component 페이지에서 받을 수 있습니다