Cコンパイラがスイッチを最適化する理由と異なる場合


9

最近、奇妙な問題に遭遇したとき、私は個人的なプロジェクトに取り組んでいました。

非常にタイトなループでは、0〜15の値の整数があります。値0、1、8、9の場合は-1を取得し、値4、5、12、13の場合は1を取得する必要があります。

私はいくつかのオプションを確認するためにgodboltを使用しましたが、コンパイラーがifチェーンと同じ方法でswitchステートメントを最適化できないようであることに驚きました。

リンクはここにあります:https//godbolt.org/z/WYVBFl

コードは次のとおりです。

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}

int b(int num) {
    num &= 0xF;

    if (num == 0 || num == 1 || num == 8 || num == 9) 
        return -1;

    if (num == 4 || num == 5 || num == 12 || num == 13)
        return 1;

    return 0;
}

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
        default:
            return 0;
    }
}

私はbとcで同じ結果が得られると考えていましたが、解決策(switchステートメント-別の形式)がかなり遅いため、ビットハックを読んで効率的な実装を自分で作成できることを期待していました。

奇妙なことに、かなり最適化されていないか、ターゲットのハードウェアに依存する別のケースに削減されているb間、ビットハックにコンパイルさcれましたa

なぜこの矛盾があるのか​​誰かが説明できますか?このクエリを最適化する「正しい」方法は何ですか?

編集:

明確化

私が欲しいスイッチソリューションは、最速、または同様に「クリーン」なソリューションであること。ただし、私のマシンで最適化を使用してコンパイルすると、ifソリューションの方が大幅に高速になります。

デモンストレーション用の簡単なプログラムを作成したところ、TIOはローカルで見つけたのと同じ結果を得ました。オンラインで試してみてください!

static inlineルックアップテーブルは少しスピードアップ:オンラインそれをお試しください!


4
その答えは、「コンパイラーはいつも正しい選択をするわけではない」と思います。私はあなたのコードをGCC 8.3.0でオブジェクト-O3にコンパイルcしましたが、それよりもaまたはより悪い何かにコンパイルされましたbc2つの条件付きジャンプといくつかのビット操作があったのに対して、1つの条件付きジャンプとより簡単なビット操作しかなかったb)。素朴なアイテムごとのテストよりも優れています。ここで本当に何を求めているのかわかりません。単純な事実は、最適化コンパイラを回すことができるということですどんなにこれらのを任意の選択したので、それならば、他の、それがまたは行うことはありませんでしょう何のための厳格なルールはありません。
ShadowRanger

私の問題は、高速である必要があることですが、ifソリューションは過度に保守可能ではありません。コンパイラーがよりクリーンなソリューションを十分に最適化する方法はありますか?この場合にそれができない理由を誰かが説明できますか?
LambdaBeta

まず、少なくとも関数を静的として定義することから始めます。
wildplasser

@wildplasserはスピードアップしますが、ifそれでもビートしますswitch(奇妙にルックアップがさらに速くなります)[フォローするTIO]
LambdaBeta

@LambdaBeta特定の方法で最適化するようコンパイラーに指示する方法はありません。clangとmsvcはこれらに対してまったく異なるコードを生成することに注意してください。気にせず、gccで最適に機能するものが何でも欲しい場合は、それを選択してください。コンパイラーの最適化はヒューリスティックに基づいており、すべてのケースで最適なソリューションを生み出すわけではありません。彼らは平均的なケースでは良いことをしようとしているが、すべてのケースで最適というわけではない。
キュービック

回答:


6

すべてのケースを明示的に列挙する場合、gccは非常に効率的です。

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
            case 2: case 3: case 6: case 7: case 10: case 11: case 14: case 15: 
        //default:
            return 0;
    }
}

単純なインデックス付きブランチでコンパイルされます:

c:
        and     edi, 15
        jmp     [QWORD PTR .L10[0+rdi*8]]
.L10:
        .quad   .L12
        .quad   .L12
        .quad   .L9
        .quad   .L9
        .quad   .L11
        .quad   .L11
        .quad   .L9
        .quad   .L9
        .quad   .L12
etc...

default:コメントされていない場合、gccはネストされたブランチバージョンに戻ります。


1
@LambdaBeta最近のIntel CPUは2つの並列インデックス付きメモリ読み取り/サイクルを実行できるのに対し、私のトリックのスループットはおそらく1ルックアップ/サイクルであるため、私の回答を受け入れずに受け入れることを検討する必要があります。フリップ側では、おそらく私のハックは、SSE2と4ウェイベクトル化により適しているpslld/ psradまたはその8ウェイAVX2同等物。コードの他の特殊性に大きく依存します。
Iwillnotexist Idonotexist

4

Cコンパイラはswitch、プログラマがイディオムを理解してswitch悪用することを期待しているため、の特別なケースがあります。

次のようなコード:

if (num == 0 || num == 1 || num == 8 || num == 9) 
    return -1;

if (num == 4 || num == 5 || num == 12 || num == 13)
    return 1;

有能なCコーダーによるレビューに合格しない; 3人または4人のレビューアが同時に「これはswitch!」

Cコンパイラがifジャンプテーブルに変換するためにステートメントの構造を分析することは価値がありません。そのための条件は正確である必要があり、一連のifステートメントで可能な変動の量は天文学的なものです。分析は複雑で否定的な結果になる可能性があります(「いいえ、これらifのをaに変換することはできませんswitch」)。


私が知っている、それが私がスイッチから始めた理由です。ただし、ifソリューションは私の場合はかなり高速です。基本的に、スイッチではなくifsでパターンを見つけることができたので、スイッチに対してより良いソリューションを使用するようにコンパイラーを説得する方法があるかどうか尋ねています。(ifsは明確または保守可能ではないため、具体的には好きではありません)
LambdaBeta

私はこの質問をした理由はまさに感情なので、賛成ですが受け入れられません。私がしたいスイッチを使用するが、それは私の場合は遅すぎる、私は避けたいifすべての可能であれば。
LambdaBeta

@LambdaBeta:ルックアップテーブルを使用しない理由はありますか?それを作成し、割り当てているものをもう少し明確にしたい場合はC99指定のイニシャライザstatic使用してください。
ShadowRanger、

1
少なくともロービットを破棄して、オプティマイザが実行する必要のある作業が少なくなるように始めます。
R .. GitHub ICEのヘルプを停止

@ShadowRanger残念ながら、それよりもまだ遅いですif(編集を参照)。@R ..私が今使用しているのは、コンパイラーの完全なビット単位のソリューションです。残念ながら、私の場合、これらはenum裸の整数ではなく値なので、ビット単位のハックはあまり維持できません。
LambdaBeta

4

次のコードは、〜3クロックサイクル、〜4の有用な命令、および〜13バイトのinline高度なx86マシンコードで、ルックアップブランチフリー、LUTフリーを計算します。

2の補数の整数表現に依存します。

ただし、u32およびs32typedefが実際に32ビットの符号なし整数型と符号付き整数型を指すようにする必要があります。stdint.hタイプuint32_tint32_t適切でしたが、ヘッダーが利用可能かどうかはわかりません。

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}


int d(int num){
    typedef unsigned int u32;
    typedef signed   int s32;

    // const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
    // 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
    // Hexadecimal:                   F     0     5     0     F     0     5     0
    const u32 K = 0xF050F050U;

    return (s32)(K<<(num+num)) >> 30;
}

int main(void){
    for(int i=0;i<16;i++){
        if(a(i) != d(i)){
            return !0;
        }
    }
    return 0;
}

こちらをご覧くださいhttps : //godbolt.org/z/AcJWWf


定数の選択について

ルックアップは、-1から+1までの16個の非常に小さな定数を対象としています。それぞれ2ビット以内に収まり、16ビットあります。これらは次のようにレイアウトできます。

// const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
// 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
// Hexadecimal:                   F     0     5     0     F     0     5     0
u32 K = 0xF050F050U;

最上位ビットに最も近いインデックス0でそれらを配置することにより、を1回シフトする2*numと、2ビット数の符号ビットがレジスタの符号ビットに配置されます。2ビットの数値を32-2 = 30ビットだけ右にシフトすると、完全にに拡張されint、トリックが完了します。


これは、それmagicを再生成する方法を説明するコメントでそれを行う最もクリーンな方法かもしれません。どのようにしてそれを思いついたのですか?
LambdaBeta '10 / 10/19

これは高速でありながら「クリーン」にすることができるため、受け入れられました。(いくつかのプリプロセッサマジックを介して:) < xkcd.com/541 >)
LambdaBeta

1
私のブランチレスの試みを!!(12336 & (1<<x))-!!(771 & (1<<x));
打ち負かす

0

演算のみを使用して同じ効果を作成できます。

// produces : -1 -1 0 0 1 1 0 0 -1 -1 0 0 1 1 0 0 ...
int foo ( int x )
{
    return 1 - ( 3 & ( 0x46 >> ( x & 6 ) ) );
}

ただし、技術的には、これは(ビットごとの)ルックアップです。

上記が難解であると思われる場合は、次のようにすることもできます。

int foo ( int x )
{
    int const y = x & 6;
    return (y == 4) - !y;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.