23

私はかなり長い間アセンブラを学んでいて、パフォーマンス上の利点(もしあれば)を見るためにいくつかの簡単なプロシージャ\関数をそれに書き直そうとしています。私の主な開発ツールはDelphi2007で、最初の例はその言語で作成されますが、他の言語にも簡単に翻訳できます。

問題は次のように述べています。

8ビットのそれぞれが画面の1行のピクセルを表す符号なしバイト値を指定しました。各単一ピクセルは、ソリッド(1)またはトランスペアレント(0)にすることができます。つまり、1バイトの値に8ピクセルがパックされています。最も若いピクセル(ビット)が配列の最も低いインデックスの下に着地するように、これらのピクセルを8バイト配列にアンパックしたいと思います。次に例を示します。

One byte value -----------> eight byte array

10011011 -----------------> [1][1][0][1][1][0][0][1]

Array index number ------->  0  1  2  3  4  5  6  7

以下に、問題を解決している5つの方法を示します。次に、彼らの時間比較と、それらの時間をどのように測定したかを示します。

私の質問は2つの部分で構成されています。

1.1。

方法との詳細な回答をお願いします。なぜメソッドはよりもやや遅いのですか?DecodePixels4aDecodePixels4b4b4a

たとえば、コードが正しく配置されていないために処理速度が遅い場合は、特定のメソッドのどの命令をより適切に配置できるか、およびメソッドを壊さないようにする方法を教えてください。

理論の裏にある実例を見たいと思います。私はアセンブリを学んでいることを覚えておいてください。あなたの答えから知識を得て、将来、より最適化されたコードを書くことができるようにしたいと思います。

2.2。

あなたはより速いルーチンを書くことができますDecodePixels4aか?もしそうなら、それを提示し、あなたが取った最適化のステップを説明してください。より高速なルーチンとは、ここに示されているすべてのルーチンの中で、テスト環境で最短時間で実行されるルーチンを意味します。

すべてのインテルファミリープロセッサーが許可されており、それらと互換性があります。

以下に私が書いたルーチンがあります:

procedure DecodePixels1(EncPixels: Byte; var DecPixels: TDecodedPixels);
var
  i3: Integer;
begin
  DecPixels[0] := EncPixels and $01;
  for i3 := 1 to 7 do
  begin
    EncPixels := EncPixels shr 1;
    DecPixels[i3] := EncPixels and $01;
    //DecPixels[i3] := (EncPixels shr i3) and $01;  //this is even slower if you replace above 2 lines with it
  end;
end;


//Lets unroll the loop and see if it will be faster.
procedure DecodePixels2(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels[0] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[1] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[2] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[3] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[4] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[5] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[6] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[7] := EncPixels and $01;
end;


procedure DecodePixels3(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    push ecx;
    mov bl, al;
    and bl, $01;
    mov [edx], bl;
    mov ecx, $00;
@@Decode:
    inc ecx;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + ecx], bl;
    cmp ecx, $07;
    jnz @@Decode;
    pop ecx;
    pop ebx;
    pop eax;
  end;
end;


//Unrolled assembly loop
procedure DecodePixels4a(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    mov bl, al;
    and bl, $01;
    mov  [edx], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $01], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $02], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $03], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $04], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $05], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $06], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $07], bl;
    pop ebx;
    pop eax;
  end;
end;


// it differs compared to 4a only in switching two instructions (but seven times)
procedure DecodePixels4b(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx], bl;        //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $01], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $02], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $03], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $04], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $05], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $06], bl;  //
    mov bl, al;
    and bl, $01;
    mov [edx + $07], bl;
    pop ebx;
    pop eax;
  end;
end;

そして、これが私がそれらをテストする方法です:

program Test;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

type
  TDecodedPixels = array[0..7] of Byte;
var
  Pixels: TDecodedPixels;
  Freq, TimeStart, TimeEnd :Int64;
  Time1, Time2, Time3, Time4a, Time4b: Extended;
  i, i2: Integer;

begin
  if QueryPerformanceFrequency(Freq) then
  begin
    for i2 := 1 to 100 do
    begin
      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels1(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time1 := Time1 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels2(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time2 := Time2 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels3(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time3 := Time3 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels4a(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time4a := Time4a + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels4b(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time4b := Time4b + ((TimeEnd - TimeStart) / Freq * 1000);

    end;
    Writeln('Time1 : ' + FloatToStr(Time1 / 100) + ' ms.    <- Delphi loop.');
    Writeln('Time2 : ' + FloatToStr(Time2 / 100) + ' ms.    <- Delphi unrolled loop.');
    Writeln('Time3 : ' + FloatToStr(Time3/ 100) + ' ms.    <- BASM loop.');
    Writeln('Time4a : ' + FloatToStr(Time4a / 100) + ' ms.    <- BASM unrolled loop.');
    Writeln('Time4b : ' + FloatToStr(Time4b / 100) + ' ms.    <- BASM unrolled loop instruction switch.');
  end;
  Readln;
end.

これが私のマシン(Win32 XP上のIntel®Pentium®E2180)の結果です。

Time1  : 1,68443549919493 ms.     <- Delphi loop.
Time2  : 1,33773024572211 ms.     <- Delphi unrolled loop.
Time3  : 1,37015271374424 ms.     <- BASM loop.
Time4a : 0,822916962526627 ms.    <- BASM unrolled loop.
Time4b : 0,862914462301607 ms.    <- BASM unrolled loop instruction switch.

結果はかなり安定しています。時間は、私が行った各テスト間で数パーセントしか変化しません。そしてそれは常に真実でした:Time1 > Time3 > Time 2 > Time4b > Time4a

したがって、Time4aとTime4bの違いは、メソッド内の命令の切り替えに依存すると思いますDecodePixels4b。4%の場合もあれば、最大10%の場合もありますが、4b常により遅くなり4aます。

一度に8バイトをメモリに書き込むMMX命令を使用する別の方法を考えていましたが、64ビットレジスタにバイトをアンパックする高速な方法がわかりません。

お時間をいただきありがとうございます。


貴重なご意見ありがとうございました。残念ながら、現代のCPUと比較すると、すべての人に同時に答えることができましたが、「パイプ」は1つしかなく、一度に「応答」する命令は1つしか実行できません;-)それで、いくつかのことをまとめてみますここに、あなたの答えの下に追加のコメントを書いてください。

まず、質問を投稿する前に、Wouter van Nifterickによって提示された解決策を思いついたのですが、実際にはアセンブリコードよりもはるかに低速でした。そのため、ここではそのルーチンを投稿しないことにしましたが、ループDelphiバージョンのルーチンでも同じアプローチを採用したことがわかります。それは私に悪い結果を与えていたので、そこにコメントされています。

これは私にとって謎です。WouterとPhilSのルーチンを使用してコードをもう一度実行しました。結果は次のとおりです。

Time1  : 1,66535493194387 ms.     <- Delphi loop.
Time2  : 1,29115785420688 ms.     <- Delphi unrolled loop.
Time3  : 1,33716934524107 ms.     <- BASM loop.
Time4a : 0,795041753757838 ms.    <- BASM unrolled loop.
Time4b : 0,843520166815013 ms.    <- BASM unrolled loop instruction switch.
Time5  : 1,49457681191307 ms.     <- Wouter van Nifterick, Delphi unrolled
Time6  : 0,400587402866258 ms.    <- PhiS, table lookup Delphi
Time7  : 0,325472442519827 ms.    <- PhiS, table lookup Delphi inline
Time8  : 0,37350491544239 ms.     <- PhiS, table lookup BASM

Time5の結果を見てください、かなり奇妙ですね。生成されたアセンブリコードがWouterによって提供されたものと異なるため、Delphiのバージョンが異なると思います。

2番目の主要な編集:


なぜ5私のマシンでルーチンが遅くなったのか知っています。コンパイラオプションで「範囲チェック」と「オーバーフローチェック」をチェックしました。assemblerルーチンにディレクティブを追加9して、それが役立つかどうかを確認しました。このディレクティブアセンブリ手順では、Delphiインラインバリアントと同じか、それよりもわずかに優れているようです。

最終結果は次のとおりです。

Time1  : 1,22508325749317 ms.     <- Delphi loop.
Time2  : 1,33004145373084 ms.     <- Delphi unrolled loop.
Time3  : 1,1473583622526 ms.      <- BASM loop.
Time4a : 0,77322594033463 ms.     <- BASM unrolled loop.
Time4b : 0,846033593023372 ms.    <- BASM unrolled loop instruction switch.
Time5  : 0,688689382044384 ms.    <- Wouter van Nifterick, Delphi unrolled
Time6  : 0,503233741036693 ms.    <- PhiS, table lookup Delphi
Time7  : 0,385254722925063 ms.    <- PhiS, table lookup Delphi inline
Time8  : 0,432993919452751 ms.    <- PhiS, table lookup BASM
Time9  : 0,362680491244212 ms.    <- PhiS, table lookup BASM with assembler directive

3番目の主要な編集:


意見では、@ Pascal Cuoqと@j_random_hackerは、ルーチン間の実行時間の違いで4aあり4b5データの依存関係が原因です。しかし、私が行ったさらなるテストに基づいて、私はその意見に反対しなければなりません。

4cまた、に基づいて新しいルーチンを発明しました4a。ここにあります:

procedure DecodePixels4c(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push ebx;
    mov bl, al;
    and bl, 1;
    mov [edx], bl;
    mov bl, al;
    shr bl, 1;
    and bl, 1;
    mov [edx + $01], bl;
    mov bl, al;
    shr bl, 2;
    and bl, 1;
    mov [edx + $02], bl;
    mov bl, al;
    shr bl, 3;
    and bl, 1;
    mov [edx + $03], bl;
    mov bl, al;
    shr bl, 4;
    and bl, 1;
    mov [edx + $04], bl;
    mov bl, al;
    shr bl, 5;
    and bl, 1;
    mov [edx + $05], bl;
    mov bl, al;
    shr bl, 6;
    and bl, 1;
    mov [edx + $06], bl;
    shr al, 7;
    and al, 1;
    mov [edx + $07], al;
    pop ebx;
  end;
end;

私はそれがかなりデータに依存していると言うでしょう。

そして、ここにテストと結果があります。事故がないことを確認するために4つのテストを行いました。また、GJによって提案されたルーチン(Time10a、Time10b)に新しい時間を追加しました。

          Test1  Test2  Test3  Test4

Time1   : 1,211  1,210  1,220  1,213
Time2   : 1,280  1,258  1,253  1,332
Time3   : 1,129  1,138  1,130  1,160

Time4a  : 0,690  0,682  0,617  0,635
Time4b  : 0,707  0,698  0,706  0,659
Time4c  : 0,679  0,685  0,626  0,625
Time5   : 0,715  0,682  0,686  0,679

Time6   : 0,490  0,485  0,522  0,514
Time7   : 0,323  0,333  0,336  0,318
Time8   : 0,407  0,403  0,373  0,354
Time9   : 0,352  0,378  0,355  0,355
Time10a : 1,823  1,812  1,807  1,813
Time10b : 1,113  1,120  1,115  1,118
Time10c : 0,652  0,630  0,653  0,633
Time10d : 0,156  0,155  0,172  0,160  <-- current winner!

、、、の結果が表示される場合がありますが、これら4aは互いに非常に近いものです。何故ですか?4aから削除したので、4b(4cにはまだありません)の2つの命令:と。コード内の他の場所ではeaxの下の値を使用しないことがわかっているので、事前に予約する必要はありません。これで、私のコードには、ルーチン5のようにプッシュ/ポップのペアが1つしかありません。ルーチン5は、最初にecxでコピーを作成するため、eaxの値を事前予約しますが、ecxを事前予約しません。4b4c5push eaxpop eax

したがって、私の結論は次のとおりです。5と4aと4bの実行時間の違い(3回目の編集前)はデータの依存関係には関係しませんでしたが、プッシュ/ポップ命令の追加のペアによって引き起こされました

私はあなたのコメントに非常に興味があります。

数日後、GJはPhiSよりもさらに高速なルーチン(Time 10d)を発明しました。いい仕事GJ!

4

13 に答える 13

16

一般に、私は個人的に、アセンブラーレベルでトリックを使用してコードを最適化しようとはしません。ただし、実際に2〜3%の速度が必要であり、読みにくいコードの価格を支払う意思がある場合を除きます。 、保守および移植。

その最後の1%を圧迫するには、プロセッサごとに最適化されたいくつかのバージョンを維持する必要があるかもしれません。新しいプロセッサと改良されたパスカルコンパイラが登場した場合、その恩恵を受けることはできません。

このDelphiコードは、最速のアセンブラコードよりも高速です。

procedure DecodePixels5(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels[0] := (EncPixels shr 0) and $01;
  DecPixels[1] := (EncPixels shr 1) and $01;
  DecPixels[2] := (EncPixels shr 2) and $01;
  DecPixels[3] := (EncPixels shr 3) and $01;
  DecPixels[4] := (EncPixels shr 4) and $01;
  DecPixels[5] := (EncPixels shr 5) and $01;
  DecPixels[6] := (EncPixels shr 6) and $01;
  DecPixels[7] := (EncPixels shr 7) and $01;
end;


Results:

Time1  : 1,03096806151283 ms.    <- Delphi loop.
Time2  : 0,740308641141395 ms.   <- Delphi unrolled loop.
Time3  : 0,996602425688886 ms.   <- BASM loop.
Time4a : 0,608267951561275 ms.   <- BASM unrolled loop.
Time4b : 0,574162510648039 ms.   <- BASM unrolled loop instruction switch.
Time5  : 0,499628206138524 ms. !!!  <- Delphi unrolled loop 5.

メモリを保存およびフェッチする必要がなく、レジスタのみで操作を実行できるため、高速です。最近のプロセッサは、連続する命令の結果が互いに独立しているため、これを部分的に並行して実行します(前の操作が終了する前に新しい操作を開始できます)。

マシンコードは次のようになります。

  push ebx;
  // DecPixels[0] := (EncPixels shr 0) and 1;
  movzx ecx,al
  mov ebx,ecx
  //  shr ebx,$00
  and bl,$01
  mov [edx],bl
  // DecPixels[1] := (EncPixels shr 1) and 1;
  mov ebx,ecx
  shr ebx,1
  and bl,$01
  mov [edx+$01],bl
  // DecPixels[2] := (EncPixels shr 2) and 1;
  mov ebx,ecx
  shr ebx,$02
  and bl,$01
  mov [edx+$02],bl
  // DecPixels[3] := (EncPixels shr 3) and 1;
  mov ebx,ecx
  shr ebx,$03
  and bl,$01
  mov [edx+$03],bl
  // DecPixels[4] := (EncPixels shr 4) and 1;
  mov ebx,ecx
  shr ebx,$04
  and bl,$01
  mov [edx+$04],bl
  // DecPixels[5] := (EncPixels shr 5) and 1;
  mov ebx,ecx
  shr ebx,$05
  and bl,$01
  mov [edx+$05],bl
  // DecPixels[6] := (EncPixels shr 6) and 1;
  mov ebx,ecx
  shr ebx,$06
  and bl,$01
  mov [edx+$06],bl
  // DecPixels[7] := (EncPixels shr 7) and 1;
  shr ecx,$07
  and cl,$01
  mov [edx+$07],cl
  pop ebx;

編集:提案されているように、テーブルルックアップは確かに高速です。

var
  PixelLookup:Array[byte] of TDecodedPixels;

// You could precalculate, but the performance gain would hardly be worth it because you call this once only.
for I := 0 to 255 do
  DecodePixels5b(I, PixelLookup[I]);


procedure DecodePixels7(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels := PixelLookup[EncPixels];
end;

Results:

Time1  : 1,03096806151283 ms.    <- Delphi loop.
Time2  : 0,740308641141395 ms.   <- Delphi unrolled loop.
Time3  : 0,996602425688886 ms.   <- BASM loop.
Time4a : 0,608267951561275 ms.   <- BASM unrolled loop.
Time4b : 0,574162510648039 ms.   <- BASM unrolled loop instruction switch.
Time5  : 0,499628206138524 ms. !!!  <- Delphi unrolled loop 5.
Time7 : 0,251533475182096 ms.    <- simple table lookup
于 2009-09-12T12:34:58.637 に答える
6

スタックエンド書き込みをメモリに8回使用するため、asmコードの相対性は遅くなります。これをチェックしてください...

procedure DecodePixels(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  xor   ecx, ecx
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 1
  mov   [DecPixels + 4], ecx
  xor   ecx, ecx
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 1
  mov   [DecPixels], ecx
end;

たぶん、ルックアップテーブルを使用したコードよりもさらに高速です!

改善されたバージョン:

procedure DecodePixelsI(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  mov   ecx, 0    //Faster than: xor   ecx, ecx
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  ror   ecx, 16
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  mov   [DecPixels + 4], ecx
  mov   ecx, 0    //Faster than: xor   ecx, ecx
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  ror   ecx, 16
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  mov   [DecPixels], ecx
end;

バージョン3:

procedure DecodePixelsX(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  add   al, al
  setc  byte ptr[DecPixels + 7]
  add   al, al
  setc  byte ptr[DecPixels + 6]
  add   al, al
  setc  byte ptr[DecPixels + 5]
  add   al, al
  setc  byte ptr[DecPixels + 4]
  add   al, al
  setc  byte ptr[DecPixels + 3]
  add   al, al
  setc  byte ptr[DecPixels + 2]
  add   al, al
  setc  byte ptr[DecPixels + 1]
  setnz byte ptr[DecPixels]
end;

バージョン4:

const Uint32DecPix : array [0..15] of cardinal = (
  $00000000, $00000001, $00000100, $00000101,
  $00010000, $00010001, $00010100, $00010101,
  $01000000, $01000001, $01000100, $01000101,
  $01010000, $01010001, $01010100, $01010101
  );

procedure DecodePixelsY(EncPixels: byte; var DecPixels: TDecodedPixels); inline;
begin
  pcardinal(@DecPixels)^ := Uint32DecPix[EncPixels and $0F];
  pcardinal(cardinal(@DecPixels) + 4)^ := Uint32DecPix[(EncPixels and $F0) shr 4];
end;
于 2009-09-14T19:04:10.253 に答える
5

Nick Dの答えを拡張して、次のテーブルルックアップベースのバージョンを試しました。これらはすべて、提供する実装よりも高速です(Wouter van Nifterickのコードよりも高速です)。

次のパック配列があるとします。


      const Uint64DecPix : PACKED ARRAY [0..255] OF UINT64 =
  ( $0000000000000000, $0000000000000001, $0000000000000100, $0000000000000101, $0000000000010000, $0000000000010001, $0000000000010100, $0000000000010101, $0000000001000000, $0000000001000001, $0000000001000100, $0000000001000101, $0000000001010000, $0000000001010001, $0000000001010100, $0000000001010101,
    $0000000100000000, $0000000100000001, $0000000100000100, $0000000100000101, $0000000100010000, $0000000100010001, $0000000100010100, $0000000100010101, $0000000101000000, $0000000101000001, $0000000101000100, $0000000101000101, $0000000101010000, $0000000101010001, $0000000101010100, $0000000101010101,
    $0000010000000000, $0000010000000001, $0000010000000100, $0000010000000101, $0000010000010000, $0000010000010001, $0000010000010100, $0000010000010101, $0000010001000000, $0000010001000001, $0000010001000100, $0000010001000101, $0000010001010000, $0000010001010001, $0000010001010100, $0000010001010101,
    $0000010100000000, $0000010100000001, $0000010100000100, $0000010100000101, $0000010100010000, $0000010100010001, $0000010100010100, $0000010100010101, $0000010101000000, $0000010101000001, $0000010101000100, $0000010101000101, $0000010101010000, $0000010101010001, $0000010101010100, $0000010101010101,
    $0001000000000000, $0001000000000001, $0001000000000100, $0001000000000101, $0001000000010000, $0001000000010001, $0001000000010100, $0001000000010101, $0001000001000000, $0001000001000001, $0001000001000100, $0001000001000101, $0001000001010000, $0001000001010001, $0001000001010100, $0001000001010101,
    $0001000100000000, $0001000100000001, $0001000100000100, $0001000100000101, $0001000100010000, $0001000100010001, $0001000100010100, $0001000100010101, $0001000101000000, $0001000101000001, $0001000101000100, $0001000101000101, $0001000101010000, $0001000101010001, $0001000101010100, $0001000101010101,
    $0001010000000000, $0001010000000001, $0001010000000100, $0001010000000101, $0001010000010000, $0001010000010001, $0001010000010100, $0001010000010101, $0001010001000000, $0001010001000001, $0001010001000100, $0001010001000101, $0001010001010000, $0001010001010001, $0001010001010100, $0001010001010101,
    $0001010100000000, $0001010100000001, $0001010100000100, $0001010100000101, $0001010100010000, $0001010100010001, $0001010100010100, $0001010100010101, $0001010101000000, $0001010101000001, $0001010101000100, $0001010101000101, $0001010101010000, $0001010101010001, $0001010101010100, $0001010101010101,
    $0100000000000000, $0100000000000001, $0100000000000100, $0100000000000101, $0100000000010000, $0100000000010001, $0100000000010100, $0100000000010101, $0100000001000000, $0100000001000001, $0100000001000100, $0100000001000101, $0100000001010000, $0100000001010001, $0100000001010100, $0100000001010101,
    $0100000100000000, $0100000100000001, $0100000100000100, $0100000100000101, $0100000100010000, $0100000100010001, $0100000100010100, $0100000100010101, $0100000101000000, $0100000101000001, $0100000101000100, $0100000101000101, $0100000101010000, $0100000101010001, $0100000101010100, $0100000101010101,
    $0100010000000000, $0100010000000001, $0100010000000100, $0100010000000101, $0100010000010000, $0100010000010001, $0100010000010100, $0100010000010101, $0100010001000000, $0100010001000001, $0100010001000100, $0100010001000101, $0100010001010000, $0100010001010001, $0100010001010100, $0100010001010101,
    $0100010100000000, $0100010100000001, $0100010100000100, $0100010100000101, $0100010100010000, $0100010100010001, $0100010100010100, $0100010100010101, $0100010101000000, $0100010101000001, $0100010101000100, $0100010101000101, $0100010101010000, $0100010101010001, $0100010101010100, $0100010101010101,
    $0101000000000000, $0101000000000001, $0101000000000100, $0101000000000101, $0101000000010000, $0101000000010001, $0101000000010100, $0101000000010101, $0101000001000000, $0101000001000001, $0101000001000100, $0101000001000101, $0101000001010000, $0101000001010001, $0101000001010100, $0101000001010101,
    $0101000100000000, $0101000100000001, $0101000100000100, $0101000100000101, $0101000100010000, $0101000100010001, $0101000100010100, $0101000100010101, $0101000101000000, $0101000101000001, $0101000101000100, $0101000101000101, $0101000101010000, $0101000101010001, $0101000101010100, $0101000101010101,
    $0101010000000000, $0101010000000001, $0101010000000100, $0101010000000101, $0101010000010000, $0101010000010001, $0101010000010100, $0101010000010101, $0101010001000000, $0101010001000001, $0101010001000100, $0101010001000101, $0101010001010000, $0101010001010001, $0101010001010100, $0101010001010101,
    $0101010100000000, $0101010100000001, $0101010100000100, $0101010100000101, $0101010100010000, $0101010100010001, $0101010100010100, $0101010100010101, $0101010101000000, $0101010101000001, $0101010101000100, $0101010101000101, $0101010101010000, $0101010101010001, $0101010101010100, $0101010101010101);
PUint64DecPix : pointer = @Uint64DecPix;

あなたは次のように書くことができます:


procedure DecodePixelsPS1Pas (EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels := TDecodedPixels(Uint64DecPix[EncPixels]);
end;

procedure DecodePixelsPS1PasInline (EncPixels: Byte; var DecPixels: TDecodedPixels); inline; begin DecPixels := TDecodedPixels(Uint64DecPix[EncPixels]); end;

procedure DecodePixelsPS1Asm (EncPixels: Byte; var DecPixels: TDecodedPixels); asm lea ecx, Uint64DecPix //[<-Added in EDIT 3] //mov ecx, dword ptr PUint64DecPix - alternative to the above line (slower for me) movzx eax, al movq xmm0, [8*eax+ecx] //Using XMM rather than MMX so we don't have to issue emms at the end movq [edx], xmm0 //use MOVQ because it doesn't need mem alignment end;

標準のPASとASMの実装は速度的にはかなり似ていますが、「INLINE」とマークされたPASの実装は、ルーチンの呼び出しに関連するすべての呼び出し/再実行を取り除くため、最速です。

--EDIT--:言い忘れました:TDecodedPixels構造のメモリレイアウトについて暗黙のうちに何かを想定しているので、次のように宣言した方がよいでしょう。


PACKED ARRAY [0..7] of byte

--EDIT2--:比較のための私の結果は次のとおりです。


Time1 : 2.51638266874701 ms.    <- Delphi loop.
Time2 : 2.11277620479698 ms.    <- Delphi unrolled loop.
Time3 : 2.21972066282167 ms.    <- BASM loop.
Time4a : 1.34093090043567 ms.    <- BASM unrolled loop.
Time4b : 1.52222070123437 ms.    <- BASM unrolled loop instruction switch.
Time5 : 1.17106364076999 ms.    <- Wouter van Nifterick
TimePS1 : 0.633099318488802 ms.    <- PS.Pas
TimePS2 : 0.551617593856202 ms.    <- PS.Pas Inline
TimePS3 : 0.70921094720139 ms.    <- PS.Asm (speed for version before 3rd EDIT)

于 2009-09-12T13:02:24.397 に答える
4

コンパイラーは、小さなルーチンを最適化するのに非常に優れています。

ルックアップテーブルを使用してコードを最適化します。
1バイト(256の異なる状態)をデコードするため、解凍された値を使用して256の配列を事前に計算できます。

編集: Pentiumプロセッサは特定の命令を並列に実行できることに注意してください(スーパースカラーアーキテクチャ)。これはペアリングと呼ばれます。

于 2009-09-12T11:45:21.107 に答える
4

純粋なソフトウェアソリューション

この質問からインスピレーションを得たこの質問の美しい手法を使用すると、 1行のコード(宣言を除く)だけでこのような優れたソリューションが得られます。

type TPackedDecodedPixels = record
case integer of
  0: (a: TDecodedPixels);
  1: (v: Int64);
end;

procedure DecodePixels(EncPixels: byte; var DecPixels: TDecodedPixels); inline;
const
  magic = $8040201008040201;
  mask  = $8080808080808080;
begin
  TPackedDecodedPixels(DecPixels).v := SwapEndian(((EncPixels*magic) and mask) shr 7);
end;

もちろん、それDecPixelsが適切に8バイトにアラインされていることを確認する必要があります。そうしないと、速度が低下する可能性があります(または他のアーキテクチャでのセグフォールトにさえ苦しむ可能性があります)。関数を簡単にベクトル化して高速化することもできます

説明

次のビットパターンがあると仮定しますabcdefgh。出力配列に含まれるようにします

0000000a 0000000b 0000000c 0000000d 0000000e 0000000f 0000000g 0000000h (1)

これをリトルエンディアンで64ビット整数として読み取ると、が得られます%0000000h0000000g0000000f0000000e0000000d0000000c0000000b0000000a。元のビットを必要なビットを抽出できる位置にシフトするマジックナンバーを見つける必要があります

値に魔法の数を掛けましょう

  |  b7  ||  b6  ||  b4  ||  b4  ||  b3  ||  b2  ||  b1  ||  b0  |
                                                          abcdefgh (1-byte value)
x 1000000001000000001000000001000000001000000001000000001000000001
  ────────────────────────────────────────────────────────────────
= h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh

この時点で、すべてのピクセルのビットが対応するバイトの最上位ビットに移動されています。彼らはすでに正しい場所に嘘をついているので、残りのビットを取り除く必要がありますand

  |  b7  ||  b6  ||  b4  ||  b4  ||  b3  ||  b2  ||  b1  ||  b0  |
  h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh
& 1000000010000000100000001000000010000000100000001000000010000000
  ────────────────────────────────────────────────────────────────
= h0000000g0000000f0000000e0000000d0000000c0000000b0000000a0000000 (8-byte array)

これで、ピクセルのビットは対応するバイトの最上位ビットになります。論理的に右に7シフトして、ピクセルを最下位の位置に移動する必要があります。OPは逆の順序で値を必要SwapEndian()とするため、バイトをビッグエンディアンに変換する必要があります。リトルエンディアンが必要な場合は、このステップで停止できます

つまり、魔法の数は%1000000001000000001000000001000000001000000001000000001000000001 = $8040201008040201であり、マスクは%1000000010000000100000001000000010000000100000001000000010000000 = $8080808080808080です。もちろん、実際には、問題を解決してそれらの値を取得するには、最終結果→乗算結果→マジックナンバーから逆方向に実行する必要があります。


しかし、なぜバイトを(1)のリトルエンディアンに入れてから、ビッグエンディアンに変換し直さなければならないのでしょうか。バイトをビッグエンディアンの順序で並べて、そのマジックナンバーを見つけてみませんか?あなたがそれについて疑問に思っているなら、それはそれが一度に最大7ビットでしか機能しないからです。私は以前の答えでそのようにしたので、少し分割して後で組み合わせる必要があります

                                                          0abcdefg
x 0000000000000010000001000000100000010000001000000100000010000001
  ────────────────────────────────────────────────────────────────
= 00000000abcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefg
& 0000000000000001000000010000000100000001000000010000000100000001
  ────────────────────────────────────────────────────────────────    
= 000000000000000a0000000b0000000c0000000d0000000e0000000f0000000g

ハードウェアサポート

これは実際には、定数マスクを使用したビット拡張の特殊なケースです。AVX2では、Intelはその目的のためにBMI2命令セットにpdep命令を導入したため、結果を得るには1つの命令が必要です。他の言語では、これを組み込み関数で使用できます_pext_u64。残念ながら、AFAIK Free Pascalはそれをサポートしておらず、アセンブリを直接使用する必要があります。ただし、式は次のようになります

TPackedDecodedPixels(DecPixels).v := _pext_u64(EncPixels, $0101010101010101);

正しさチェック

OPのバージョンを両方のバージョンと比較してみましたが、今まで問題は見つかりませんでした。コンパイラの出力は次のようになります

mov al, dil
mov rbx, rsi
movzx edi, al
movabs rax, 0x8040201008040201
imul rdi, rax
movabs rax, 0x8080808080808080
and rdi, rax
shr rdi, 0x7
call 4016a0 <SYSTEM_$$_SWAPENDIAN$INT64$$INT64>
mov QWORD PTR [rbx], rax

SwapEndianFPC出力は、コンパイラがへの呼び出しをに置き換えることを認識しておらずBSWAP、データを不必要にコピーするため、依然としてかなり最適ではありません。なぜmov al, dil; movzx edi, alだけではなくmovzx edi, dil?ご覧のとおり、CおよびC++コンパイラからの出力ははるかに優れています

8つのブール値からバイトを作成する方法(およびその逆)を参照してください。

于 2014-05-26T17:59:40.503 に答える
3

WoutervanNifterickと同じアルゴリズムを提供しようとしていました。

さらに、依存関係チェーンの観点からパフォーマンスが向上することを説明します。提案した各バージョンでは、基本ループを展開したときに、2つの連続する反復間の依存関係を維持しました。それぞれのバージョンでは、shr al, $01;alの前の値が計算されている必要があります。展開されたイテレーションを並行して実行できるように編成すると、実際には最新のプロセッサ上で実行されます。レジスタの名前変更によって抑制できる誤った依存関係にだまされないでください。

Pentiumは一度に2つの命令を実行できると誰かが指摘しました。それは本当ですが、最新のプロセッサ(Pentium Pro、PII、...、Core、Core 2以降)は、チャンスがあるとき、つまり依存関係がないときに、同時に2つ以上の命令を実行しています。実行中の命令間。Wouter van Nifterickのバージョンでは、各行を他の行から独立して実行できることに注意してください。

http://www.agner.org/optimize/には、最新のプロセッサのアーキテクチャとそれらを活用する方法を理解するために必要となる可能性のあるすべての情報が含まれています。

于 2009-09-12T12:54:58.647 に答える
2

80386以降のみをサポートしている場合は、BTccおよびSETccの一連の命令を次のように使用できます。

BT ax,1
SETC [dx]
inc dx

BT ax,2
SETC [dx]
inc dx

于 2009-09-12T12:31:55.103 に答える
2

次のようなものはどうですか?

/* input byte in eax, address to store result in edx */
and eax, 0xff    /* may not be needed */
mov ebx, eax
shl ebx, 7
or  eax, ebx
mov ebx, eax
shl ebx, 14
or  eax, ebx
mov ebx, eax
and eax, 0x01010101
mov [edx], eax
shr ebx, 4
and ebx, 0x01010101
mov [edx+4], ebx
于 2009-09-15T22:15:30.297 に答える
1

4bが4aよりも高速であると考えられる理由は、並列化が優れているためです。4aから:

mov bl, al;
and bl, $01;          // data dep (bl)
mov  [edx], bl;       // data dep (bl)
shr al, $01;
mov bl, al;           // data dep (al)
and bl, $01;          // data dep (bl)
mov [edx + $01], bl;  // data dep (bl)

「datadep」とマークされた命令は、前の命令が終了するまで実行を開始できません。このデータ依存性を引き起こすレジスターを作成しました。最新のCPUは、依存関係がない場合、最後のCPUが完了する前に命令を開始できます。しかし、これらの操作を注文した方法はこれを防ぎます。

4bでは、データの依存関係が少なくなります。

mov bl, al;
and bl, $01;          // data dep (bl)
shr al, $01;
mov [edx], bl;
mov bl, al;
and bl, $01;          // data dep (bl)
shr al, $01;
mov [edx + $01], bl;

この命令の順序付けでは、前の命令に依存する命令が少なくなるため、並列処理の機会が増えます。

これが速度差の理由であることを保証することはできませんが、それはおそらく候補です。残念ながら、あなたが探しているものほど絶対的な答えに出くわすことは困難です。最新のプロセッサには、分岐予測、マルチレベルキャッシュ、ハードウェアプリフェッチャー、およびパフォーマンスの違いの理由を特定するのが困難になる可能性のあるその他のあらゆる種類の複雑さがあります。あなたができる最善のことは、たくさん読んで、実験を行い、そして良い測定をするためのツールに精通することです。

于 2009-09-12T12:16:38.563 に答える
0

メモリ(実際にはキャッシュメモリ)への書き込みは、レジスタを操作するよりも遅いと思います。

それで、

mov [edx+...], bl
shr al, $01;
mov bl, al;

レジスタが再び必要blになる前に、プロセッサにメモリへの書き込み時間を与えます。bl

shr al, $01;
mov [edx], bl;
mov bl, al;

すぐに必要blになるため、プロセッサは停止し、メモリの書き込みが完了するのを待つ必要があります。

これは私にとって驚くべきことです。最近のIntelプロセッサは、クレイジーなパイプライン処理とレジスタの名前変更を行っているので、私の意見では、各命令の依存関係がさらに後退しているため、DecodePixels4bの方が高速であるはずです。上記は、これを除いて、私が提供できるすべての説明です:

x86はひどい命令セットであり、Intelはそれを効率的にするために驚くべき非常に高度なhocus-pocusを実行します。もし私があなたなら、私は何か他のものを調べます。今日、PC用のmegaMcOptimizedソフトウェアの需要はほとんどありません。私の友好的な提案は、モバイルデバイス(主にARM)用のプロセッサを調べることです。モバイルデバイスでは、プロセッサの速度、消費電力、およびバッテリ寿命の懸念から、マイクロ最適化ソフトウェアの方が重要です。また、ARMにはx86に設定された優れた命令があります。

于 2009-09-12T12:13:14.947 に答える
0

SIMD

アルゴリズムを処理アレイに拡張すると、SIMDが最適化オプションになります。これは、最適化されたCの同等の時間の1/3のSIMDバージョンです。

int main ()
{
  const int
    size = 0x100000;

  unsigned char
    *source = new unsigned char [size],
    *dest,
    *dest1 = new unsigned char [size * 32],
    *dest2 = new unsigned char [size * 32];

  for (int i = 0 ; i < size ; ++i)
  {
    source [i] = rand () & 0xff;
  }

  LARGE_INTEGER
    start,
    middle,
    end;

  QueryPerformanceCounter (&start);
  dest = dest1;
  for (int i = 0 ; i < size ; ++i)
  {
    unsigned char
      v = source [i];

    for (int b = 0 ; b < 8 ; ++b)
    {
      *(dest++) = (v >> b) & 1;
    }
  }
  unsigned char
    bits [] = {1,2,4,8,16,32,64,128,1,2,4,8,16,32,64,128},
    zero [] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    ones [] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};

  QueryPerformanceCounter (&middle);
  __asm
  {
    movdqu xmm1,bits
    movdqu xmm2,zero
    movdqu xmm3,ones
    mov ecx,0x100000/4
    mov esi,source
    mov edi,dest2
l1:
    lodsd
    movd xmm0,eax
    movd xmm4,eax
    punpcklbw xmm0,xmm0
    punpcklbw xmm4,xmm4
    punpcklwd xmm0,xmm0
    punpcklwd xmm4,xmm4
    punpckldq xmm0,xmm0
    punpckhdq xmm4,xmm4
    pand xmm0,xmm1
    pand xmm4,xmm1
    pcmpeqb xmm0,xmm2
    pcmpeqb xmm4,xmm2
    paddb xmm0,xmm3
    paddb xmm4,xmm3
    movdqu [edi],xmm0
    movdqu [edi+16],xmm4
    add edi,32
    dec ecx
    jnz l1
  }
  QueryPerformanceCounter (&end);

  cout << "Time taken = " << (middle.QuadPart - start.QuadPart) << endl;
  cout << "Time taken = " << (end.QuadPart - middle.QuadPart) << endl;
  cout << "memcmp = " << memcmp (dest1, dest2, size * 32) << endl;

  return 0;
}
于 2009-09-15T14:44:31.030 に答える
-1

お気づきのように、4aと4bの実装における速度の違いは、CPUの最適化(複数の命令を並列/パイプライン命令で実行することによる)によるものです。しかし、その要因はオペランドではなく、演算子自体の性質によるものです。

4a Instruction Sequence:
AND - MOV - SHR

4b Instruction Sequence:
AND - SHR - MOV

ANDとSHRはどちらもフラグレジスタを使用するため、これら2つの命令のパイプラインには待機状態があります。

次のように読んでください。

4a: AND (piped) MOV (piped) SHR
4b: AND (WAIT) SHR (piped) MOV

結論:4bのパイプラインには4aよりも7多い待機状態があるため、速度が遅くなります。

Joshは、データの依存関係があると述べました。

mov bl, al;
and bl, $01;          // data dep (bl)

ただし、これら2つの命令はCPUレベルで部分的に並列で実行できるため、完全に正しいわけではありません。

mov bl, al -> (A:) read al (B:) write bl  => (2 clocks in i386)
and bl, 01 -> (C:) read 01 (D:) write bl  => idem

順次4クロックかかりますが、パイプラインでは3つの「クロック」しかかかりません(実際、「クロック」という用語はパイプラインの観点からは適切ではありませんが、単純化の観点から使用しました)

[--A--][--B--]
 [--C--]<wait>[---D--]
于 2012-03-01T07:33:02.883 に答える
-1

信じられないほどスマートなソリューションChris、逆問題をどうしますか:8バイトの配列から1バイトを作成しますか?

逆問題の最適化されていないソリューション:

BtBld PROC Array:DWORD, Pixels:DWORD
  mov  eax, [Array]
  add  eax, 7
  mov  edx, [Pixels]

  mov  bx, 0

  mov  ecx, 8
rpt:  or  bx, [eax]
  dec  eax
  shl  bx, 1
  loop rpt
  shr  bx, 1
  mov  [edx], bl
  ret
BtBld ENDP
于 2009-10-02T10:35:59.797 に答える