2013-02-15 19 views
6

私は、32bppArgbビットマップからヒストグラムを計算する簡単な関数の通常の並行したバージョンを実装しました。パラレルバージョンは0.07秒かかりますが、通常のバージョンは1920x1080イメージで約0.03秒かかります。ヒストグラム関数の並列化

スレッドオーバーヘッドは本当に重いですか?このプロセスをスピードアップできるParallel.For以外にもいくつかの構造がありますか?私は30fpsビデオで作業しているので、これをスピードアップする必要があります。ここで

は単純化されたコードです:

public sealed class Histogram 
{ 
    public int MaxA = 0; 
    public int MaxR = 0; 
    public int MaxG = 0; 
    public int MaxB = 0; 
    public int MaxT = 0; 

    public int [] A = null; 
    public int [] R = null; 
    public int [] G = null; 
    public int [] B = null; 

    public Histogram() 
    { 
     this.A = new int [256]; 
     this.R = new int [256]; 
     this.G = new int [256]; 
     this.B = new int [256]; 

     this.Initialize(); 
    } 

    public void Initialize() 
    { 
     this.MaxA = 0; 
     this.MaxR = 0; 
     this.MaxG = 0; 
     this.MaxB = 0; 
     this.MaxT = 0; 

     for (int i = 0; i < this.A.Length; i++) 
      this.A [i] = 0; 
     for (int i = 0; i < this.R.Length; i++) 
      this.R [i] = 0; 
     for (int i = 0; i < this.G.Length; i++) 
      this.G [i] = 0; 
     for (int i = 0; i < this.B.Length; i++) 
      this.B [i] = 0; 
    } 

    public void ComputeHistogram (System.Drawing.Bitmap bitmap, bool parallel = false) 
    { 
     System.Drawing.Imaging.BitmapData data = null; 

     data = bitmap.LockBits 
     (
      new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), 
      System.Drawing.Imaging.ImageLockMode.ReadOnly, 
      System.Drawing.Imaging.PixelFormat.Format32bppArgb 
     ); 

     try 
     { 
      ComputeHistogram(data, parallel); 
     } 
     catch 
     { 
      bitmap.UnlockBits(data); 

      throw; 
     } 

     bitmap.UnlockBits(data); 
    } 

    public void ComputeHistogram (System.Drawing.Imaging.BitmapData data, bool parallel = false) 
    { 
     int stride = System.Math.Abs(data.Stride); 

     this.Initialize(); 

     if (parallel) 
     { 
      unsafe 
      { 
       System.Threading.Tasks.Parallel.For 
       (
        0, 
        data.Height, 
        new System.Threading.Tasks.ParallelOptions() { MaxDegreeOfParallelism = System.Environment.ProcessorCount }, 
        y => 
        { 
         byte* pointer = ((byte*) data.Scan0) + (stride * y); 

         for (int x = 0; x < stride; x += 4) 
         { 
          this.B [pointer [x + 0]]++; 
          this.G [pointer [x + 1]]++; 
          this.R [pointer [x + 2]]++; 
          this.A [pointer [x + 3]]++; 
         } 
        } 
       ); 
      } 
     } 
     else 
     { 
      unsafe 
      { 
       for (int y = 0; y < data.Height; y++) 
       { 
        byte* pointer = ((byte*) data.Scan0) + (stride * y); 

        for (int x = 0; x < stride; x += 4) 
        { 
         this.B [pointer [x + 0]]++; 
         this.G [pointer [x + 1]]++; 
         this.R [pointer [x + 2]]++; 
         this.A [pointer [x + 3]]++; 
        } 
       } 
      } 
     } 

     for (int i = 0; i < this.A.Length; i++) 
      if (this.MaxA < this.A [i]) this.MaxA = this.A [i]; 
     for (int i = 0; i < this.R.Length; i++) 
      if (this.MaxR < this.R [i]) this.MaxR = this.R [i]; 
     for (int i = 0; i < this.G.Length; i++) 
      if (this.MaxG < this.G [i]) this.MaxG = this.G [i]; 
     for (int i = 0; i < this.B.Length; i++) 
      if (this.MaxB < this.B [i]) this.MaxB = this.B [i]; 

     if (this.MaxT < this.MaxA) this.MaxT = this.MaxA; 
     if (this.MaxT < this.MaxR) this.MaxT = this.MaxR; 
     if (this.MaxT < this.MaxG) this.MaxT = this.MaxG; 
     if (this.MaxT < this.MaxB) this.MaxT = this.MaxB; 
    } 
} 
+2

各スレッドが単なる1行以上を計算してみましたか?可能であれば、プロセス10-20を少し速くすることができます。 –

+0

まあ私は4つのステートメントで1920回実行するループをグループ化しました。どのように構造化するかはわかりません。助言がありますか? –

+1

'Parallel.For'に渡されたラムダについては、' y'から 'y' +(見つけなければならない最適な数)にループしてみてください。もちろん、これは 'Parallel.For'の第2パラメータを' data.Height'から別のものに調整することを意味します。 –

答えて

8

まあ、最初のオフ、あなたはあなたの並列ループに巨大なバグを持っています画像を複数回使用すると、本質的な競合条件のために大きく異なる結果になります。

しかし、それはあなたが尋ねたものではありません。

パラレル実装を使用してパフォーマンスが低下するのはなぜですか?単純な答えは、新しいタスクを作成するための「スピンアップコスト」を相殺するために、各並列タスクの本体で十分な作業を行っていない可能性が高いということですそれをスケジューリングするなど。

おそらく、もっと重要なのは、あなたがL1/L2キャッシュから抜け出して、メモリ内を飛び回っていると思うかもしれないということでしょう。各タスクスレッドは、それが思っているものをロードしようとしますキャッシュメモリが必要になりますが、その場でインデックスを作成すると、一貫したアクセスパターンが作成されなくなるため、ビットマップバッファまたは内部配列にアクセスしようとするたびにキャッシュミスが発生する可能性が高くなります。

危険なコードを使用せずに、ビットマップの読み取り専用データの取得の均等パフォーマンスの方法は、実際に、のは、その最初のをやらせる...もあります:

ですから、LockBitsを呼び出して、アンマネージメモリへのポインタを持っています。のは、そのコピーを作ってみましょう:今すぐ

System.Drawing.Imaging.BitmapData data = null; 
data = bitmap.LockBits 
(
    new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), 
    System.Drawing.Imaging.ImageLockMode.ReadOnly, 
    System.Drawing.Imaging.PixelFormat.Format32bppArgb 
); 

// For later usage 
var imageStride = data.Stride; 
var imageHeight = data.Height; 

// allocate space to hold the data 
byte[] buffer = new byte[data.Stride * data.Height]; 

// Source will be the bitmap scan data 
IntPtr pointer = data.Scan0; 

// the CLR marshalling system knows how to move blocks of bytes around, FAST. 
Marshal.Copy(pointer, buffer, 0, buffer.Length); 

// and now we can unlock this since we don't need it anymore 
bitmap.UnlockBits(data); 

ComputeHistogram(buffer, imageStride, imageHeight, parallel); 

、競合状態のためとして - あなたはNOTE !!!マルチスレッドプログラミングは(カウントをつり上げるためにInterlocked呼び出しをされて使用することにより、合理的にパフォーマンス的にこれを克服することができますHARD、それはここに私の解決策は完璧ではありません!完全に可能です)

public void ComputeHistogram (byte[] data, int stride, int height, bool parallel = false) 
{ 
    this.Initialize(); 

    if (parallel) 
    { 
     System.Threading.Tasks.Parallel.For 
     (
      0, 
      height, 
      new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount }, 
      y => 
      { 
       int startIndex = (stride * y); 
       int endIndex = stride * (y+1); 
       for (int x = startIndex; x < endIndex; x += 4) 
       { 
        // Interlocked actions are more-or-less atomic 
        // (caveats abound, but this should work for us) 
        Interlocked.Increment(ref this.B[data[x]]); 
        Interlocked.Increment(ref this.G[data[x+1]]); 
        Interlocked.Increment(ref this.R[data[x+2]]); 
        Interlocked.Increment(ref this.A[data[x+3]]); 
       } 
      } 
     ); 
    } 
    else 
    { 
     // the original way is ok for non-parallel, since only one 
     // thread is mucking around with the data 
    } 

    // Sorry, couldn't help myself, this just looked "cleaner" to me 
    this.MaxA = this.A.Max(); 
    this.MaxR = this.R.Max(); 
    this.MaxG = this.G.Max(); 
    this.MaxB = this.B.Max(); 
    this.MaxT = new[] { this.MaxA, this.MaxB, this.MaxG, this.MaxR }.Max(); 
} 

だから、これは実行時の動作に何をするのでしょうか?

ロット全体ではありませんが、少なくとも並列フォークは正しい結果を計算します。:)本当にテストリグ安っぽい使用

:私はこのような結果を得る

void Main() 
{  
    foreach(var useParallel in new[]{false, true}) 
    { 
     var totalRunTime = TimeSpan.Zero; 
     var sw = new Stopwatch(); 
     var runCount = 10; 
     for(int run=0; run < runCount; run++) 
     { 
      GC.Collect(); 
      GC.WaitForPendingFinalizers(); 
      GC.Collect(); 
      sw.Reset(); 
      sw.Start(); 
      var bmp = Bitmap.FromFile(@"c:\temp\banner.bmp") as Bitmap; 
      var hist = new Histogram(); 
      hist.ComputeHistogram(bmp, useParallel); 
      sw.Stop(); 
      totalRunTime = totalRunTime.Add(sw.Elapsed); 
     } 
     Console.WriteLine("Parallel={0}, Avg={1} ms", useParallel, totalRunTime.TotalMilliseconds/runCount); 
    } 
} 

を:あなたが見ることができるように

Parallel=False, Avg=1.69777 ms 
Parallel=True, Avg=5.33584 ms 

を、我々はまだあなたの元の質問に対処していません。 :)それでは、並列作業は「より良く」することで刺してみましょう

:タスクへ

さんは「より多くの仕事を与える」か見てみましょうではありません:

if (parallel) 
{ 
    var batchSize = 2; 
    System.Threading.Tasks.Parallel.For 
    (
     0, 
     height/batchSize, 
     new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount }, 
     y => 
     { 
      int startIndex = (stride * y * batchSize); 
      int endIndex = startIndex + (stride * batchSize); 
      for (int x = startIndex; x < endIndex; x += 4) 
      { 
       // Interlocked actions are more-or-less atomic 
       // (caveats abound, but this should work for us) 
       Interlocked.Increment(ref this.B[data[x]]); 
       Interlocked.Increment(ref this.G[data[x+1]]); 
       Interlocked.Increment(ref this.R[data[x+2]]); 
       Interlocked.Increment(ref this.A[data[x+3]]); 
      } 
     } 
    ); 
} 

結果:

Parallel=False, Avg=1.70273 ms 
Parallel=True, Avg=4.82591 ms 

おっと、それは有望そうです...私たちは変化するので何が起こるのだろうかbatchSize

のは以下のようなものを我々のテストリグを変更してみましょう:

void Main() 
{  
    foreach(var useParallel in new[]{false, true}) 
    { 
     for(int batchSize = 1; batchSize < 1024; batchSize <<= 1) 
     { 
      var totalRunTime = TimeSpan.Zero; 
      var sw = new Stopwatch(); 
      var runCount = 10; 
      for(int run=0; run < runCount; run++) 
      { 
       GC.Collect(); 
       GC.WaitForPendingFinalizers(); 
       GC.Collect(); 
       sw.Reset(); 
       sw.Start(); 
       var bmp = Bitmap.FromFile(@"c:\temp\banner.bmp") as Bitmap; 
       var hist = new Histogram(); 
       hist.ComputeHistogram(bmp, useParallel, batchSize); 
       sw.Stop(); 
       totalRunTime = totalRunTime.Add(sw.Elapsed); 
      } 
      Console.WriteLine("Parallel={0}, BatchSize={1} Avg={2} ms", useParallel, batchSize, totalRunTime.TotalMilliseconds/runCount); 
     }   
    } 
} 

結果:(非平行が変更されないので、唯一、真=パラレル示す)

Parallel=True, BatchSize=1 Avg=5.57644 ms 
Parallel=True, BatchSize=2 Avg=5.49982 ms 
Parallel=True, BatchSize=4 Avg=5.20434 ms 
Parallel=True, BatchSize=8 Avg=5.1721 ms 
Parallel=True, BatchSize=16 Avg=5.00405 ms 
Parallel=True, BatchSize=32 Avg=4.44973 ms 
Parallel=True, BatchSize=64 Avg=2.28332 ms 
Parallel=True, BatchSize=128 Avg=1.39957 ms 
Parallel=True, BatchSize=256 Avg=1.29156 ms 
Parallel=True, BatchSize=512 Avg=1.28656 ms 

我々は漸近線に近づいているように見えます一度私たちはバッチサイズで64-128の範囲を取得しますが、もちろんあなたのビットマップのサイズなどに応じて異なることがあります。

私はこれが助けてくれることを望みます!プロダクションビルドが完了するのを待っていたのは、楽しい気分でした。 :)

+0

ありがとう!これらのような答えは伝染性があり、SO'ersがより多くの質問に答えることを奨励します。ブラボー –

+0

memcopyに関しては、安全でないコードを避けるためにあなたはそれをやっていると思いますか? –

+0

イメージサイズに基づいて最適なバッチサイズをプログラムで計算する方法があるのだろうかと思います。もちろん、ヒューリスティックを使うことはできますが、それは別のマシンにはうまくいきません。または、あなたと同様のテストリグを使用して別のスレッドで実行時の調整を行います。 –

1

は、スレッドの作成は非常に大きなオーバーヘッドがあります。実行はシングルスレッドバージョンよりも大幅に高速に実行できますが、この初期オーバーヘッドを補うには速すぎます。

これをフレームごとに行うと、遅くなるだけです。

ただし、手動でスレッドプールを作成し、手動でワークを割り当て、フレームごとにスレッドを再利用すると、フレームの2〜3つのコードロケットが単一のスレッドバージョンを過ぎていることがわかります。 - ちょうど同じであなたのサンプルコードを実行しているあなたは、複数のスレッドが、アクセスインクリメント、および共有の配列を更新する必要があるとしている

関連する問題