3

ARM9 プロセッサ用のロギング C コードを書いています。動的モジュールが存在する場合、このコードはいくつかのデータを記録します。モジュールは通常、製品ビルドには存在しませんが、ロギング コードは常にコンパイルされます。これは、顧客がバグに遭遇した場合に、このモジュールをロードすると、ロギング コードがデバッグ情報をダンプするという考え方です。

モジュールが存在しない場合、ロギング コードは最小限の影響しか与えない必要があるため、すべてのサイクルがカウントされます。一般に、ロギング コードは次のようになります。

__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
    if (NULL == pProvider)
        return;
    ... logging code goes here ...
}

最適化をオンにすると、RVCT 4.0 は次のようなコードを生成します。

ldr     r4,[r0,#0x2C]     ; pProvider,[r0,#44]
cmp     r4,#0x0           ; pProvider,#0
beq     0x23BB4BE (usually taken)
... logging code goes here...
... regular code starts at 0x23BB4BE

このプロセッサには分岐予測子がありません。私の理解では、分岐が行われるたびに 2 サイクルのペナルティがあります (分岐が行われなければペナルティはありません)。

一般的なケースであるNULL == pProviderが、分岐が行われない高速なケースであることを望みます。RVCT 4.0 でこのようなコードを生成するにはどうすればよいですか?

私は__builtin_expect次のように使用してみました:

if (__builtin_expect(NULL == pProvider, 1))
    return;

残念ながら、これは生成されたコードには影響しません。私は__builtin_expect間違って使用していますか?別の方法はありますか (できればインライン アセンブリなし)。

4

4 に答える 4

1

次の構成を使用する場合:

void log_some_stuff_implementation(Provider *pProvider, int x, int y, char const* str);


__inline void log_some_stuff(Provider *pProvider, int x, int y, char const* str)
{
    if (__builtin_expect( pProvider != NULL, 0)) {
        log_some_stuff_implementation(pProvider, x, y, str);
    }

    return;
}

GCC 4.5.2 を使用すると-O2、次の呼び出しに対して次のコードが生成されます (少なくとも私の簡単なテストでは) log_some_stuff()

// r0 already has the Provider* in it - r2 has a value that indicates whether 
//      r0 was loaded with a valid pointer or not
cmp r2, #0
ldrne   r3, [r1, #0]
addne   r1, r2, #1
ldrneb  r2, [r3, #0]    @ zero_extendqisi2
blne    log_some_stuff_implementation

したがって、一般的なケース (Provider* が NULL の場合) では、4 つの命令が使用されますが、条件により実行されませんが、ARM のパイプラインはフラッシュされません。これは、ロギング コードを実際に実行したくないという一般的なケースとほぼ同じくらい良いと思います。

重要なのは、実際にロギング作業を行うコードが別の関数で非インラインで実行されるため、コンパイラはその関数のセットアップと呼び出しを、条件付きで実行されるいくつかの命令のインライン シーケンスにすることが合理的であるということだと思います。実際のロギング コードは最適化する必要がないため、インラインにする必要はありません。これは一般的なケースではなく、実際の作業を行うコードであると思われます。したがって、関数呼び出しのオーバーヘッドは許容できるはずです (少なくともそれは私の仮定です)。

ちなみに、私の単純なテストでは、 を省略しても同じコード シーケンス (または本質的に同じシーケンス) が生成さ__builtin_expect()れますが、単純なテストよりも複雑なシーケンスでは、ビルトインがコンパイラを支援する可能性があると思います。したがって、おそらくそのままにしておきますが、Linux カーネルのマクロのような、より読みやすいバージョンも使用するでしょう。

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)
于 2011-03-23T05:31:09.253 に答える
1

では、分岐予測子がなく、分岐を取るときに 2 サイクルのペナルティが発生する場合は、それに応じてプログラムを書き直してください。(実際には、上記の例はすでに「正しい」コードになっていると思うかもしれませんが、試すことができます)

__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
    if (pProvider) {
       ... logging code goes here ...
    }
}

コンパイルして次のように「できた」:

ldr     r4,[r0,#0x2C]     ; pProvider,[r0,#44]
cmp     r4,#0x0           ; pProvider,#0
bneq     logging_code (usually NOT taken)
... regular code here
logging_code: .. well anywhere

運が良ければ、たとえ今そうなったとしても、コンパイラへのすべての変更が変更される可能性があり、使用しているコンパイラでアセンブリコードが生成されるかどうかさえわかりません. とにかく、おそらくインラインアセンブリでそれを書きますか?それほど多くのコードと gcc はありません (VC と同様です。他の人もそうだと思います)。最も簡単なのは、ロギング コードで追加のメソッドを定義し、それを呼び出すことです (ARM ABI についてはわからないため、自分で作成する必要があります)。

于 2011-03-22T22:01:09.600 に答える
0

使用できますgoto

__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
    if (pProvider != NULL)
        goto LOGGING;
    return;
LOGGING:
    ... logging code goes here ...
}

__builtin_expectを使用する方が簡単ですが、RVCT にそれがあるかどうかはわかりません。

于 2011-03-23T08:00:28.717 に答える
0

ブランチを最適化しても、得られるものはほとんどありません。次のことを行うと、さらに多くを得ることができます。

#define log_some_stuff(pProvider, other_arg) \
        do {\
           if(pProvider != NULL) \
              real_log_some_stuff(pProvider, other_arg); \
         } \
        while(0)

これが行うことは、すべての呼び出しコードに NULL チェックをインライン化することです。それは損失のように思えるかもしれませんが、実際に起こることは、コンパイラーが関数呼び出しのオーバーヘッドを回避できるということです。これには、レジスターのプッシュ、分岐自体、r0-r3 および lr の単純な NULL チェックによる無効化が含まれます。とにかくしなければならなかった)。全体として、これは、1 つの命令を早期に終了することによって節約された 1 サイクルよりもはるかに多くの利益をもたらすに違いありません。

于 2011-03-23T06:15:31.957 に答える