2013-02-15 11 views
6

Zaimplementowałem normalną i równoległą wersję prostej funkcji, która oblicza histogram z mapy bitowej 32bppArgb. Normalna wersja zajmuje około 0,03 sekundy na obrazie 1920 x 1080, podczas gdy równoległa wersja zajmuje 0,07 sekundy.Paralilizacja funkcji histogramu

Czy naprężanie jest naprawdę ciężkie? Czy istnieje jakiś inny konstrukt oprócz Parallel.For może przyspieszyć ten proces? Muszę to przyspieszyć, ponieważ pracuję z filmem 30 klatek na sekundę.

Oto kod uproszczony:

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

Czy próbowałeś, aby każdy wątek obliczył więcej niż 1 linię? Prawdopodobnie ich przetworzenie na 10-20 może trochę przyspieszyć. –

+0

Cóż, zgrupowałem pętlę, która działa 1920 razy z czterema instrukcjami. Nie wiem, jak inaczej to zorganizować. Jakieś sugestie? –

+1

Dla lambda przekazanego do 'Parallel.For', spróbuj zapętlić od' y' do 'y' + (kilka optymalnych liczb, które musisz znaleźć). Oczywiście oznacza to dostosowanie drugiego parametru 'Parallel.For' z' data.Height' do czegoś innego. –

Odpowiedz

8

Cóż, po pierwsze, masz ogromny błąd w Parallel pętli:

będziesz mieć dostęp do wielu wątków, zwiększając i aktualizację współdzielonych tablic - tak działa kod przykładowy na tym samym Obraz wielokrotnie powoduje bardzo różne wyniki ze względu na nieodłączne warunki wyścigu.

Ale nie o to prosiliście.

Jeśli chodzi o przyczyny spadku wydajności przy użyciu implementacji równoległej, prostą odpowiedzią jest to, że prawdopodobnie nie wykonujesz wystarczającej ilości pracy w ciele każdego równoległego zadania, aby skompensować "koszt rozproszenia" tworzenia nowego zadania , planowanie go itd.

Prawdopodobnie bardziej krytyczne jest to, że wierzysz, że wyrzucasz piekło z pamięci podręcznej L1/L2 z wszystkimi skaczącymi w pamięci - każdy wątek z zadaniem spróbuje załadować to, co myśli. będzie potrzebował pamięci podręcznej, ale podczas indeksowania w dowolnym miejscu, nie tworzysz już spójnego wzorca dostępu, więc prawdopodobnie będziesz otrzymywać pomyłki w pamięci podręcznej za każdym razem, gdy spróbujesz uzyskać dostęp do bufora bitmapowego lub wewnętrznych tablic .

Jest też równie wydajnych sposób dotarcia na tylko do odczytu danych z bitmapy bez użycia niebezpieczny kod ... faktycznie, zróbmy to pierwszy:

Więc trzeba, wywołując LockBits, wskaźnik do pamięci niekontrolowana . Zróbmy kopię:

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); 

Teraz, jak na wyścigu - można przezwyciężyć ten w stosunkowo wydajnych sposób za pomocą połączeń Interlocked zderzyć się liczniki (UWAGA !!! programowanie wielowątkowe jest HARD, i to całkiem możliwe, moje rozwiązanie nie jest idealne tutaj!)

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(); 
} 

Więc, co to zrobić z zachowaniem wykonywania?

Nie za dużo, ale przynajmniej widelec równoległy oblicza teraz poprawne wyniki.:)

Korzystanie z naprawdę cheapo stanowiska badawczego:

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); 
    } 
} 

uzyskać wyniki podobne do tego:

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

Jak widać, nadal nie skierowana oryginalne pytanie. :)

Więc rzućmy ukłucie w podejmowaniu pracy równoległej „więcej lepiej”:

Zobaczmy, co „daje więcej pracy” do zadań robi:

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]]); 
      } 
     } 
    ); 
} 

Wyniki:

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

Ooh, wygląda obiecująco ... Zastanawiam się, co się dzieje, gdy zmienimy batchSize?

Niech thusly zmiany naszego stanowiska badawczego:

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); 
     }   
    } 
} 

Wyniki: (tylko pokazując równolegle = true, ponieważ nierównoległe nie zmieni)

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 

Wydaje się, że zbliża się asymptoty rodzajów, gdy tylko osiągniemy rozmiar 64-128 w wielkości partii, chociaż oczywiście przebieg może się różnić w zależności od rozmiaru bitmapy, itp.

Mam nadzieję, że to pomoże! To było zabawne odciągnięcie od mojego dnia oczekiwania na ukończenie produkcji! :)

+0

Dzięki! Odpowiedzi takie jak te są zaraźliwe i zachęcają SO'erów do odpowiedzi na więcej pytań. Brawo. –

+0

Co do notatek, zakładam, że robisz to, aby uniknąć niebezpiecznego kodu, prawda? –

+0

Zastanawiam się, czy istnieje sposób programowo obliczyć optymalną wielkość partii w oparciu o rozmiar obrazu. Oczywiście można użyć heurystyki, ale to nie będzie dobrze pasowało do różnych maszyn. Lub wykonaj korekty środowiska wykonawczego w innym wątku za pomocą platformy testowej podobnej do twojej. –

1

Tworzenie wątków ma dość znaczny narzut. Wykonanie może przebiegać znacznie szybciej niż wersja z pojedynczym gwintem, ale kończy się zbyt szybko, aby nadrobić to początkowe obciążenie.

Jeśli zrobisz to w każdej klatce, po prostu spowolni cię.

Jeśli jednak ręcznie utworzysz paczkę z wątkami, ręcznie przypiszesz pracę i ponownie użyjesz wątków dla każdej klatki, możesz zauważyć, że klatka po klatce dwa lub trzy szybciej przechodzi przez wersję z pojedynczym gwintem.

Powiązane problemy