it-swarm-ja.com

Linuxカーネルの可能性の高い/そうでないマクロはどのように機能し、それらの利点は何ですか?

Linuxカーネルのいくつかの部分を掘り下げてみたところ、次のような呼び出しが見つかりました。

if (unlikely(fd < 0))
{
    /* Do something */
}

または

if (likely(!err))
{
    /* Do something */
}

私はそれらの定義を見つけました:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

私はそれらが最適化のためであることを知っていますが、どのように機能しますか?そして、それらを使用することでどれだけのパフォーマンス/サイズの低下が期待できますか?少なくともボトルネックコード(もちろん、ユーザー空間で)で面倒な(そしておそらく移植性を失う)価値はありますか。

312
terminus

これらは、分岐予測がジャンプ命令の「可能性の高い」側を優先させる命令を発行するようにコンパイラに示唆しています。これは大きな勝利になる可能性があります。予測が正しい場合は、ジャンプ命令が基本的に無料であり、ゼロサイクルを要することを意味します。一方、予測が間違っている場合、プロセッサパイプラインをフラッシュする必要があり、数サイクルかかる可能性があります。ほとんどの場合、予測が正しい限り、これはパフォーマンスに良い傾向があります。

このようなすべてのパフォーマンスの最適化と同様に、コードが実際にボトルネックになっていることを確実にするために、大規模なプロファイリング後にのみ実行する必要があります。一般に、Linux開発者はかなり経験が豊富なので、そうすることになると思います。彼らはgccのみを対象としているため、移植性についてはあまり気にしておらず、生成するアセンブリについて非常に近い考えを持っています。

293

これらは、分岐がどの方向に進むかについてコンパイラにヒントを与えるマクロです。マクロは、使用可能な場合、GCC固有の拡張に展開されます。

GCCはこれらを使用して分岐予測を最適化します。たとえば、次のようなものがある場合

if (unlikely(x)) {
  dosomething();
}

return x;

次に、このコードを次のように再構築できます。

if (!x) {
  return x;
}

dosomething();
return x;

これの利点は、プロセッサが最初に分岐を行うときに、かなり先に投機的にコードをロードして実行している可能性があるため、かなりのオーバーヘッドがあることです。ブランチを取ると判断した場合、それを無効にし、ブランチターゲットから開始する必要があります。

現在、ほとんどの最新のプロセッサには何らかの分岐予測がありますが、これは以前に分岐を通過したことがある場合にのみ役立ち、分岐はまだ分岐予測キャッシュ内にあります。

これらのシナリオでコンパイラーとプロセッサーが使用できる他の多くの戦略があります。ウィキペディアで分岐予測がどのように機能するかについての詳細を見つけることができます: http://en.wikipedia.org/wiki/Branch_predictor

68
dvorak

GCC 4.8の機能を確認するために逆コンパイルします

__builtin_expectなし

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

GCC 4.8.2 x86_64 Linuxでコンパイルおよび逆コンパイルします。

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

出力:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

メモリ内の命令の順序は変更されていません。最初にprintf、次にputs、そしてretqが戻ります。

__builtin_expectを使用

ここでif (i)を次のように置き換えます。

if (__builtin_expect(i, 0))

そして私達は得る:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf__printf_chkにコンパイル)は、putsの後に関数の最後に移動し、他の回答で述べられているように分岐予測を改善するために戻りました。

したがって、基本的には次と同じです。

int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

この最適化は-O0では行われませんでした。

しかし、__builtin_expectを使用すると、使用しない場合よりも高速に動作する例を作成できます。 CPUは、当時は本当にスマートです 。私の素朴な試み はここにあります

これらは、ハードウェアがサポートする適切なブランチヒントをコンパイラに出力させます。これは通常、命令オペコード内の数ビットを調整することを意味するため、コードサイズは変更されません。 CPUは予測された場所から命令のフェッチを開始し、パイプラインをフラッシュして、分岐に到達したときに間違っていることが判明した場合は最初からやり直します。ヒントが正しい場合、これによりブランチが非常に高速になります-正確にどのくらい高速になるかはハードウェアに依存します。そして、これがコードのパフォーマンスにどの程度影響するかは、正しい時間ヒントの割合に依存します。

たとえば、PowerPC CPUでは、ヒントのないブランチは16サイクル、正しくヒントされた8サイクル、誤ってヒントされた24サイクルかかる場合があります。最も内側のループでは、適切なヒントが大きな違いをもたらします。

移植性は実際には問題ではありません。おそらく、定義はプラットフォームごとのヘッダーにあります。静的ブランチヒントをサポートしていないプラットフォームでは、「可能性のある」および「可能性の低い」を何も定義しないでください。

6
moonshadow
long __builtin_expect(long EXP, long C);

このコンストラクトは、式EXPが値Cを持っている可能性が最も高いことをコンパイラに伝えます。戻り値はEXPです。 __ builtin_expectは、条件式で使用するためのものです。ほとんどすべての場合、ブール式のコンテキストで使用されます。この場合、2つのヘルパーマクロを定義する方がはるかに便利です。

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

これらのマクロは、次のように使用できます。

if (likely(a > 1))

参照: https://www.akkadia.org/drepper/cpumemory.pdf

5
Ashish Maurya

多くのLinuxリリースでは、/ usr/linux /にcomplier.hがあり、簡単に使用できるように含めることができます。そして別の意見、likely()はlikes()よりも有用です。なぜなら、

if ( likely( ... ) ) {
     doSomething();
}

多くのコンパイラでも最適化できます。

ところで、コードの詳細な動作を観察したい場合は、次のように簡単に実行できます。

gcc -c test.c objdump -d test.o> obj.s

次に、obj.sを開くと、答えが見つかります。

2
Finaldie

Cody のコメントによると、これはLinuxとは関係ありませんが、コンパイラーへのヒントです。何が起こるかは、アーキテクチャとコンパイラのバージョンによって異なります。

Linuxのこの特定の機能は、ドライバーで多少誤用されています。 osgxホット属性のセマンティクス を指し示すように、ブロック内で呼び出されるhotまたはcold関数は、条件がありそうかどうかを自動的に示唆できます。たとえば、dump_stack()coldとマークされているため、これは冗長です。

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

gccの将来のバージョンは、これらのヒントに基づいて関数を選択的にインライン化する可能性があります。 booleanではなく、最も可能性の高いなどのスコアであるという提案もあります。一般に、代替メカニズムを使用することをお勧めします。 coldなど。ホットパス以外の場所で使用する理由はありません。あるアーキテクチャでコンパイラが行うことは、別のアーキテクチャではまったく異なる場合があります。

2
artless noise

(一般的なコメント-その他の回答が詳細をカバーしています)

それらを使用して移植性を失うべき理由はありません。

常に、他のコンパイラーを使用して他のプラットフォームでコンパイルできる単純なnil効果「インライン」またはマクロを作成するオプションがあります。

他のプラットフォームを使用している場合、最適化の恩恵を受けることはできません。

2

これらは、分岐のヒントプレフィックスを生成するためのコンパイラへのヒントです。 x86/x64では、これらは1バイトを占有するため、ブランチごとに最大で1バイト増加します。パフォーマンスに関しては、アプリケーションに完全に依存します。ほとんどの場合、プロセッサのブランチプレディクタはこれらを無視します。

編集:実際に彼らが実際に助けることができる1つの場所を忘れました。これにより、コンパイラは制御フローグラフの順序を変更して、「可能性のある」パスに使用される分岐の数を減らすことができます。これにより、複数の終了ケースをチェックしているループが著しく改善されます。

1
Cody Brocious

これらは、プログラマが特定の式で最も可能性の高い分岐条件となるものについてコンパイラにヒントを与えるためのGCC関数です。これにより、コンパイラは分岐命令を作成できるため、最も一般的なケースでは実行する命令の数が最も少なくなります。

分岐命令の作成方法は、プロセッサアーキテクチャによって異なります。

1
dcgibbons