Hoher Speicherverbrauch bei Massen-OCR
Das Ausführen von OCR über viele PDF-Segmente zugleich vervielfacht den Speicherverbrauch. Jede Aufgabe rendert vollseitige Bitmaps durch einen OcrInput, und eine neue IronTesseract Engine pro Segment lädt die Sprachmoduldateien jedes Mal neu. Bei voller Prozessor-Konkurrenz wird der maximale Speicherbedarf in den Multi-GB-Bereich gedrückt, mit Spitzen, die in speicherbeschränkten Umgebungen scheitern.
OCR ist von Natur aus speicherintensiv. Jeder OcrInput rendert vollseitige Bitmaps, und jede IronTesseract Engine lädt Sprachmoduldateien in den Speicher. Das Erstellen einer neuen Engine pro Segment lädt diese Modelle wiederholt neu, und das Ausführen einer OCR-Aufgabe pro CPU-Kern (Environment.ProcessorCount) erlaubt es, viele bitmap-lastige Jobs nebeneinander laufen zu lassen. Da es keine Begrenzung gibt, wie viele Aufgaben aktiv sind, skaliert der maximale Speicherbedarf direkt mit der Konkurrenz.
Die Lösung besteht darin, die Anzahl der gerade in Arbeit befindlichen Jobs zu begrenzen: Begrenzen Sie die Konkurrenz, wiederverwendbare Motoren aus einem Pool verwenden und Arbeiten mit einem Semaphor steuern.
Lösung
1. OCR-Konkurrenz begrenzen
Begrenzen Sie die Anzahl der gleichzeitigen OCR-Aufgaben auf eine kleine Obergrenze. Weniger gleichzeitige Aufgaben bedeuten weniger Vollseiten-Bitmaps im Speicher zugleich, was die Spitzenwerte direkt senkt. Passen Sie die Obergrenze an die Leistungsfähigkeit der Maschine an.
// Clamp concurrency to avoid memory saturation and CPU over-subscription.
int concurrency = Math.Clamp(Environment.ProcessorCount / 2, 1, 4);
// Clamp concurrency to avoid memory saturation and CPU over-subscription.
int concurrency = Math.Clamp(Environment.ProcessorCount / 2, 1, 4);
Imports System
' Clamp concurrency to avoid memory saturation and CPU over-subscription.
Dim concurrency As Integer = Math.Clamp(Environment.ProcessorCount \ 2, 1, 4)
2. Die Motoren Poolen
Erstellen Sie genau eine IronTesseract Engine pro konkurrierendem Slot beim Start und verwenden Sie sie über jedes Segment hinweg erneut, anstatt jedes Mal eine neue Engine zu konstruieren und das Sprachmodell neu zu laden.
// Pre-create one engine per concurrent slot and reuse them across segments.
var enginePool = new ConcurrentBag<IronTesseract>(
Enumerable.Range(0, concurrency).Select(_ => new IronTesseract())
);
// Pre-create one engine per concurrent slot and reuse them across segments.
var enginePool = new ConcurrentBag<IronTesseract>(
Enumerable.Range(0, concurrency).Select(_ => new IronTesseract())
);
' Pre-create one engine per concurrent slot and reuse them across segments.
Dim enginePool As New ConcurrentBag(Of IronTesseract)(
Enumerable.Range(0, concurrency).Select(Function(_) New IronTesseract())
)
Das Bauen des Pools einmal amortisiert die Kosten für das Laden des Sprachmoduls über den gesamten Lauf statt pro Segment zu bezahlen.
3. Arbeiten mit einem Semaphor steuern
Initialisieren Sie ein SemaphoreSlim auf das Konkurrenzlimit und umwickeln Sie es mit using. Jede Aufgabe ruft WaitAsync() auf, bevor sie startet, und Release() in einem finally auf, sodass nur die erlaubte Anzahl von Segmenten gleichzeitig aktiv ist.
using var semaphore = new SemaphoreSlim(concurrency);
await semaphore.WaitAsync();
try
{
// Rent a pre-loaded engine from the pool.
if (!enginePool.TryTake(out var ocr))
ocr = new IronTesseract(); // Defensive fallback; should never be reached.
try
{
using var input = new OcrInput();
input.LoadPdf(segmentStream); // page-range segment produced upstream
var ocrResult = await ocr.ReadAsync(input);
ocrResult.SaveAsSearchablePdf(outputPath);
}
finally
{
enginePool.Add(ocr); // Return engine to pool for the next waiting segment.
}
}
finally
{
semaphore.Release();
}
using var semaphore = new SemaphoreSlim(concurrency);
await semaphore.WaitAsync();
try
{
// Rent a pre-loaded engine from the pool.
if (!enginePool.TryTake(out var ocr))
ocr = new IronTesseract(); // Defensive fallback; should never be reached.
try
{
using var input = new OcrInput();
input.LoadPdf(segmentStream); // page-range segment produced upstream
var ocrResult = await ocr.ReadAsync(input);
ocrResult.SaveAsSearchablePdf(outputPath);
}
finally
{
enginePool.Add(ocr); // Return engine to pool for the next waiting segment.
}
}
finally
{
semaphore.Release();
}
Imports System.Threading
Imports IronOcr
Dim semaphore As New SemaphoreSlim(concurrency)
Await semaphore.WaitAsync()
Try
' Rent a pre-loaded engine from the pool.
Dim ocr As IronTesseract = Nothing
If Not enginePool.TryTake(ocr) Then
ocr = New IronTesseract() ' Defensive fallback; should never be reached.
End If
Try
Using input As New OcrInput()
input.LoadPdf(segmentStream) ' page-range segment produced upstream
Dim ocrResult = Await ocr.ReadAsync(input)
ocrResult.SaveAsSearchablePdf(outputPath)
End Using
Finally
enginePool.Add(ocr) ' Return engine to pool for the next waiting segment.
End Try
Finally
semaphore.Release()
End Try
Der WaitAsync() Aufruf blockiert, bis ein Slot frei wird, und das Zurückgeben der Engine im inneren finally übergibt eine vorab geladene Engine direkt an das nächste wartende Segment.
4. Dispose OcrInput per segment
Wickeln Sie jede OcrInput in using ein, sodass die gerenderten Seiten-Bitmaps freigegeben werden, sobald das Segment gelesen wird, bevor die nächste Aufgabe den Slot beansprucht.
using auf OcrInput verhindert, dass sich Bitmap-Speicher über Segmente hinweg ansammelt; ohne es würden freigegebene Slots immer noch ihre Seiten-Bitmaps halten.

