3

関数のパフォーマンスを改善しようとしています。プロファイラーは、内部ループのコードを指します。おそらくSSE組み込み関数を使用して、そのコードのパフォーマンスを向上させることはできますか?

void ConvertImageFrom_R16_FLOAT_To_R32_FLOAT(char* buffer, void* convertedData, DWORD width, DWORD height, UINT rowPitch)
{
    struct SINGLE_FLOAT
    {
        union {
            struct {
                unsigned __int32 R_m : 23;
                unsigned __int32 R_e : 8;
                unsigned __int32 R_s : 1;
            };
            struct {
                float r;
            };
        };
    };
    C_ASSERT(sizeof(SINGLE_FLOAT) == 4); // 4 bytes
    struct HALF_FLOAT
    {
        unsigned __int16 R_m : 10;
        unsigned __int16 R_e : 5;
        unsigned __int16 R_s : 1;
    };
    C_ASSERT(sizeof(HALF_FLOAT) == 2);
    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
    for(DWORD j = 0; j< height; j++)
    {
        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
        for(DWORD i = 0; i< width; i++)
        {
            d->R_s = s->R_s;
            d->R_e = s->R_e - 15 + 127;
            d->R_m = s->R_m << (23-10);
            d++;
            s++;
        }
    }
}

アップデート:

分解

; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01 

    TITLE   Utils.cpp
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
; Function compile flags: /Ogtp
;   COMDAT ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z
_TEXT   SEGMENT
_buffer$ = 8                        ; size = 4
tv83 = 12                       ; size = 4
_convertedData$ = 12                    ; size = 4
_width$ = 16                        ; size = 4
_height$ = 20                       ; size = 4
_rowPitch$ = 24                     ; size = 4
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z PROC ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT, COMDAT

; 323  : {

    push    ebp
    mov ebp, esp

; 343  :    for(DWORD j = 0; j< height; j++)

    mov eax, DWORD PTR _height$[ebp]
    push    esi
    mov esi, DWORD PTR _convertedData$[ebp]
    test    eax, eax
    je  SHORT $LN4@ConvertIma

; 324  :    union SINGLE_FLOAT {
; 325  :        struct {
; 326  :            unsigned __int32 R_m : 23;
; 327  :            unsigned __int32 R_e : 8;
; 328  :            unsigned __int32 R_s : 1;
; 329  :        };
; 330  :        struct {
; 331  :            float r;
; 332  :        };
; 333  :    };
; 334  :    C_ASSERT(sizeof(SINGLE_FLOAT) == 4);
; 335  :    struct HALF_FLOAT
; 336  :    {
; 337  :        unsigned __int16 R_m : 10;
; 338  :        unsigned __int16 R_e : 5;
; 339  :        unsigned __int16 R_s : 1;
; 340  :    };
; 341  :    C_ASSERT(sizeof(HALF_FLOAT) == 2);
; 342  :    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;

    push    ebx
    mov ebx, DWORD PTR _buffer$[ebp]
    push    edi
    mov DWORD PTR tv83[ebp], eax
$LL13@ConvertIma:

; 344  :    {
; 345  :        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
; 346  :        for(DWORD i = 0; i< width; i++)

    mov edi, DWORD PTR _width$[ebp]
    mov edx, ebx
    test    edi, edi
    je  SHORT $LN5@ConvertIma
    npad    1
$LL3@ConvertIma:

; 347  :        {
; 348  :            d->R_s = s->R_s;

    movzx   ecx, WORD PTR [edx]
    movzx   eax, WORD PTR [edx]
    shl ecx, 16                 ; 00000010H
    xor ecx, DWORD PTR [esi]
    shl eax, 16                 ; 00000010H
    and ecx, 2147483647             ; 7fffffffH
    xor ecx, eax
    mov DWORD PTR [esi], ecx

; 349  :            d->R_e = s->R_e - 15 + 127;

    movzx   eax, WORD PTR [edx]
    shr eax, 10                 ; 0000000aH
    and eax, 31                 ; 0000001fH
    add eax, 112                ; 00000070H
    shl eax, 23                 ; 00000017H
    xor eax, ecx
    and eax, 2139095040             ; 7f800000H
    xor eax, ecx
    mov DWORD PTR [esi], eax

; 350  :            d->R_m = s->R_m << (23-10);

    movzx   ecx, WORD PTR [edx]
    and ecx, 1023               ; 000003ffH
    shl ecx, 13                 ; 0000000dH
    and eax, -8388608               ; ff800000H
    or  ecx, eax
    mov DWORD PTR [esi], ecx

; 351  :            d++;

    add esi, 4

; 352  :            s++;

    add edx, 2
    dec edi
    jne SHORT $LL3@ConvertIma
$LN5@ConvertIma:

; 343  :    for(DWORD j = 0; j< height; j++)

    add ebx, DWORD PTR _rowPitch$[ebp]
    dec DWORD PTR tv83[ebp]
    jne SHORT $LL13@ConvertIma
    pop edi
    pop ebx
$LN4@ConvertIma:
    pop esi

; 353  :        }
; 354  :    }
; 355  : }

    pop ebp
    ret 0
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ENDP ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
_TEXT   ENDS
4

10 に答える 10

3

x86 F16C命令セット拡張は、単精度floatベクトルを半精度floatのベクトルとの間で変換するためのハードウェアサポートを追加します。

形式は、説明したのと同じIEEE754半精度binary16です。エンディアンが構造体と同じであることを確認しませんでしたが、必要に応じて(を使用してpshufb)簡単に修正できます。

F16Cは、IntelIvyBridgeおよびAMDPiledriverからサポートされています。(そして、コードがチェックする必要がある独自のCPUID機能ビットを持っています。そうでない場合は、SIMD整数シフトとシャッフルにフォールバックします)。

VCVTPS2PHの本質は次のとおりです。

__m128i _mm_cvtps_ph ( __m128 m1, const int imm);
__m128i _mm256_cvtps_ph(__m256 m1, const int imm);

即値バイトは丸め制御です。コンパイラは、これをメモリへの直接の変換および格納として使用できます(オプションでメモリオペランドを使用できるほとんどの命令とは異なり、レジスタの代わりにメモリを使用できるソースオペランドです)。


VCVTPH2PSは逆方向に進み、他のほとんどのSSE命令とまったく同じです(レジスタ間またはロードとして使用できます)。

__m128 _mm_cvtph_ps ( __m128i m1);
__m256 _mm256_cvtph_ps ( __m128i m1)

F16Cは非常に効率的であるため、半精度形式で画像を残し、そこからのデータのベクトルが必要になるたびにオンザフライで変換することを検討することをお勧めします。これは、キャッシュのフットプリントに最適です。

于 2016-10-23T04:20:24.510 に答える
2

ここにいくつかのアイデアがあります:

定数をconst register変数に入れます。

一部のプロセッサは、メモリから定数をフェッチすることを好みません。それは厄介で、多くの命令サイクルを要するかもしれません。

ループ展開

ループ内のステートメントを繰り返し、増分を増やします。
プロセッサは連続命令を好みます。ジャンプと枝は彼らを怒らせます。

データのプリフェッチ(またはキャッシュのロード)

volatileループでより多くの変数を使用し、コンパイラーがそれらを最適化しないように それらを宣言します。

SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
SINGLE_FLOAT* d1 = d + 1;
SINGLE_FLOAT* d2 = d + 2;
SINGLE_FLOAT* d3 = d + 3;
for(DWORD j = 0; j< height; j++)
{
    HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
    HALF_FLOAT* s1 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 1));
    HALF_FLOAT* s2 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 2));
    HALF_FLOAT* s3 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 3));
    for(DWORD i = 0; i< width; i += 4)
    {
        d->R_s = s->R_s;
        d->R_e = s->R_e - 15 + 127;
        d->R_m = s->R_m << (23-10);
        d1->R_s = s1->R_s;
        d1->R_e = s1->R_e - 15 + 127;
        d1->R_m = s1->R_m << (23-10);
        d2->R_s = s2->R_s;
        d2->R_e = s2->R_e - 15 + 127;
        d2->R_m = s2->R_m << (23-10);
        d3->R_s = s3->R_s;
        d3->R_e = s3->R_e - 15 + 127;
        d3->R_m = s3->R_m << (23-10);
        d += 4;
        d1 += 4;
        d2 += 4;
        d3 += 4;
        s += 4;
        s1 += 4;
        s2 += 4;
        s3 += 4;
    }
}
于 2011-04-01T17:28:35.697 に答える
2

もちろん、アーキテクチャによっては、メモリ内のビットフィールドにアクセスするのは非常に難しい場合があります。

floatと32ビット整数の和集合を作成し、ローカル変数を使用してすべての分解と合成を実行すると、パフォーマンスが向上する可能性があります。このようにして、生成されたコードは、プロセッサレジスタのみを使用して操作全体を実行できます。

于 2011-04-01T15:42:14.627 に答える
1

ループは互いに独立しているため、SIMDまたはOpenMPを使用してこのコードを簡単に並列化できます。単純なバージョンでは、画像の上半分と下半分を2つのスレッドに分割し、同時に実行します。

于 2011-04-01T15:30:02.490 に答える
1

SSE組み込み関数は優れたアイデアのようです。あなたがその道を行く前に、あなたはすべきです

  • コンパイラによって生成されたアセンブリコードを見てください(最適化の可能性はありますか?)

  • SSEコードを自動的に生成する方法について、コンパイラのドキュメントを検索してください。

  • ソフトウェアライブラリのドキュメント(または16ビット浮動小数点型が発生した場所)で、この型を一括変換する関数を検索してください。(64ビット浮動小数点への変換も役立つ可能性があります。)この問題に最初に遭遇したのはあなたではない可能性が非常に高いです。

それがすべて失敗した場合は、SSE組み込み関数を使って運試しをしてみてください。アイデアを得るために、32ビットから16ビットの浮動小数点に変換するSSEコードを次に示します。(逆にしたい)

SSEに加えて、マルチスレッド化とタスクのGPUへのオフロードも検討する必要があります。

于 2011-04-01T15:51:10.157 に答える
1

データを2次元配列として処理しています。メモリ内でのレイアウトを検討すると、1次元配列として処理できる可能性があり、ネストされたループの代わりに1つのループを使用することで、オーバーヘッドを少し節約できます。

また、アセンブリコードにコンパイルして、コンパイラの最適化が機能し、何百回も再計算(15 + 127)されていないことを確認します。

于 2011-04-01T15:55:41.353 に答える
1

これを、次のCVT16命令セットを使用するチップ上の単一の命令に減らすことができるはずです。そのウィキペディアの記事によると:

The CVT16 instructions allow conversion of floating point vectors between single precision and half precision.

于 2011-04-01T15:59:29.667 に答える
0

SSE組み込み関数についてはわかりませんが、内部ループの分解を見るのは興味深いでしょう。昔ながらの方法(あまり役に立たないかもしれませんが、試してみるのは簡単です)は、2つの内部ループを実行することによって反復回数を減らすことです。1つは処理をN回(たとえば32回)繰り返します(ループカウントwidth / N)、次に1つで残り(width%Nのループカウント)を終了します...これらのdivとmodulosは、再計算を避けるために最初のループの外側で計算されます。それが明白に聞こえる場合はお詫びします!

于 2011-04-01T15:41:56.977 に答える
0

この関数は、いくつかの小さなことを実行しているだけです。最適化によって時間を大幅に短縮するのは難しいでしょうが、誰かがすでに言ったように、並列化には約束があります。

取得しているキャッシュミスの数を確認してください。データがページインおよびページアウトしている場合は、キャッシュスワップを最小限に抑えるために、順序付けにより多くのインテリジェンスを適用することで、データを高速化できる可能性があります。

マクロ最適化も検討してください。回避できる可能性のあるデータ計算の冗長性はありますか(たとえば、必要なときに古い結果を再計算する代わりにキャッシュする)?本当にデータセット全体を変換する必要がありますか、それとも必要なビットだけを変換できますか?私はあなたのアプリケーションを知らないので、ここで大げさに推測していますが、その種の最適化の余地があるかもしれません。

于 2011-04-01T16:07:31.730 に答える
0

私の疑惑は、この操作はすでにメモリアクセスでボトルネックになっており、より効率的にする(たとえば、SSEを使用する)と、実行が速くなることはないということです。しかし、これは単なる疑いです。

x86 / x64を想定した場合、他に試すべきことは次のとおりです。

  • とはしないでくださいd++。ただし、各反復でとをs++使用してください。(もちろん、各スキャンラインの後にバンプします。)の要素は4バイトであり、2の要素であるため、この操作はアドレス計算に組み込むことができます。(残念ながら、これが必ずしも実行をより効率的にすることを保証することはできません。)d[i]s[i]dds
  • ビットフィールド操作を削除し、手動で操作を実行します。(抽出するときは、最初にシフトし、次にマスクして、マスクが小さな即値に収まる可能性を最大にします。)
  • ループを展開しますが、これと同じくらい簡単に予測できるループでは、大きな違いはないかもしれません。
  • 各線に沿っwidthてゼロまで数えます。widthこれにより、コンパイラは毎回フェッチする必要がなくなります。x86はレジスタが非常に少ないため、おそらくより重要です。d[i](CPUが私の「」と「」の提案を気に入った場合は、s[i]幅に署名を付け、代わりにカウントしwidth-1て、後方に歩くことができます。)

これらはすべて、SSEに変換するよりも試行が速く、メモリにバインドされていない場合は、メモリにバインドされていることを願っています。その時点で、あきらめることができます。

最後に、出力が書き込み結合メモリにある場合(たとえば、テクスチャまたは頂点バッファ、AGP、PCI Express、または最近のPCにあるもの)を介してアクセスされる場合、パフォーマンスが低下する可能性があります。コンパイラが内部ループ用に生成したコード。したがって、その場合は、各スキャンラインをローカルバッファに変換し、それを使用memcpyして最終的な宛先にコピーすることで、より良い結果が得られる可能性があります。

于 2011-04-01T18:49:26.623 に答える