40페이지짜리 PDF에서 3페이지를 복사하는 데 2분이 걸린다면 그것은 성능 튜닝 문제가 아닙니다. 그것은 잘못된 API 경로가 사용되고 있다는 신호입니다. 제가 처음 HotPDF 컴포넌트 페이지 복사 샘플에서 이 타이밍을 보았을 때, 제 직감은 먼저 문서 구조를 살펴보고 그 다음 코드를 살펴보는 것이었습니다. 그 순서가 중요했다는 것이 밝혀졌습니다
실제로 느렸던 점
문제의 PDF는 단일 평면 배열 대신 다중 중간 /Pages 노드가 있는 간단하지 않은 페이지 트리 구조를 가진 40페이지짜리 참조 문서였습니다. 원본 샘플 코드는 LoadFromFile을 호출한 다음 BeginDoc을 사용하여 새 문서를 빌드하고, 선택한 페이지 번호에 대해 루프를 돌면서 각 반복마다 디스크에서 소스 문서를 다시 로드하여 페이지를 가져오고 있었습니다. 그것은 원하는 페이지 수를 곱한 전체 파싱 비용입니다. 12MB 파일은 3페이지 추출을 위해 디스크에 6번이나 액세스했습니다. 반복 작업 동안 파일이 열려 있어야 하는지 아무도 확인하지 않았기 때문입니다
두 번째 원인은 코드에서 보이지 않았습니다: HotPDF의 LoadFromFile은 로드 시 전체 상호 참조 테이블을 해결하고 모든 객체 스트림을 압축 해제합니다. 이것은 수정하려는 문서에 대한 올바른 동작이지만, 페이지 수와 페이지 하위 집합만 원하는 경우 필요한 것보다 많은 작업입니다. 구조에 대한 읽기 전용 액세스의 경우 DAOpenFileReadOnly는 전체 객체 트리를 역직렬화하는 것을 방지하며, 이는 대용량 이미지 리소스가 포함된 압축 파일에서 중요합니다
이 둘 다 라이브러리 버그가 아닙니다. 두 가지 모두 호출자가 한 가지 작업을 위해 설계된 API를 선택하여 다른 작업에 사용한 것입니다
페이지 추출에 InsertPagesFromDocument 사용하기
하나의 HotPDF 문서에서 다른 문서로 일정 범위의 페이지를 복사하는 올바른 경로는 소스에 대해 LoadFromFile을 호출한 후 InsertPagesFromDocument를 호출하는 것입니다. 소스를 한 번 로드하고, 대상을 한 번 로드하거나 생성하고, 페이지를 이동하고, 저장합니다. 모든 페이지 삽입 동안 소스는 메모리에 유지됩니다:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// 소스를 한 번 로드: 전체 구문 분석은 여기서만 발생합니다
Source.LoadFromFile(SourceFile);
// 최소한의 대상 문서 생성
Dest.FileName := DestFile;
Dest.BeginDoc;
// 요청된 범위를 복사; '1-3'은 대상의 위치 1에서 시작하여
// 1부터 3까지의 페이지를 삽입합니다
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
PageRange 매개변수는 명령줄 샘플과 동일한 형식을 허용합니다: '1-3' 또는 '1,5,7-9'와 같은 페이지 번호 또는 범위의 쉼표로 구분된 목록입니다. 페이지는 1부터 시작합니다. InsertPagesFromDocument는 복사된 페이지에서 참조되지 않는 한 메타데이터, 책갈피 또는 첨부 파일을 건드리지 않고 콘텐츠 스트림, 리소스 딕셔너리 및 페이지 기하 구조를 복사합니다. 40페이지 문서에서 3페이지를 추출하는 데 이는 작은 워킹 셋(working set)입니다
이전에 2분이 걸렸던 동일한 12MB 파일에서의 타이밍: 이 패턴을 사용하면 1.5초 미만입니다. 이 시간의 대부분은 단일 LoadFromFile 호출입니다. 처음 객체 테이블이 해결되고 나면 문서 구조는 관련이 없습니다
LoadFromFile이 과할 때: 직접 파일 API (Direct File API)
페이지 수를 계산하거나, 문서 정보를 검사하거나, 내용을 건드리지 않고 파일을 복사하기만 하면 되는 경우 직접 파일 API는 전체 파싱을 완전히 우회합니다. DAOpenFileReadOnly는 객체 스트림을 압축 해제하지 않고 상호 참조 테이블을 매핑하므로 페이지 계산은 O(파일 크기)가 아닌 O(xref 크기)가 됩니다:
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile은 재직렬화 없는 바이트 보존 복사입니다
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
주의 사항: DAOpenFileReadOnly는 암호 매개변수를 허용하지만 암호화된 입력에 대해서는 전체 파싱으로 돌아갑니다. 암호 해독에는 암호화 딕셔너리를 해결하기 위한 객체 트리가 필요하기 때문입니다. 소스 파일이 암호화되어 있다면, 먼저 DecryptFile로 해독하여 암호화되지 않은 복사본을 얻은 다음, 직접 파일 API로 그것을 엽니다. 파일 수준의 DecryptFile 함수는 표준 암호화를 위한 직접적인 AES-256 재작성 경로를 취하며 전체 인메모리 객체 모델을 빌드하지 않기 때문에 대용량 파일의 경우 LoadFromFile 후 SaveLoadedDocument를 호출하는 것보다 빠릅니다
대규모 일괄 처리 중의 메모리
루프에서 수십 개의 파일을 처리하는 일괄 작업에는 루프 내부에서 THotPDF를 생성하고, LoadFromFile을 호출하고, 작업을 수행하고, Free를 호출하는 정확해 보이는 패턴이 있지만 메모리가 누적됩니다. 이는 구조적으로 괜찮습니다. 문제는 내부 작업이 스크래치(임시) 객체를 할당하고, 예외를 포착하고, 오류 경로에서 이러한 스크래치 객체를 활성 상태로 남겨둘 때입니다. 델파이의 메모리 관리자는 압축을 하지 않으므로 일괄 실행에서 수백 개의 오류 경로 누출이 발생하면 메모리를 다른 모든 것의 할당을 느리게 할 만큼 높게 밀어 올릴 수 있습니다
해결책은 특이한 것이 아닙니다. 모든 THotPDF와 PDF 작업에 참여하는 모든 중간 TStream 또는 TBitmap은 Free가 마지막 문장인 try/finally 블록에 속해야 합니다. 초기화가 중간에 실패할 때 finally 분기가 if Assigned(x) then x.Free를 안전하게 사용할 수 있도록 try 전에 로컬 포인터를 nil로 설정하십시오. 이것은 표준 델파이 소유권 규칙이며 이 문제 부류에 대한 전체 내용입니다
일괄(batch) 컨텍스트에서 확인해야 할 한 가지 더: AddImage는 이미지를 THotPDF 인스턴스의 수명 기간 동안 유지되는 내부 목록에 등록합니다. LoadFromFile을 반복해서 호출하여 여러 문서에 걸쳐 단일 인스턴스를 재사용하는 경우, 이전 문서의 이미지 등록이 목록에 그대로 남습니다. 문서당 새로운 인스턴스를 생성하거나 문서 사이에 이미지 목록 지우기 경로를 호출하십시오
아무 것도 변경하기 전에 측정하기
이러한 패턴에 손을 뻗기 전에 먼저 측정하십시오. System.Diagnostics의 델파이 TStopwatch는 QueryPerformanceCounter를 포장하며 파일 I/O의 실시간(wall-clock) 프로파일링에 충분히 정확합니다. LoadFromFile만 포장하여 시간이 얼마나 걸리는지 확인하십시오. 총 시간의 90%라면 직접 파일 API가 해결책이거나 동일한 파일을 구문 분석하는 횟수를 줄이는 것입니다. 20% 미만인 경우 병목 현상은 다른 곳에 있으며 잘못된 대상을 쫓고 있는 것입니다
이 게시물을 시작하게 한 2분의 추출은 결국 전체가 반복 로드 패턴 때문이었습니다. 문서 구조는 아무 기여도 하지 않았으며, 평면 페이지 트리도 동일한 방식으로 실행되었을 것입니다. 단일 LoadFromFile과 그에 이은 한 번의 InsertPagesFromDocument 호출로 변경한 후 다른 것을 건드리지 않고 동일한 하드웨어에서 1.3초로 줄었습니다
여기에 표시된 페이지 조작 API는 델파이 및 C++Builder용 HotPDF 컴포넌트의 일부입니다