質問
コード内に、レコードを行ごとに解析し、検証して別のファイルに書き込む巨大な関数がある状況があります。
ファイルにエラーがある場合は、レコードを拒否する別の関数を呼び出し、拒否の理由を書き込みます。
プログラム内のメモリ リークが原因で、SIGSEGV でクラッシュします。クラッシュした場所からファイルを「再起動」するための 1 つの解決策は、最後に処理されたレコードを単純なファイルに書き込むことでした。
これを実現するには、処理ループ内の現在のレコード番号をファイルに書き込む必要があります。ループ内のファイルでデータが確実に上書きされるようにするにはどうすればよいですか?
fseek を使用してループ内で最初の位置決めや巻き戻しを行うと、パフォーマンスが低下しますか?
レコードの数は、場合によっては多数になることがあります (最大 500K)。
ありがとう。
編集:メモリリークはすでに修正されています。再起動ソリューションは、Skip n Recordsソリューションとともに再起動メカニズムを提供するための追加の安全対策と手段として提案されました。先ほど言及せずに申し訳ありません。
解決
この種の問題に直面した場合は、次の 2 つの方法のいずれかを採用できます。
- あなたが提案した方法:読み取ったレコードごとに、 レコード番号を書き出す (またはによって返される位置
ftell
入力ファイル上) を別のファイルに ブックマーク ファイル。重複レコードが発生しないように、中断したところから正確に再開するには、次のことを行う必要があります。fflush
毎回の書き込み後(両方に)bookmark
これと、一般にバッファなし書き込み操作により、典型的な (障害のない) シナリオの速度が大幅に低下します。完全を期すために、ブックマーク ファイルに書き込むには 3 つの方法があることに注意してください。fopen(..., 'w') / fwrite / fclose
- 非常に遅いrewind / truncate / fwrite / fflush
- わずかに速いrewind / fwrite / fflush
- 多少速い;スキップしてもいいですtruncate
レコード番号以降 (またはftell
位置) は、常に前のレコード番号 (またはftell
位置))、起動時にファイルを一度切り詰めると、完全に上書きされます。 (これはあなたの元の質問に答えます)
- すべてがうまくいくと思い込む ほとんどの場合;失敗後に再開する場合は、単純に すでに出力されているレコードの数をカウントします (通常の出力と拒否)、入力ファイルから同数のレコードをスキップします。
- これにより、障害後の再開シナリオの場合でもパフォーマンスを大幅に損なうことなく、一般的な (障害のない) シナリオが非常に高速に維持されます。
- あなたはそれをする必要が無い
fflush
ファイル、または少なくともそれほど頻繁ではありません。まだ必要がありますfflush
リジェクトファイルへの書き込みに切り替える前のメイン出力ファイル、およびfflush
メイン出力ファイルへの書き込みに戻る前に拒否ファイルを削除します (500k レコード入力の場合、おそらく数百回または千回)。出力/拒否ファイルから終了していない最後の行を削除するだけで、その行までのすべてが一貫性を保ちます。 。
方法 2 を強くお勧めします. 。方法 #1 (3 つの可能性のいずれか) に伴う書き込みは、方法 #2 で必要な追加の (バッファされた) 読み取りと比較して、非常にコストがかかります (fflush
数ミリ秒かかる場合があります。それに 500k を掛けると、分が得られます。一方、500k レコードのファイルの行数を数えるのにかかる時間はわずか数秒で、さらに、ファイルシステムのキャッシュが機能しています。 反対ではなく、賛成 それについてはあなた。)
編集方法 2 を実装するために必要な正確な手順を明確にしたかっただけです。
出力ファイルに書き込むときとファイルを拒否するときは、あるファイルへの書き込みから別のファイルへの書き込みに切り替えるときにのみフラッシュする必要があります。これらのファイル切り替え時のフラッシュを実行する必要性を示す例として、次のシナリオを考えてみましょう。
- メイン出力ファイルに 1000 レコードを書き込むとします。
- 最初にメイン出力ファイルを手動でフラッシュせずに、拒否ファイルに 1 行を書き込む必要があります。
- 最初に拒否ファイルを手動でフラッシュせずに、メイン出力ファイルにさらに 200 行を書き込みます。
- ランタイム 自動的に メイン出力ファイルのバッファに大量のデータが蓄積されているため、メイン出力ファイルをフラッシュします。1200レコード
- しかし ファイルバッファには 1 つのレコードしか含まれておらず、自動的にフラッシュするには十分な量ではないため、ランタイムはまだ拒否ファイルを自動的にディスクにフラッシュしていません。
- この時点でプログラムがクラッシュします
- 再開して、メイン出力ファイル内の 1200 レコードをカウントします (ランタイムがそれらをフラッシュします)。しかし、拒否ファイル内のレコードは 0 (!) です (フラッシュされません)。
- メイン出力ファイルに正常に処理されたレコードが 1200 件だけであると仮定して、レコード #1201 から入力ファイルの処理を再開します。拒否されたレコードは失われ、1200 番目の有効なレコードが繰り返されます。
- あなたはこれを望んでいません!
- ここで、出力/拒否ファイルを切り替えた後に手動でフラッシュすることを検討してください。
- メイン出力ファイルに 1000 レコードを書き込むとします。
- 拒否ファイルに属する無効なレコードが 1 つ見つかりました。最後のレコードは有効でした。これは、拒否ファイルへの書き込みに切り替えることを意味します。拒否ファイルに書き込む前にメイン出力ファイルをフラッシュします。
- ここで、拒否ファイルに 1 行を書き込みます。
- メイン出力ファイルに属する有効なレコードが 1 つ見つかります。最後のレコードは無効でした。これは、メイン出力ファイルへの書き込みに切り替えることを意味します。メイン出力ファイルに書き込む前に拒否ファイルをフラッシュします。
- 最初に拒否ファイルを手動でフラッシュせずに、メイン出力ファイルにさらに 200 行を書き込みます。
- メイン出力ファイルに対する最後の手動フラッシュ以降にバッファリングされた 200 レコードでは自動フラッシュをトリガーするには十分ではないため、ランタイムが自動的に何もフラッシュしなかったと仮定します。
- この時点でプログラムがクラッシュします
- 再開して、メイン出力ファイル内の 1000 件の有効なレコード (拒否ファイルに切り替える前に手動でフラッシュしたもの) と、拒否ファイル内の 1 つの有効なレコード (メイン出力ファイルに切り替える前に手動でフラッシュしたもの) をカウントします。
- これにより、無効なレコードの直後の最初の有効なレコードであるレコード #1001 で入力ファイルの処理が正しく再開されます。
- 次の 200 個の有効なレコードはフラッシュされなかったため再処理しますが、欠落レコードも重複レコードも得られません。
ランタイムの自動フラッシュの間隔に満足できない場合は、100 レコードごとまたは 1000 レコードごとに手動フラッシュを実行することもできます。これは、レコードの処理がフラッシュよりも高価かどうかによって異なります (処理が高価な場合は、各レコード後などに頻繁にフラッシュします。それ以外の場合は、出力/拒否を切り替えるときにのみフラッシュします)。
失敗からの再開
- 出力ファイルと拒否ファイルを開く 読み書きの両方に, そして、各レコードを読み取ってカウントすることから始めます(たとえば、
records_resume_counter
) ファイルの終わりに達するまで - 後で洗い流していない限り それぞれ 出力しているレコード, 、出力ファイルと拒否ファイルの両方の最後のレコードに対して少し特別な処理を実行する必要もあります。
- 中断された出力/拒否ファイルからレコードを読み取る前に、当該出力/拒否ファイル内での現在の位置を覚えておいてください (使用
ftell
)、そう呼びましょうlast_valid_record_ends_here
- 記録を読んでください。レコードが部分レコードではないことを検証します (つまり、ランタイムはファイルを次の時点までフラッシュしていません。 真ん中 記録の)。
- 1 行に 1 つのレコードがある場合、レコードの最後の文字がキャリッジ リターンまたはライン フィード (
\n
または「r」)- レコードが完了している場合は、レコード カウンタをインクリメントし、次のレコード (またはファイルの終わりのいずれか最初に来る方) に進みます。
- 記録が部分的な場合、
fseek
戻るlast_valid_record_ends_here
, 、この出力/拒否ファイルからの読み取りを停止します。カウンタをインクリメントしないでください。次の出力に進むか、すべてを完了していない限りファイルを拒否します
- 中断された出力/拒否ファイルからレコードを読み取る前に、当該出力/拒否ファイル内での現在の位置を覚えておいてください (使用
- 入力ファイルを開いて読み取り、スキップします
records_resume_counter
そこからの記録- 処理と出力/拒否ファイルへの出力を続行します。これは、すでに処理されたレコードの読み取り/カウントを中断した出力/拒否ファイルに自動的に追加されます。
- 部分的なレコードのフラッシュに対して特別な処理を実行する必要がある場合、次に出力するレコードは、前回の実行での部分的な情報を上書きします。
last_valid_record_ends_here
) - 重複したレコード、ゴミになったレコード、または欠落したレコードはなくなります。
- 出力ファイルと拒否ファイルを開く 読み書きの両方に, そして、各レコードを読み取ってカウントすることから始めます(たとえば、
他のヒント
コードを変更して、最後に処理されたレコードをファイルに書き込むことができる場合、メモリリークを修正するために変更できないのはなぜですか?
症状を治療するよりも、問題の根本原因を修正するより良い解決策のように思えます。
fseek()
および fwrite()
はパフォーマンスを低下させますが、オープン/書き込み/クローズ型の操作ほどではありません。
2番目のファイルに ftell()
の値を保存することを前提としています(中断したところから再開できます)。 CランタイムライブラリからOSバッファーにデータが確実に書き込まれるように、ファイルも常に fflush()
する必要があります。そうしないと、SEGVは値が最新でないことを保証します。
レコード全体を書き出すよりも、各ファイルの先頭でftell()を呼び出して、ファイルポインターの位置を書き込む方が簡単です。プログラムを再起動する必要がある場合は、fseek()をファイル内の最後に書き込まれた位置に移動して続行します。
もちろん、メモリリークを修正することをお勧めします;)
すべてのレコードに対して最後に処理された位置を書き込むと、書き込みをコミットし(通常はファイルを閉じる)、ファイルを再度開く必要があるため、パフォーマンスに顕著な影響があります。他の作品では、fseekはあなたの心配の最小です。
より深い穴を掘るのをやめて、 Valgrind を介してプログラムを実行するだけです。そうすることで、リークやその他の問題が回避されるはずです。