2011-06-02 9 views
4

私はこのコードをプロファイリングしていないと言っています。これは主に自分の好奇心のためです。静的変数最適化のC分岐

私は、静的intを既知のエラー値に宣言または定義する関数を持っています。この関数は、コードが分岐するようにします。しかし、関数が成功すると、ブランチが決して再び取られないことが確実にわかります。これのコンパイル時間の最適化はありますか?特にGNU/gcc/glibc?

だから私は、この持っている:関数は(この関数はnullを返した場合、私はプログラムを終了)正常に完了したら、私は将来のすべての呼び出しのためのFD意志が有効であることを知っていると取ることはありませんので、

static unsigned long volatile *getReg(unsigned long addr){ 

    static int fd = -1; 

    if (fd < 0){ 
     if (fd = open("file", O_RDWR | O_SYNC) < 0){ 
      return NULL; 
     } 
    } 
} 

を最初の枝。私は__builtin_expect()マクロがあります知っているので、私は

if (__builtin_expect((fd<0),0){ 

しかし、私はそれがコンパイラへのヒントだけだし、それはまだ条件チェックを実行する必要があります理解して何から書くことができます。また、99.9999%のケースで十分なパフォーマンスが得られ、それ以上のパフォーマンスの向上はごくわずかであることがわかります。

最初の状態チェック(fd < 0)が初めて実行された後でさえも防ぐ方法があるのだろうかと思っていました。

+0

自己修正コードが不足しているため、条件に応じて異なる動作が予想される状況で条件付き(または同等の機能ポインタ)を回避する方法はないと私は考えています。 –

答えて

1

これを修正する方法の1つは、関数ポインタを使用してメソッドを呼び出すことです。関数ptrを長い関数に初期化し、最初の呼び出しの最後に追加の初期化をせずにバージョンに設定します。

これは絶対的なメンテナンスの悪夢のように聞こえますが、確かに1つのブランチを避ける価値はありませんが、ブランチを取り除くことはできます(そして、関数がどのようにインライン化されるのか長い間機能がほぼ確実に有害になる)

+0

私は実際に最も正確な答えのように聞こえるので、私はこの答えを受け入れています。私は関数ポインタの切り替えについては考えなかった。 – Falmarri

+0

間接呼び出しは直接呼び出し(潜在的に何百倍も遅い)よりも遅いため、単純な実装よりもほとんど確実に_slower_になります。だから、これは実装が難しく、読みにくく、遅くなります...しかしそれ以外は完璧な答えです。 (ちなみに、これは私が "関数へのポインタでトリック"を述べたときに私が参照していたものです。) – Nemo

+0

うん。私はそれが遅くなるという事実を理解しています。私の質問はパフォーマンスの最適化のためのものではありませんでした。私はもっ​​と考えていました、もしあなたが望んでいない副作用がコンディションチェック自体にあったらどうでしょうか?または、エラー状態の後に実行すると、チェック自体がエラーを引き起こす可能性がありますか? (私は知っている、チェック/状態をリファクタリングする、明らかに= P) – Falmarri

2

、独自のソース・ファイル内のスプリット2内の関数、...、それについて、発信者の心配をしましょう:)

static int fd; 

unsigned long volatile *getReg(unsigned long addr) { 
    /* do stuff with fd and addr */ 
    return 0; 
} 

int getRegSetup(void) { 
    fd = open("file", O_RDWR | O_SYNC); 
    if (fd < 0) return 1;    /* error */ 
    /* continue processing */ 
    return 0;       /* ok */ 
} 

呼び出し側はその後、

/* ... */ 
    if (getRegSetup()) { 
    /* error */ 
    } else { 
    do { 
     ptr = getReg(42); 
    } while (ptr); 
    } 
    /* ... */ 
+0

それは条件チェックを排除しません。他の場所に移動するだけです。関数アドレスなどを検索しなければならないので、これ以上の命令はいくつかあります。 – Falmarri

+0

'getReg'を百万回呼び出す必要がある場合でも条件は一度だけテストされます – pmg

+0

@Falmarri、それは_does_すべての呼び出しで' getReg'。 – paxdiablo

4

を行い、短い答えは "ですいいえ "。

確かに、関数へのポインタ、猿のパッチなどでトリックを演奏することもできますが、それはテストを行うよりも遅くなるでしょう。

ブランチは誤って予測された場合にのみ高価です。 __builtin_expectは、このブランチが最初に誤って予測されることを保証するように手配します。

ここでは、このコードの近くでCPUが何をしているのかによって、文字通り1〜2サイクルの話があります。

[更新]

このようなものが本当に毎秒数百万回または数十億呼び出されている場合は、早期にfdを初期化し、テストを煩わせることなく、それを繰り返し使用するようにコードを再構築することによって、それに対処するでしょう。たとえば、initGlobalState();コールをmain()の上部近くに追加してからファイルを開くことができます。 (対応するdestroyGlobalState();がもう一度それを閉じることを望むでしょう)。

もちろん、ファイルディスクリプタは恐ろしい例です。あなたがやっていることは、とにかく1〜2サイクル以上かかるからです。

C++では、コンストラクタ、デストラクタ、およびRAII idiomは、この種のアプローチを非常に自然なものにしています。

+0

+1:同意してください。もちろん、いくつかの状況では、その関数が毎秒何百万回も呼び出されている場合は、2つのサイクルが重要になる可能性があります。 –

+0

私はあなたに同意します。条件チェックは重要ではありません。しかし、私が言ったように、これは純粋に教育とhactucationalの目的のためです。] – Falmarri

+0

@Oli:関数呼び出し自体はそれよりも時間がかかることを除いて...しかし、私は更新を追加すると思います。ありがとう。 – Nemo

0

__builtin_expectはヒントです。コンパイラがより良いコードを生成するのに役立ちます。たとえば、メインラインコードがメモリ内で連続的に整列するようにジャンプラベルを並べ替えることで、コードキャッシュラインをより使いやすくし、メインメモリなどから簡単にフェッチすることができます。プロファイルガイドによる最適化の実行はさらに優れています。

私はあなたのコードにロックが見当たらないので、この関数は複数のスレッドから同時に呼び出されることは想定されていません。この場合、関数スコープからfdを移動しなければならないので、ダブルチェックロックは適用されません。次に、コードを少し並べ替えます(GCCがブランチのヒントとしていることですが、あなたは知っています...)。さらに、頻繁にアクセスする場合は、ファイルディスクリプタをメインメモリ/キャッシュラインからレジスタにコピーすることができます。コードは次のようになります:

static int g_fd = -1; 

static unsigned long volatile *getReg(unsigned long addr) 
{ 
    register int fd = g_fd; 

    if (__builtin_expect ((fd > 0), 1)) 
    { 
on_success: 
     return NULL; // Do important stuff here. 
    } 

    fd = open("file", O_RDWR | O_SYNC); 

    if (__builtin_expect ((fd > 0), 1)) 
    { 
     g_fd = fd; 
     goto on_success; 
    } 

    return NULL; 
} 

しかし、これを真剣に受けないでください。システムコールとファイルI/Oはとても悪いので、このような最適化は意味がありません(いくつかの例外を除いて)。

本当に一度呼びたい場合は、一度呼び出された別の関数にファイルを移動する方が良いでしょう。そして、はい、GCCのプロファイルフィードバックとLTOを見てください。これは、このようなものにあまりにも多くの時間を費やすことなく、良い結果を達成するのに役立ちます。

+0

__builtin_expectはそれ以上のことを行います。実際にはコンパイラの分岐予測に役立ちます。デフォルトの分岐予測は、前方分岐が取られず、後方分岐が取られると仮定することである。 (これは実行時にCPUが各ブランチの使用頻度を記録するのに伴って動的に変化する可能性があります)x86_64を__builtin_expectの異なる設定でコンパイルしてアセンブリを見ると、 (forward branch = not taken、backward branch = takenと仮定します)。いくつかのアーキテクチャでは、__builtin_expectは実際にブランチのビットをinsn – Nemo

+0

に設定します。@Nemoはx86の述部を__builtin_expectに設定しますか?それらが現代のCPUで有益かどうかは分かりませんが、そうすることができるように思えます。 – Voo

+0

@Voo:x86は分岐命令に予測ビットを持ちませんが、__builtin_expectはGCCに生成されたコードを再配置し、CPUのデフォルトの予測ヒューリスティックと一致させます。 – Nemo

0

私が思いついたのは、これが私が思いついたものです。これは、長時間実行されるより大きなプログラムへのモジュールであることに注意してください。また、それは見直されておらず、基本的にはとにかく悪いハックです。

__attribute__((noinline)) static unsigned int volatile *get_mem(unsigned int addr) { 
    static void *map = 0 ; 
    static unsigned prevPage = -1U ; 
    static int fd = -1; 
    int poss_err = 0; 
    register unsigned page = addr & ~MAP_MASK ; 

    if (unlikely(fd < 0)) { 
     if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) { 
      longjmp(mem_err, errno); 
     } 
    } 
    if (page != prevPage) { 
     if (map) { 
      if (unlikely((munmap(map,MAP_SIZE) < 0))) poss_err = 1; 
     } 
     if (unlikely((map = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, page)) == MAP_FAILED)) longjmp(mem_err, errno); 

     prevPage = page ; 
    } 
    return (unsigned int volatile *)((char *)map+(addr & MAP_MASK)); 
} 

static void set_reg(const struct reg_info * const r, unsigned int val) 
{ 
    unsigned int volatile * const mem = get_mem(r->addr); 
    *mem = (*mem & (~(r->mask << r->shift))) | (val << r->shift); 
} 

// This isn't in the final piece. There are several entry points into this module. Just an example 

static int entryPoint(unsigned int value){ 

    if (setjmp(mem_err)!=0) { 
     // Serious error 
     return -1; 
    } 

    for (i=0; i<n; i++) { 
     if (strlen(regs[i].name) == strlen(name) && 
       strncmp(regs[i].name, name, strlen (name))==0) { 

      set_reg(&regs[i], value); 
      return value; 
     } 
    } 
} 

これは明らかに、すべての呼び出しで条件をチェックするので、この質問に対する答えではありません。