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


348

私は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)

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


7
これは本当にLinuxカーネルやマクロに固有のものではなく、コンパイラーの最適化です。これを反映するようにタグを付け直す必要がありますか?
Cody Brocious

11
すべてのプログラマがメモリについて知っておくべきこと(p。57)のペーパーには、詳細な説明が含まれています。
Torsten Marek、

2
参照BOOST_LIKELY
Ruggero


13
移植性の問題はありません。この種のヒントをサポートしていないプラットフォーム#define likely(x) (x)#define unlikely(x) (x)プラットフォームで簡単に行うことができます。
David Schwartz

回答:


329

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

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


3
これらのマクロは主にエラーチェックに使用されていました。エラーはおそらく通常の操作よりも少ないためです。少数の人々がプロファイリングまたは計算を行って、最も使用された葉を決定します...
ジヴェンコア

51
フラグメントに関しては"[...]that it is being run in a tight loop"、多くのCPUに分岐予測子があるため、これらのマクロを使用するのは、コードが初めて実行されたとき、または履歴テーブルが分岐テーブルへの同じインデックスを持つ別の分岐によって上書きされたときだけです。タイトなループでは、ほとんどの場合分岐が一方通行であると想定すると、分岐予測子はおそらく正しい分岐を非常に迅速に推測し始めます。-歩兵のあなたの友人。
ロスロジャース

8
@RossRogers:実際に起こるのは、コンパイラーがブランチを配置するため、一般的なケースは行われないことです。これは、分岐予測が機能する場合でも高速です。分岐は完全に予測されている場合でも、命令のフェッチとデコードに問題があります。一部のCPUは、履歴テーブルにない分岐を静的に予測します。通常は、前方分岐では行われないと想定します。Intel CPUはそのように機能しません。予測子テーブルエントリがこのブランチ用であることを確認しようとせず、とにかくそれを使用します。ホットブランチとコールドブランチは同じエントリをエイリアスする可能性があります...
Peter Cordes '15

12
主な主張は分岐予測に役立つということであり、@ PeterCordesが指摘するように、ほとんどの最新のハードウェアでは暗黙的または明示的な静的分岐予測はありません。実際、このヒントは、静的分岐ヒントや他のタイプの最適化を含む、コードを最適化するためにコンパイラーによって使用されます。ほとんどのアーキテクチャのために、今日、それは「他の最適化」であり、その事項、例えば、より良いなど、など、唯一の期待パスをベクトル、遅いパスのサイズを最小限に抑え、ホットパスをスケジューリングし、ホットパスが連続して作る
BeeOnRope

3
@BeeOnRopeはキャッシュのプリフェッチとワードサイズのため、線形にプログラムを実行することには依然として利点があります。次のメモリの場所はすでにフェッチされており、キャッシュにあります。ブランチターゲットは、そうでない場合もあります。64ビットCPUでは、一度に少なくとも64ビットを取得します。DRAMインターリーブによっては、2x 3x以上のビットがグラブされる場合があります。
ブライス

88

逆コンパイルして、GCC 4.8が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 main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

この最適化は、では行われていません-O0

しかし__builtin_expectCPUは、使用しない場合よりも速く実行される例を書くのに幸運です。最近のCPUは本当に賢いです。私の素朴な試みはここにあります

C ++ 20 [[likely]]および[[unlikely]]

C ++ 20はこれらのC ++ビルトインを標準化しました:if-elseステートメントでC ++ 20の見込み/非見込み属性を使用する方法彼らはおそらく(しゃれ!)同じことをします。


71

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

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

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

コンパイラとプロセッサがこれらのシナリオで使用できる他の多くの戦略があります。ブランチプレディクターの動作の詳細については、ウィキペディアをご覧ください。http//en.wikipedia.org/wiki/Branch_predictor


3
また、それはicacheのフットプリントに影響を及ぼします-ありそうもないコードのスニペットをホットパスから遠ざけることによって。
2015

2
より正確には、でそれを行うことができますgoto繰り返すことなく、S return xstackoverflow.com/a/31133787/895245
チロSantilli郝海东冠状病六四事件法轮功

7

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

たとえば、PowerPC CPUでは、ヒンティングされていないブランチは16サイクルかかります。正しくヒントされたブランチは8で、正しくヒントされていないブランチは24です。

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


3
記録のために、x86はブランチヒント用に追加のスペースを使用します。適切なヒントを指定するには、ブランチに1バイトのプレフィックスを付ける必要があります。ただし、ヒンティングはグッドシング(TM)であることに同意しました。
Cody Brocious

2
Dang CISC CPUとその可変長命令;)
moonshadow

3
Dang RISC CPU-15バイトの命令に
近づかない

7
@CodyBrocious:ブランチヒントはP4で導入されましたが、P4とともに廃止されました。他のすべてのx86 CPUは、それらのプレフィックスを単に無視します(プレフィックスは、意味のないコンテキストでは常に無視されるためです)。これらのマクロによって、x86でgccが実際にブランチヒントプレフィックスを発行することはありません。これらは、gccが高速パス上で分岐した分岐が少ない関数をレイアウトするのに役立ちます。
Peter Cordes

5
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


1
別の答えへのコメントで尋ねられたように-マクロの二重反転の理由は何ですか(すなわち、なぜ__builtin_expect(!!(expr),0)単に代わりに使用するの__builtin_expect((expr),0)ですか?
Michael Firth

1
@MichaelFirthの「二重反転」!!は、何かをにキャストすることと同じboolです。一部の人々はこのようにそれを書くのが好きです。
ベンXO

2

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

それらを使用して移植性を失う必要がある理由はありません。

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

他のプラットフォームを使用している場合、最適化のメリットは得られません。


1
あなたは移植性を使用しません-それらをサポートしないプラットフォームは、それらを空の文字列に拡張するように定義するだけです。
シャープトゥース

2
私はあなた方2人は実際には互いに同意していると思います-混乱を招くように言い表されているだけです。(その見た目から、Andrewのコメントは「移植性を失うことなくそれらを使用できる」と言っていますが、シャープトゥースは彼が「ポータブルではないのでそれらを使用しないでください」と考えて反対しました。)
Miral

2

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

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

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

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


2

多くのLinuxリリースでは、complier.hは/ usr / linux /にあり、簡単に使用するために含めることができます。また、別の見方として、likely()は尤度()ではなく便利です。

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

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

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

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

次に、obj.sを開きます。答えを見つけることができます。


1

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

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


10
gccがx86ブランチヒントを生成することはありません。少なくとも、すべてのIntel CPUはそれらを無視します。ただし、インライン化やループのアンロールを回避することで、ありそうもない領域のコードサイズを制限しようとします。
アレックス奇妙な

1

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.