19

逆座標でネストされたループを単純に実行するよりも、大きなビットマップを 90 度または 270 度回転させるより速い方法はありますか?

ビットマップは 8bpp で、通常は 2048x2400x8bpp です。

現在、私はこれを、大まかに引数を反転してコピーするだけで行っています(疑似コード:

for x = 0 to 2048-1
  for y = 0 to 2048-1
    dest[x][y]=src[y][x];

(実際には、もう少し速度を上げるためにポインターを使用しますが、それはほぼ同じ大きさです)

GDI は大きな画像では非常に遅く、テクスチャ (GF7 カード) の GPU ロード/ストア時間は現在の CPU 時間と同じ大きさです。

ヒント、指針はありますか?インプレース アルゴリズムはさらに優れていますが、速度はインプレースよりも重要です。

ターゲットは Delphi ですが、それはアルゴリズムの問​​題です。SSE(2) のベクトル化は問題ありません。アセンブラーでコーディングするには十分な問題です。


ニルスの答えをフォローアップ

  • 画像 2048x2700 -> 2700x2048
  • 最適化をオンにしたコンパイラ Turbo Explorer 2006。
  • Windows: 電源設定が「常時オン」に設定されています。(重要!!!! )
  • マシン: Core2 6600 (2.4 GHz)

古いルーチンの時間: 32ms (ステップ 1)

ステップサイズ 8 の時間: 12ms

ステップサイズ 16 の時間: 10ms

ステップサイズ 32+ の時間: 9ms

一方、Athlon 64 X2 (5200+ iirc) でもテストを行いましたが、速度は 4 倍をわずかに上回りました (80 ~ 19 ミリ秒)。

スピードアップはそれだけの価値があります、ありがとう。たぶん、夏の間は SSE(2) バージョンで自分を苦しめるでしょう。しかし、私はすでにそれに取り組む方法を考えていました。ストレートな実装では SSE2 レジスタが不足すると思います。

for n:=0 to 7 do
  begin
    load r0, <source+n*rowsize> 
    shift byte from r0 into r1
    shift byte from r0 into r2
    ..
    shift byte from r0 into r8
  end; 
store r1, <target>   
store r2, <target+1*<rowsize>
..
store r8, <target+7*<rowsize>   

したがって、8x8 には 9 つのレジスタが必要ですが、32 ビット SSE には 8 つしかありません。とにかく、それは夏の間のものです :-)

ポインターのことは私が本能的に行うものですが、実際には何かがある可能性があります。次元がハードコーディングされていない場合、コンパイラーは mul をシフトに変換できません。最近ではマルチは安価ですが、より多くの音域のプレッシャーも発生します。

コード (「素朴な」rotate1 実装から結果を減算することによって検証されます):

const stepsize = 32;
procedure rotatealign(Source: tbw8image; Target:tbw8image);

var stepsx,stepsy,restx,resty : Integer;
   RowPitchSource, RowPitchTarget : Integer;
   pSource, pTarget,ps1,ps2 : pchar;
   x,y,i,j: integer;
   rpstep : integer;
begin
  RowPitchSource := source.RowPitch;          // bytes to jump to next line. Can be negative (includes alignment)
  RowPitchTarget := target.RowPitch;        rpstep:=RowPitchTarget*stepsize;
  stepsx:=source.ImageWidth div stepsize;
  stepsy:=source.ImageHeight div stepsize;
  // check if mod 16=0 here for both dimensions, if so -> SSE2.
  for y := 0 to stepsy - 1 do
    begin
      psource:=source.GetImagePointer(0,y*stepsize);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(target.imagewidth-(y+1)*stepsize,0);
      for x := 0 to stepsx - 1 do
        begin
          for i := 0 to stepsize - 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[stepsize-1-i];       //  (maxx-i,0);
              for j := 0 to stepsize - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
          inc(psource,stepsize);
          inc(ptarget,rpstep);
        end;
    end;
  // 3 more areas to do, with dimensions
  // - stepsy*stepsize * restx        // right most column of restx width
  // - stepsx*stepsize * resty        // bottom row with resty height
  // - restx*resty                    // bottom-right rectangle.
  restx:=source.ImageWidth mod stepsize;   // typically zero because width is 
                                          // typically 1024 or 2048
  resty:=source.Imageheight mod stepsize;
  if restx>0 then
    begin
      // one loop less, since we know this fits in one line of  "blocks"
      psource:=source.GetImagePointer(source.ImageWidth-restx,0);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(Target.imagewidth-stepsize,Target.imageheight-restx);
      for y := 0 to stepsy - 1 do
        begin
          for i := 0 to stepsize - 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[stepsize-1-i];       //  (maxx-i,0);
              for j := 0 to restx - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
         inc(psource,stepsize*RowPitchSource);
         dec(ptarget,stepsize);
       end;
    end;
  if resty>0 then
    begin
      // one loop less, since we know this fits in one line of  "blocks"
      psource:=source.GetImagePointer(0,source.ImageHeight-resty);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(0,0);
      for x := 0 to stepsx - 1 do
        begin
          for i := 0 to resty- 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[resty-1-i];       //  (maxx-i,0);
              for j := 0 to stepsize - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
         inc(psource,stepsize);
         inc(ptarget,rpstep);
       end;
    end;
 if (resty>0) and (restx>0) then
    begin
      // another loop less, since only one block
      psource:=source.GetImagePointer(source.ImageWidth-restx,source.ImageHeight-resty);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(0,target.ImageHeight-restx);
      for i := 0 to resty- 1 do
        begin
          ps1:=@psource[rowpitchsource*i];   // ( 0,i)
          ps2:=@ptarget[resty-1-i];       //  (maxx-i,0);
          for j := 0 to restx - 1 do
            begin
              ps2[0]:=ps1[j];
              inc(ps2,RowPitchTarget);
            end;
       end;
    end;
end;

更新 2 ジェネリック

このコードを Delphi XE のジェネリック バージョンに更新しようとしました。QC 99703 が原因で失敗しました。フォーラムの人々は、XE2 にも存在することを既に確認しています。それに投票してください:-)

Update 3 Generics Works in XE10

更新 4

2017 年に、8bpp 画像のみの 8x8 キューブのアセンブラー バージョンと、Peter Cordes が寛大に私を助けてくれたシャッフルのボトルネックに関する関連するSO の質問にいくつかの作業を行いました。このコードはまだ機会を逃しており、複数の 8x8 ブロックの繰り返しを 64x64 のような疑似大きなものに集約するために、別のループタイリング レベルが再度必要です。今はまた行全体であり、それは無駄です。

4

4 に答える 4

22

はい、これを行うより速い方法があります。

単純なループは、ほとんどの時間をキャッシュ ミスに費やしています。これは、タイトなループの非常に異なる場所で大量のデータに触れるために発生します。さらに悪いことに、メモリの場所は正確に 2 の累乗です。これは、キャッシュのパフォーマンスが最も悪いサイズです。

メモリ アクセスの局所性を改善すると、このローテーション アルゴリズムを改善できます。

これを行う簡単な方法は、ビットマップ全体に使用したのと同じコードを使用して、各 8x8 ピクセル ブロックを独自に回転させ、画像の回転をそれぞれ 8x8 ピクセルのチャンクに分割する別のループをラップすることです。

たとえば、次のようなものです(チェックされておらず、Cコードで申し訳ありません。私のDelphiスキルは最新ではありません):

 // this is the outer-loop that breaks your image rotation
 // into chunks of 8x8 pixels each:
 for (int block_x = 0; block_x < 2048; block_x+=8)
 {
    for (int block_y = 0; blocky_y < 2048; block_y+=8)
    { 
       // this is the inner-loop that processes a block
       // of 8x8 pixels.
       for (int x= 0; x<8; x++)
         for (int y=0; y<8; y++)
            dest[x+block_x][y+block_y] = src[y+block_y][x+block_x]
    }
 } 

他の方法もあります。Hilbert-Order または Morton-Order でデータを処理できます。これは理論的にはさらに高速ですが、コードははるかに複雑になります。

ところで-SSEがオプションであると述べたので。SSE レジスタ内で 8x8 バイト ブロックをローテーションできることに注意してください。それを機能させるのは少し難しいですが、SSE マトリックス転置コードを見ると、同じことなので始められるはずです。


編集:

ちょうどチェックしました:

ブロックサイズが 8x8 ピクセルの場合、コードは約 1 秒で実行されます。私のマシンでは5倍高速です。ブロックサイズが 16x16 の場合、10 倍高速に実行されます。

さまざまなブロックサイズを試してみることをお勧めします。

私が使用した(非常に単純な)テストプログラムは次のとおりです。

#include <stdio.h>
#include <windows.h>

char temp1[2048*2048];
char temp2[2048*2048];

void rotate1 (void)
{
  int x,y;
  for (y=0; y<2048; y++)
  for (x=0; x<2048; x++)
    temp2[2048*y+x] = temp1[2048*x+y];
}

void rotate2 (void)
{
  int x,y;
  int bx, by;

  for (by=0; by<2048; by+=8)
  for (bx=0; bx<2048; bx+=8)
  for (y=0; y<8; y++)
  for (x=0; x<8; x++)
    temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
}

void rotate3 (void)
{
  int x,y;
  int bx, by;

  for (by=0; by<2048; by+=16)
  for (bx=0; bx<2048; bx+=16)
  for (y=0; y<16; y++)
  for (x=0; x<16; x++)
    temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
}


int main (int argc, char **args)
{
  int i, t1;

  t1 = GetTickCount();
  for (i=0; i<20; i++) rotate1();
  printf ("%d\n", GetTickCount()-t1);

  t1 = GetTickCount();
  for (i=0; i<20; i++) rotate2();
  printf ("%d\n", GetTickCount()-t1);

  t1 = GetTickCount();
  for (i=0; i<20; i++) rotate3();
  printf ("%d\n", GetTickCount()-t1);

}
于 2009-05-11T13:34:05.287 に答える
0

画像が正方形でない場合、その場で行うことはできません。正方形の画像で作業している場合でも、変換はインプレース作業には適していません。

物事をもう少し速くしたい場合は、行のストライドを利用して機能させることができますが、ソースから一度に 4 バイトずつ読み取り、次に、dest の 4 つの連続する行に書き込みます。これにより、オーバーヘッドの一部が削減されるはずですが、5% 以上の改善は期待できません。

于 2009-05-11T13:59:25.967 に答える
0

現時点では、いずれかの src dest のストライドがミスになるため (delphi が行優先か列優先かによって)、行単位ではなくキャッシュに整列されたブロックをコピーすることで改善できる場合があります

于 2009-05-11T13:16:41.193 に答える