バルクローバルOCR中のハイピークメモリー
一度に多くのPDFセグメントに対してOCRを実行することはメモリー消費を倍増させます。 各タスクはIronTesseractエンジンが言語モデルファイルを毎回リロードします。フルプロセッサーの同時実行性では、これによりメモリピークが数GB範囲に達し、メモリに制限のある環境でのスパイクが失敗します。
OCRは生来メモリを多く使用します。 すべてのIronTesseractエンジンは言語モデルファイルをメモリーにロードします。 各セグメントごとに新しいエンジンを作成すると、それらのモデルが繰り返しリロードされ、CPUコアごとに1つのOCRタスクを実行すること(Environment.ProcessorCount)により、多くのビットマップヘビージョブが並行して実行されます。アクティブなタスクの制限がまったくないと、ピークメモリは完全に同時実行性に比例して拡大します。
修正方法は、飛行中のジョブの数を制限し: 同時実行性の制限を設定し、プールからエンジンを再利用し、セマフォで作業をゲートします。
解決策
1. OCRの同時実行性を制限します
同時に行うOCRタスクの数を小さな天井にクランプします。 少ない同時タスクは、一度にメモリーにあるフルページビットマップの数が減り、それがピークを直接下げます。 機械の能力に合わせて天井を調整します。
// 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. エンジンをプールします
起動時に同時スロットごとに1つだけ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.
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())
)
一度プールを構築することで、セグメントごとに支払うのではなく、全体の実行にわたって言語モデルのロードコストを分割します。
3. セマフォで作業をゲートします
同時実行の制限にusingでラップします。 各タスクは開始前に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();
}
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
finallyにエンジンを戻すことで、事前ロードされたエンジンが次の待機中セグメントに直接渡されます。
4. 各セグメントごとにOcrInputを破棄します
各usingでラップして、そのセグメントが読み取られるとすぐにそのレンダードページビットマップが解放され、次のタスクがスロットを獲得する前に解放されます。
using の OcrInput です; それなしでは、解放されたスロットもページビットマップを保持します。

