整数の範囲を指定してオプティマイザにヒントを与えることはできますか?


173

intタイプを使用して値を格納しています。プログラムのセマンティクスにより、値は常に非常に小さな範囲(0〜36)で変化し、int(a charではなく)はCPUの効率のためにのみ使用されます。

このような小さな範囲の整数に対して、多くの特別な算術最適化を実行できるようです。これらの整数に対する多くの関数呼び出しは、「魔法の」操作の小さなセットに最適化される可能性があり、一部の関数は、テーブルルックアップに最適化されることもあります。

それで、これintが常にその小さな範囲にあることをコンパイラーに伝えることは可能ですか?そしてコンパイラーがそれらの最適化を行うことは可能ですか?


4
値の範囲の最適化は、多くのコンパイラに存在します。llvmですが、それを宣言するための言語のヒントは知りません。
Remus Rusanu 2016年

2
決して負の数がない場合unsignedは、コンパイラーが推論するのが簡単なため、型を使用することで多少の利益が得られる可能性があることに注意してください。
user694733

4
@RemusRusanu:Pascalでは、部分範囲タイプを定義できますvar value: 0..36;
Edgar Bonet、2016年

7
" int(charではない)は、CPUの効率性のためにのみ使用されます。狭いタイプは、レジスタ幅全体にゼロ拡張または符号拡張する必要がある場合があります。配列インデックスとして使用した場合、これは無料で発生する場合があります。このタイプの配列を使用している場合、通常、キャッシュフットプリントの削減は他のすべてを上回ります。
Peter Cordes

1
:回答を忘れてしまったintunsigned int符号拡張または64ビット・ポインタを持つほとんどのシステム上で、あまりにも、32から64ビットからゼロ拡張する必要があります。x86-64では、32ビットでの操作は無料で64ビットにゼロ拡張を登録します(符号拡張ではありませんが、符号付きオーバーフローは未定義の動作なので、コンパイラーは必要に応じて64ビット符号付き計算を使用できます)。したがって、計算結果ではなく、32ビット関数の引数をゼロ拡張する追加の命令のみが表示されます。あなたはより狭い符号なしタイプのためにでしょう。
Peter Cordes

回答:


230

はい、可能です。たとえば、次のようにして、不可能な条件についてコンパイラに伝えるgccことができます__builtin_unreachable

if (value < 0 || value > 36) __builtin_unreachable();

上記の条件をマクロでラップすることができます:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

そしてそれを次のように使用します:

assume(x >= 0 && x <= 10);

ご覧のようにgccこの情報に基づいて最適化を実行します。

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

生成する:

func(int):
    mov     eax, 17
    ret

ただし、欠点として、コードがそのような仮定を破った場合、動作が未定義になることがあります

これが発生した場合、デバッグビルドであっても通知しません。仮定を使用してバグをより簡単にデバッグ/テスト/キャッチするには、次のようなハイブリッドの仮定/アサートマクロ(@David Zへのクレジット)を使用できます。

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

デバッグビルド(NDEBUG 定義)では、通常のassert印刷エラーメッセージとabort'ingプログラムのように機能し、リリースビルドでは、仮定を利用して最適化されたコードを生成します。

ただし、それは定期的に代わるものではありませんことをassert- condリリースに残ってビルドので、あなたのような何かをするべきではありませんassume(VeryExpensiveComputation())


5
@Xofoはそれを取得しませんでした。私の例でreturn 2は、コンパイラによってコードからブランチが削除されたため、これはすでに発生しています。

6
ただし、gcc 、OPが期待するとおり、関数を魔法の演算やテーブル検索に最適化できないようです。
jingyu9575 2016年

19
@ user3528438 __builtin_expectは厳密ではないヒントです。__builtin_expect(e, c)は、「eに評価される可能性が最も高い」と読む必要があり、c分岐予測を最適化するのに役立ちeますがc、常にに限定されるわけではないため、オプティマイザが他のケースを破棄することはできません。ブランチがアセンブリでどのように構成されているかを確認します

6
理論的には、無条件に未定義の動作を引き起こすコードであれば、の代わりに使用できます__builtin_unreachable()
CodesInChaos 2016年

14
私はこの悪い考えになり、それについて知っていないいくつかの癖がありますがない限り、それはでこれを組み合わせること意味をなさないかもしれないassert例えば、定義した、assumeassertするときは、NDEBUGとして定義され、されていない__builtin_unreachable()ときはNDEBUG定義されています。そうすれば、本番用コードで想定のメリットを享受できますが、デバッグビルドでも明示的なチェックが行われます。もちろん、想定実際に満たされることを確認するのに十分なテストを行う必要があります。
David Z

61

これには標準サポートがあります。あなたがしなければならないことは、stdint.hcstdint)をインクルードしてからタイプを使用することですuint_fast8_t

これは、0〜255の数値のみを使用していることをコンパイラに通知しますが、より高速なコードが得られる場合は、より大きな型を使用してもかまいません。同様に、コンパイラーは変数が255を超える値を持つことはないと想定し、それに応じて最適化を行います。


2
これらのタイプは、本来あるべき程度に使用されていません(個人的には存在することを忘れがちです)。彼らは、高速で移植性があり、かなり素晴らしいコードを提供します。そして、彼らは1999
。– Lundin

これは一般的なケースに適した提案です。デニスの答えは、特定のシナリオに対してより柔軟なソリューションを示しています。
Orbitのライトネスレース

1
コンパイラーは、x86 / ARM / MIPS / PPC(godbolt.org/g/KNyc31)にあるようなuint_fast8_t実際に8ビットタイプ(例:)であるシステムでのみ、0〜255の範囲情報を取得します。上21164A前に早く12月アルファ、バイトのロード/ストアはサポートされていなかったので、まともな実装では、使用します。私の知る限り、ほとんどのコンパイラ(gccなど)で型に追加の範囲制限を設定するメカニズムはないため、その場合とまったく同じように動作するか、まったく同じように動作することは間違いありません。unsigned chartypedef uint32_t uint_fast8_tuint_fast8_tunsigned int
Peter Cordes 2016年

boolは特別で、範囲は0または1に制限されていますがchar、gcc / clangでは、ヘッダーファイルで定義されていない組み込み型です。前述のように、ほとんどのコンパイラーにはメカニズムがないと思いますそれはそれを可能にするでしょう。)
Peter Cordes

1
とにかく、uint_fast8_tこれはと同じくらい効率的なプラットフォームで8ビット型を使用するため、これは良い推奨事項unsigned intです。(実際には、fast型が何のために高速であると想定されているか、およびキャッシュフットプリントのトレードオフがその一部であると想定されているかどうかはわかりません。)x86は、メモリソースを使用してバイトを追加する場合でも、バイト操作を幅広くサポートしているため、個別にゼロ拡張ロードを行う必要もありません(これも非常に安価です)。gccはuint_fast16_t、x86で64ビットタイプを作成します。これは、ほとんどの用途(32ビットと比較して)には異常です。 godbolt.org/g/Rmq5bv
Peter Cordes

8

現在の答えは、範囲が確実にわかっている場合に適していますが、値が期待される範囲外の場合でも正しい動作が必要な場合は機能しません。

その場合、私はこのテクニックがうまくいくことを発見しました:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

アイデアは、コードとデータのトレードオフです。つまり、1ビットのデータ(かどうかx == c)を制御ロジックに移動します。
これxは、実際には既知の定数であるオプティマイザへのヒントであり、cそれをインライン化してfoo、残りの部分とは別に、最初の呼び出しを最適化することを奨励します。

fooただし、実際にはコードを1つのサブルーチンに分解してください。コードを複製しないでください。

例:

この手法が機能するためには、少し幸運である必要があります-コンパイラーが物事を静的に評価しないことを決定する場合があり、それらは一種の恣意的です。しかし、うまくいくとうまくいきます。

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

ただ、使用-O3して事前評価を行った定数に気づく0x200x30eアセンブラ出力


あなたがしたくないですif (x==c) foo(c) else foo(x)か?のconstexpr実装をキャッチする場合のみfoo
MSalters 2016年

@MSalters:誰かがそれを尋ねるつもりだと知っていた!! 以前constexprはこのテクニックを思いついたので、後で「更新」することはありませんでした(ただしconstexpr、その後も心配することはありませんでした)。コンパイラーがそれらを一般的なコードとして分解し、最適化せずに通常のメソッド呼び出しのままにすることにした場合は、ブランチを削除しやすくします。これを確認したことはcありませんが、コンパイラがc(申し訳ありませんが、冗談ですが)を同じコードにするのは本当に難しいと思いました。
user541686 16

4

より標準的なC ++のソリューションが必要な場合は、[[noreturn]]属性を使用して独自のを作成できると言っていますunreachable

だから私はデニスの優れた例を再利用してデモンストレーションします:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

これはあなたが見ることができるように、ほぼ同じコードでの結果:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

もちろん欠点は、[[noreturn]]関数が実際に戻るという警告が表示されることです。


それはで動作しclang私の元のソリューションがないとき、とても素敵なトリックと+1。しかし、全体は非常にコンパイラーに依存しているため(Peter Cordesが示したように、パフォーマンス低下iccする可能性があります)、それでもまだ普遍的に適用できるわけではありません。また、マイナーな注意:オプティマイザがunreachable定義を使用できるようにし、これを機能させるにはインライン化する必要があります
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.