4

現在、ICsharpCode.SharpZipLibライブラリのGZipOutputStreamクラスを圧縮に使用しています。単一のスレッドから実行します。

入力データストリームをチャンクに分割し、それらを並列に圧縮したいと思います。このライブラリには、複数のスレッドから上書きされて結果のストリームが破損する静的なものが含まれている可能性があるのではないかと心配しています。

どんな考えでもありがたいです。

4

3 に答える 3

11

これは本当に興味深い質問です。圧縮はCPUに非常に負荷がかかり、多くの検索と比較に依存します。したがって、メモリアクセスが妨げられていない複数のCPUがある場合は、並列化するのが非常に適切です。

ParallelDeflateOutputStreamあなたが説明していることを行うDotNetZipライブラリ内に呼び出されるクラスがあります。クラスはここに文書化されています。

圧縮にのみ使用でき、解凍はできません。readまた、厳密には出力ストリームです。圧縮することはできません。これらの制約を考慮すると、基本的にはDeflateOutputStreamであり、内部で複数のスレッドを使用します。

仕組み:着信ストリームをチャンクに分割し、各チャンクを個別のワーカースレッドにドロップして、個別に圧縮します。次に、これらすべての圧縮ストリームをマージして、最後に1つの順序付けられたストリームに戻します。

ストリームによって維持される「チャンク」サイズがNバイトであると仮定します。呼び出し元がWrite()を呼び出すと、データはバケットまたはチャンクにバッファリングされます。メソッド内ではStream.Write()、最初の「バケット」がいっぱいになると、を呼び出しThreadPool.QueueUserWorkItemて、バケットを作業項目に割り当てます。ストリームへの後続の書き込みは次のバケットの充填を開始し、それがいっぱいになると、再度Stream.Write()呼び出しますQUWI。各ワーカースレッドは、の「フラッシュタイプ」Sync(deflate仕様を参照)を使用してバケットを圧縮し、圧縮されたBLOBに出力の準備ができていることを示します。次に、このさまざまな出力が並べ替えられ(チャンクnは必ずしもチャンクn + 1の前に圧縮されるとは限らないため)、キャプティブ出力ストリームに書き込まれます。各バケットが書き込まれると、空のマークが付けられ、次のバケットで補充できるようになります。Stream.Write()。結合されたバイトストリームが正当なDEFLATEストリームになるためには、単純な連結による再結合を可能にするために、各チャンクをフラッシュタイプの同期で圧縮する必要があります。最後のチャンクには、フラッシュタイプ=終了が必要です。

このストリームの設計は、呼び出し元が複数のスレッドで書き込む必要がないことを意味します。 呼び出し元は、出力に使用されるバニラDeflateStreamのように、通常どおりストリームを作成し、それに書き込みます。ストリームオブジェクトは複数のスレッドを使用しますが、コードはそれらと直接インターフェースしません。の「ユーザー」のコードはParallelDeflateOutputStream次のようになります。

using (FileStream raw = new FileStream(CompressedFile, FileMode.Create))
{
    using (FileStream input = File.OpenRead(FileToCompress))
    {
        using (var compressor = new Ionic.Zlib.ParallelDeflateOutputStream(raw))
        {
            // could tweak params of parallel deflater here
            int n;
            var buffer = new byte[8192];
            while ((n = input.Read(buffer, 0, buffer.Length)) != 0)
            {
                compressor.Write(buffer, 0, n);
            }                    
        }
    }
}

DotNetZip ZipFileクラス内で使用するように設計されていますが、スタンドアロンの圧縮出力ストリームとして非常に使用できます。結果として得られるストリームは、任意のインフレータでデデフレート(膨張?)することができます。結果は仕様に完全に準拠しています。

ストリームは調整可能です。使用するバッファーのサイズと並列処理のレベルを設定できます。大きなストリーム(GBスケールなど)の場合、メモリ不足の状態が発生するため、無制限にバケットを作成することはありません。したがって、サポートできるバケットの数、つまり並列度には一定の制限があります。

私のデュアルコアマシンでは、このストリームクラスは、標準のDeflateStreamと比較して、大きな(100mb以上の)ファイルの圧縮速度をほぼ2倍にしました。私はより大きなマルチコアマシンを持っていないので、それ以上テストすることはできませんでした。トレードオフは、並列実装がより多くのCPUとより多くのメモリを使用し、上記の同期フレーミングのために圧縮効率がわずかに低下することです(大きなファイルの場合は1%少なくなります)。パフォーマンスの利点は、出力ストリームのI / Oスループット、およびストレージが並列コンプレッサースレッドに対応できるかどうかによって異なります。


警告:
これはDEFLATEストリームであり、GZIPではありません。違いについては、 RFC 1951(DEFLATE)RFC 1952(GZIP)をお読みください。

ただし、本当にgzipが必要な場合は、このストリームのソースを利用できるので、それを表示して、自分でアイデアを得ることができます。GZIPは、実際にはDEFLATEの単なるラッパーであり、いくつかの追加のメタデータ(Adlerチェックサムなど-仕様を参照)があります。を構築することはそれほど難しいことではないように私には思えますParallelGzipOutputStreamが、それも簡単ではないかもしれません。

私にとって最も難しい部分は、Flush()とClose()のセマンティクスを正しく機能させることでした。


編集

楽しみのために、GZip用にParallelGZipOutputStreamを作成しました。これは、基本的に上記で説明したことを実行します。QUWIの代わりに.NET4.0のタスクを使用して、並列圧縮を処理します。マルコフ連鎖エンジンを介して生成された100MBのテキストファイルでテストしました。そのクラスの結果を他のいくつかのオプションと比較しました。外観は次のとおりです。

uncompressed: 104857600
running 2 cycles, 6 Flavors

System.IO.Compression.GZipStream:  .NET 2.0 builtin
  compressed: 47550941
  ratio     : 54.65%
  Elapsed   : 19.22s

ICSharpCode.SharpZipLib.GZip.GZipOutputStream:  0.86.0.518
  compressed: 37894303
  ratio     : 63.86%
  Elapsed   : 36.43s

Ionic.Zlib.GZipStream:  DotNetZip v1.9.1.5, CompLevel=Default
  compressed: 37896198
  ratio     : 63.86%
  Elapsed   : 39.12s

Ionic.Zlib.GZipStream:  DotNetZip v1.9.1.5, CompLevel=BestSpeed
  compressed: 47204891
  ratio     : 54.98%
  Elapsed   : 15.19s

Ionic.Exploration.ParallelGZipOutputStream: DotNetZip v1.9.1.5, CompLevel=Default
  compressed: 39524723
  ratio     : 62.31%
  Elapsed   : 20.98s

Ionic.Exploration.ParallelGZipOutputStream:DotNetZip v1.9.1.5, CompLevel=BestSpeed
  compressed: 47937903
  ratio     : 54.28%
  Elapsed   : 9.42s

結論:

  1. .NETに組み込まれているGZipStreamは非常に高速です。また、あまり効率的ではなく、調整もできません。

  2. DotNetZipのバニラ(並列化されていない)GZipStreamの「BestSpeed」は、.NET組み込みストリームよりも約20%高速であり、ほぼ同じ圧縮を提供します。

  3. 圧縮に複数のタスクを使用すると、バニラのDotNetZip GZipStreamを並列のものと比較して、デュアルコアラップトップ(3GB RAM)で必要な時間を約45%短縮できます。コア数が多いマシンほど時間の節約になると思います。

  4. GZIPを並列化するにはコストがかかります。フレーミングにより、圧縮ファイルのサイズが約4%増加します。これは、使用するコアの数によって変わりません。

結果の.gzファイルは、任意のGZIPツールで解凍できます。

于 2011-05-06T17:10:15.900 に答える
1

私の理解では、zipは単一の基になるストリームに書き込み(または読み取り)しています。だから私の仮定ははっきりとノーだろう; 単一の基になるストリームについて話している場合、これはスレッドセーフにはなりません。

でも; 基になるストリームを分離するために通信する個別のインスタンスは問題ないはずです。実際、通常、単一のタスクを並列化するよりも、別々の(無関係の)タスクを並列に実行する方が簡単です。

于 2011-05-05T16:47:19.257 に答える
0

クラスをコーディングするときは、すべての静的メンバーがスレッドセーフであることを確認するのが標準的な方法です。ですから、その問題が原因で問題が発生する可能性は非常に低いと思います。もちろん、異なるスレッドから同じものを使用することを計画している場合、そのクラスのインスタンスメンバーはスレッドセーフではないため、これは間違いなく問題になります。 GZipOutputStream

あなたができるかもしれないことは、スレッドセーフなミドルマンStreamクラス(デコレータパターンを考えてください)を作成し、それをに渡すことGZipOutputStreamです。このカスタムストリームクラスは、それを呼び出しThreadSafeStream、それ自体がインスタンスを受け入れStream、適切なメカニズムを使用してそのインスタンスへのアクセスを同期します。

GZipOutputStreamスレッドごとに1つのインスタンスを作成し、それらはすべて同じThreadSafeStreamラッパーインスタンスを共有します。メソッドにはおそらく多くのボトルネックがあるとThreadSafeStream思いますが、これからある程度の並列性を得ることができるはずです。

于 2011-05-05T18:42:19.653 に答える