web-dev-qa-db-ja.com

最小3つの数字を見つける最も速い方法は?

私が書いたプログラムでは、このルーチンでは、時間の20%が内部ループ内の最小3つの数値を見つけることに費やされています。

static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
    unsigned int m = a;
    if (m > b) m = b;
    if (m > c) m = c;
    return m;
}

これをスピードアップする方法はありますか? x86/x86_64のアセンブリコードでも問題ありません。

編集:コメントのいくつかへの返信:
*使用されているコンパイラはgcc4.3.3です。
*議会に関する限り、私はそこの初心者にすぎません。これを行う方法を学ぶために、私はここでアセンブリを求めました。 :)
*クアッドコアIntel64を実行しているので、MMX/SSEなどがサポートされています。
*ここにループを投稿するのは難しいですが、レーベンシュタインアルゴリズムの高度に最適化された実装であると言えます。

これは、コンパイラがインライン化されていないバージョンのminに対して提供しているものです。

.globl min
    .type   min, @function
min:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    movl    12(%ebp), %eax
    movl    16(%ebp), %ecx
    cmpl    %edx, %eax
    jbe .L2
    movl    %edx, %eax
.L2:
    cmpl    %ecx, %eax
    jbe .L3
    movl    %ecx, %eax
.L3:
    popl    %ebp
    ret
    .size   min, .-min
    .ident  "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
    .section    .note.GNU-stack,"",@progbits

インライン化されたバージョンは-O2最適化コード内にあり(私のマーカーmrk = 0xfefefefeでさえ、min()の呼び出しの前後で)gccによって最適化されているので、私はそれを手に入れることができませんでした。

更新:Nils、ephemientによって提案された変更をテストしましたが、min()のアセンブリバージョンを使用しても、パフォーマンスが大幅に向上することはありません。ただし、-march = i686を使用してプログラムをコンパイルすると、12.5%のブーストが得られます。これは、プログラム全体が、gccがこのオプションで生成する新しい高速命令の利点を享受しているためだと思います。助けてくれてありがとう。

P.S. -パフォーマンスを測定するためにRubyプロファイラーを使用しました(私のCプログラムはRubyプログラム)によってロードされた共有ライブラリであるため、 Rubyプログラムによって呼び出された最上位のC関数。これにより、スタックの下位でmin()が呼び出されます。これを参照してください 質問

20
Sudhanshu

まず、適切な-march設定を使用していることを確認してください。 GCCはデフォルトで、元のi386でサポートされていなかった命令を使用しません。新しい命令セットを使用できるようにすると、大きな違いが生じる場合があります。 -march=core2 -O2で私は得る:

min:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    movl    12(%ebp), %ecx
    movl    16(%ebp), %eax
    cmpl    %edx, %ecx
    leave
    cmovbe  %ecx, %edx
    cmpl    %eax, %edx
    cmovbe  %edx, %eax
    ret

ここでcmovを使用すると、分岐の遅延を回避できる場合があります。-marchを渡すだけで、インラインasmなしで取得できます。より大きな関数にインライン化すると、これはさらに効率的になる可能性が高く、おそらく4回のアセンブリ操作だけです。これよりも高速なものが必要な場合は、SSEベクトル演算をアルゴリズム全体のコンテキストで機能させることができるかどうかを確認してください。

11
bdonlan

コンパイラーが昼食に出ていないことを前提とすると、これは2つの比較と2つの条件付き移動にコンパイルされるはずです。それ以上のことをすることは不可能です。

コンパイラが実際に生成しているアセンブリを投稿すると、速度を低下させる不要なものがないかどうかを確認できます。

確認する最も重要なことは、ルーチンが実際にインライン化されていることです。コンパイラーはそうする義務を負わず、関数呼び出しを生成する場合、そのような単純な操作には非常にコストがかかります。

DigitalRossが言ったように、呼び出しが実際にインライン化されている場合は、ループ展開が有益であるか、ベクトル化が可能である可能性があります。

編集:コードをベクトル化し、最近のx86プロセッサを使用している場合は、SSE4.1 pminud命令(組み込み:_mm_min_epu32)、それぞれ4つのunsigned intの2つのベクトルを取り、4つのunsignedintのベクトルを生成します。結果の各要素は、2つの入力の対応する要素の最小値です。

また、コンパイラが条件付き移動の代わりにブランチを使用していることにも注意してください。おそらく、最初に条件付き移動を使用するバージョンを試して、ベクトル実装のレースに進む前に、それによってスピードアップが得られるかどうかを確認する必要があります。

11
Stephen Canon

X86アセンブラーの実装、GCC構文についての私の見解。別のインラインアセンブラ構文に変換するのは簡単なはずです。

int inline least (int a, int b, int c)
{
  int result;
  __asm__ ("mov     %1, %0\n\t"
           "cmp     %0, %2\n\t" 
           "cmovle  %2, %0\n\t"
           "cmp     %0, %3\n\t"
           "cmovle  %3, %0\n\t" 
          : "=r"(result) : 
            "r"(a), "r"(b), "r"(c)
          );
  return result;
}

新規および改善されたバージョン:

int inline least (int a, int b, int c)
{
  __asm__ (
           "cmp     %0, %1\n\t" 
           "cmovle  %1, %0\n\t"
           "cmp     %0, %2\n\t"
           "cmovle  %2, %0\n\t" 
          : "+r"(a) : 
            "%r"(b), "r"(c)
          );
  return a;
}

注:Cコードより高速な場合と高速でない場合があります。

これは多くの要因に依存します。通常、ブランチが予測できない場合はcmovが優先されます(一部のx86アーキテクチャでは)OTOHインラインアセンブラはオプティマイザにとって常に問題であるため、周囲のコードの最適化ペナルティがすべてのゲインを上回る可能性があります。

ところでSudhanshu、このコードがテストデータでどのように機能するかを聞くのは興味深いでしょう。

6

このドロップイン交換は、AMD Phenomで約1.5%速くなります。

static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
    asm("cmp   %1,%0\n"
        "cmova %1,%0\n"
        "cmp   %2,%0\n"
        "cmova %2,%0\n"
        : "+r" (a) : "r" (b), "r" (c));
    return a;
}

結果は異なる場合があります。一部のx86プロセッサはCMOVをうまく処理しません。

5
ephemient

SSE2命令拡張には、一度に8つの最小値を選択できる整数のmin命令が含まれています。見る _mm_mulhi_epu16 in http://www.intel.com/software/products/compilers/clin/docs/ug_cpp/comm1046.htm

5
Mark Ransom

まず、分解を見てください。それはあなたにたくさん教えてくれるでしょう。たとえば、書かれているように、2つのifステートメントがあります(つまり、分岐予測が2つある可能性があります)が、まともな最新のCコンパイラには、分岐なしでそれを実行できる巧妙な最適化があると思います。知りたいのですが。

次に、libcに特別な組み込みの最小/最大関数がある場合は、それらを使用します。 GNU libcには浮動小数点用のfmin/fmaxがあり、「一部のプロセッサでは、これらの関数は特別なマシン命令を使用して、同等のCコードよりも高速にこれらの操作を実行できます」と主張しています。たぶん、uintにも似たようなものがあります。

最後に、これを多数の数値に対して並行して実行している場合、これを実行するためのベクトル命令がおそらく存在します。これにより、大幅な高速化が可能になります。しかし、ベクトル単位を使用すると、非ベクトルコードの方が高速になることもあります。 「1つのuintをベクトルレジスタにロードし、ベクトルmin関数を呼び出し、結果を取得する」のようなものは馬鹿げているように見えますが、実際にはもっと速いかもしれません。

1
Ken

これらはすべて良い答えです。質問に答えなかったと非難されるリスクを冒して、私は他の80%の時間も調べます。 Stackshots は、最適化する価値のあるコードを見つけるための私のお気に入りの方法です。特に、絶対に必要ではないことがわかった関数呼び出しの場合はそうです。

0
Mike Dunlavey

比較を1つだけ行う場合は、ループを手動で展開することをお勧めします。

まず、コンパイラにループを展開させることができるかどうかを確認し、できない場合は自分で実行します。これにより、少なくともループ制御のオーバーヘッドが削減されます。

0
DigitalRoss

このようなことを試して、宣言と不要な比較を節約することができます。

static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{ 
    if (a < b)
    {
        if (a < c) 
             return a; 
        else 
             return c;
    }

    if (b < c)
        return b;
    else return c;
}
0
Paul Sasik

はい、アセンブリ後ですが、私の素朴な最適化は次のとおりです。

static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
    unsigned int m = a;
    if (m > b) m = b;
    if (m > c) return c;
    return m;
}
0
Hamish Grubijan