11

私は2つのファイルを持っています:

#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

int main()
{
    unsigned int input;
    scanf("%u", &input);

    switch (input)
    {
        case 0: print0(); break;
        case 1: print1(); break;
        case 2: print2(); break;
        case 3: print3(); break;
        case 4: print4(); break;
    }
    return 0;
}

#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

int main()
{
    unsigned int input;
    scanf("%u", &input);

    static void (*jt[])() = { print0, print1, print2, print3, print4 };
    jt[input]();
    return 0;
}

私は、それらがほぼ同一のアセンブリ コードにコンパイルされることを期待していました。どちらの場合もジャンプ テーブルが生成されますが、最初のファイルの呼び出しは で表されjmp2 番目のファイルの呼び出しは で表されますcallcallコンパイラがsを最適化しないのはなぜですか? jmps の代わりにsを表示したいことを gcc に示唆することは可能callですか?

gcc -Wall -Winline -O3 -S -masm=intel、GCC バージョン 4.6.2 でコンパイル。GCC 4.8.0 ではわずかに少ないコードしか生成されませんが、問題は解決しません。

UPDjt : asの定義const void (* const jt[])() = { print0, print1, print2, print3, print4 };と関数の作成static const inlineは役に立ちませんでした: http://ideone.com/97SU0

4

6 に答える 6

8

コンパイラの作成者は、やるべきことがたくさんあります。明らかに、彼らは最大かつ最速の見返りがある仕事を優先します。

Switchステートメントはすべての種類のコードで共通であるため、それらに対して実行される最適化は多くのプログラムに影響を及ぼします。

このコード

jt[input](); 

あまり一般的ではないため、コンパイラ設計者のTODOリストの下位にあります。おそらく、彼らはそれを最適化しようとする努力の価値を(まだ)見つけていませんか?それは彼らに既知のベンチマークを勝ち取るでしょうか?または、広く使用されているコードベースを改善しますか?

于 2012-05-15T14:03:19.900 に答える
5

関数ポインターの配列は変更可能であるためです。コンパイラは、ポインターが変更されないと想定できないと判断しました。C++ ではアセンブリが異なる場合や、jt const を作成する場合があります。

于 2012-05-15T13:57:36.240 に答える
3

私の推測では、この最適化は、 : の直後にステートメントがあるという事実に関係しているとreturn思われます。選択した 内のCPU ヒットは、からのリターンとして機能します。switchprint0print4calljmpretprintNmain

スイッチのにコードを挿入して、コンパイラが に置き換えるかどうかを確認jmpしてくださいcall

#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

int main()
{
    unsigned int input;
    scanf("%u", &input);

    switch (input)
    {
        case 0: print0(); break;
        case 1: print1(); break;
        case 2: print2(); break;
        case 3: print3(); break;
        case 4: print4(); break;
    }
    /* Inserting this line should force the compiler to use call */
    printf("\nDone");
    return 0;
}

編集: ideone のコードにはjmp別の理由があります: これと同等です:

static const char* LC0 ="Zero";
static const char* LC1 ="One";
static const char* LC2 ="Two";
static const char* LC3 ="Three";
static const char* LC4 ="Four";

int main()
{
    unsigned int input;
    scanf("%u", &input);

    switch (input)
    {
        case 0: printf(LC0); break;
        case 1: printf(LC1); break;
        case 2: printf(LC2); break;
        case 3: printf(LC3); break;
        case 4: printf(LC4); break;
    }
    printf("\nDone");
    return 0;
}
于 2012-05-15T13:56:56.840 に答える
1

call後者の関数のコードは、間接関数と後続関数の間で何もしませんretか? 間接呼び出しのアドレス計算で、後者の関数が保持する必要がある値のレジスタを使用しても驚かないでしょう (つまり、計算の前に値を保存し、後で復元する必要があります)。間接呼び出しの前にレジスタ復元コードを移動することは可能かもしれませんが、コンパイラは、正当な機会として認識するようにプログラムされている場合にのみ、そのようなコード移動を実行できます。

また、それは問題ではないと思いますがinline、コンパイラーがそのように実行できないため、ルーチンはそうすべきではないことをお勧めします。

于 2012-05-15T18:04:26.720 に答える
1

別のコードをプロファイリングしましたか? 間接呼び出しが最適化されているという議論がなされるかもしれないと思います。次の分析は、x64 プラットフォーム (MinGW) を対象とする GCC 4.6.1 で行われました。

を使用すると何が起こるかを見るとjt[input]()、呼び出しにより、次の一連のコードが実行されます。

  • printX()関数の 1 つへの間接呼び出し
  • printX()関数は の引数を設定し、printf()次に
  • にジャンプしますprintf()
  • 呼び出しは、printf()間接呼び出しのサイトに直接戻ります。

全部で3支店。

switch ステートメントを使用すると、次のようになります。

  • ケースごとのカスタム コードへの間接的なジャンプ (インラインprintX()呼び出し)
  • 「ケースハンドラー」は、printf()呼び出しに適切な引数をロードします
  • 通話printf()
  • 呼び出しはprintf()「ケースハンドラー」に戻ります。
  • スイッチの終了ポイントにジャンプします (終了コードがインライン化されている 1 つのケース ハンドラーを除く - 他のケースはそこにジャンプします)

合計 4 つの分岐 (一般的な場合)。

どちらの状況でも、次のような場合があります。 - 間接分岐 (一方は呼び出し、もう一方はジャンプ) - への分岐printf()(一方はジャンプ、もう一方は呼び出し) - 呼び出しサイトに戻る分岐

ただし、switchステートメントを使用すると、スイッチの「最後」に到達するための追加の分岐があります (ほとんどの場合)。

さて、実際にプロファイリングした場合、プロセッサは間接ジャンプを間接呼び出しよりも速く処理する可能性がありますが、その場合でも、スイッチベースのコードで使用される追加のブランチがスケールを押し上げると思います.関数ポインターを介した呼び出しを支持します。


興味のある方のために、以下を使用して生成されたアセンブラーを示しますjk[input]();(x64 をターゲットとする GCC MinGW 4.6.1 でコンパイルされた両方の例、使用されたオプションは-Wall -Winline -O3 -S -masm=intel):

print0:
    .seh_endprologue
    lea rcx, .LC4[rip]
    jmp printf
    .seh_endproc

// similar code is generated for each printX() function
// ...

main:
    sub rsp, 56
    .seh_stackalloc 56
    .seh_endprologue
    call    __main
    lea rdx, 44[rsp]
    lea rcx, .LC5[rip]
    call    scanf
    mov edx, DWORD PTR 44[rsp]
    lea rax, jt.2423[rip]
    call    [QWORD PTR [rax+rdx*8]]
    xor eax, eax
    add rsp, 56
    ret

スイッチベースの実装用に生成されたコードは次のとおりです。

main:
    sub rsp, 56
    .seh_stackalloc 56
    .seh_endprologue
    call    __main
    lea rdx, 44[rsp]
    lea rcx, .LC0[rip]
    call    scanf
    cmp DWORD PTR 44[rsp], 4
    ja  .L2
    mov edx, DWORD PTR 44[rsp]
    lea rax, .L8[rip]
    movsx   rdx, DWORD PTR [rax+rdx*4]
    add rax, rdx
    jmp rax
    .section .rdata,"dr"
    .align 4
.L8:
    .long   .L3-.L8
    .long   .L4-.L8
    .long   .L5-.L8
    .long   .L6-.L8
    .long   .L7-.L8
    .section    .text.startup,"x"
.L7:
    lea rcx, .LC5[rip]
    call    printf
    .p2align 4,,10


.L2:
    xor eax, eax
    add rsp, 56
    ret

.L6:
    lea rcx, .LC4[rip]
    call    printf
    jmp .L2

     // all the other cases are essentially the same as the one above (.L6)
     // where they jump to .L2 to exit instead of simply falling through to it
     // like .L7 does
于 2012-05-15T15:59:02.880 に答える