GCCがstd :: vector :: sizeがこのループで変更されないと想定できないのはなぜですか?


14

if (i < input.size() - 1) print(0);このループで最適化されinput.size()、すべての反復で読み取られるわけではない同僚に私は主張しましたが、これはそうではありません!

void print(int x) {
    std::cout << x << std::endl;
}

void print_list(const std::vector<int>& input) {
    int i = 0;
    for (size_t i = 0; i < input.size(); i++) {
        print(input[i]);
        if (i < input.size() - 1) print(0);
    }
}

gccオプション付きのコンパイラエクスプローラによると、-O3 -fno-exceptions実際にはinput.size()各反復を読み取りlea、減算を実行するために使用しています。

        movq    0(%rbp), %rdx
        movq    8(%rbp), %rax
        subq    %rdx, %rax
        sarq    $2, %rax
        leaq    -1(%rax), %rcx
        cmpq    %rbx, %rcx
        ja      .L35
        addq    $1, %rbx

興味深いことに、Rustではこの最適化が行われます。反復ごとに減分さiれる変数に置き換えられたように見えj、テストi < input.size() - 1はのようなものに置き換えられj > 0ます。

fn print(x: i32) {
    println!("{}", x);
}

pub fn print_list(xs: &Vec<i32>) {
    for (i, x) in xs.iter().enumerate() {
        print(*x);
        if i < xs.len() - 1 {
            print(0);
        }
    }
}

ではコンパイラエクスプローラの関連アセンブリは、次のようになります。

        cmpq    %r12, %rbx
        jae     .LBB0_4

私はチェックしましたが、カウンターでr12あるxs.len() - 1と確信していrbxます 以前は、addfor rbxmovのループの外側にありますr12

どうしてこれなの?これは、GCCはインライン展開することができる場合のように思えるsize()し、operator[]それがなかったとして、それは知っていることができるはずsize()は変更されません。しかし、GCCのオプティマイザが変数に引き出す価値はないと判断したのでしょうか?あるいは、これを安全でないものにする可能性のある他のいくつかの副作用があるかもしれません-誰か知っていますか?


1
またprintln、おそらく複雑な方法であり、コンパイラーはprintlnベクトルを変更しないことを証明できない場合があります。
Mooing Duck

1
@MooingDuck:別のスレッドはdata-race UBです。コンパイラは、それが起こらないことを前提にできます。ここでの問題は、への非インライン関数呼び出しcout.operator<<()です。コンパイラーは、このブラックボックス関数がstd::vectorグローバルからの参照を取得しないことを知りません。
Peter Cordes

@PeterCordes:他のスレッドはスタンドアロンの説明ではなく、printlnまたはの複雑さoperator<<が重要であるとあなたは正しいです。
Mooing Duck

コンパイラーは、これらの外部メソッドのセマンティクスを認識していません。
user207421

回答:


10

への非インライン関数呼び出しcout.operator<<(int)は、オプティマイザーのブラックボックスです(ライブラリはC ++で記述されており、オプティマイザーが見るすべてはプロトタイプなので、コメントの説明を参照してください)。グローバル変数が指す可能性のあるメモリが変更されていると想定する必要があります。

(またはstd::endl呼び出し。ちなみに、単に印刷するのではなく、なぜその時点でcoutのフラッシュを強制するのですか?'\n'ですか?)

たとえば、知っているすべてのことstd::vector<int> &inputは、グローバル変数への参照であり、それらの関数呼び出しの1つがそのグローバル変数を変更します。(または、vector<int> *ptrどこかにグローバルがあるか、static vector<int>他のコンパイルユニットのへのポインターを返す関数があるか、または関数がこのベクターへの参照を渡さずにこのベクターへの参照を取得できる他の方法があります。

アドレスが取得されたことがないローカル変数がある場合、コンパイラー 、非インライン関数呼び出しではそれを変更できないと想定できます。グローバル変数がこのオブジェクトへのポインタを保持する方法がないからです。(これは、エスケープ分析と呼ばれます)。これが、コンパイラがsize_t i関数呼び出し全体でレジスタに保持できる理由です。(int iそれによって影にされsize_t i、それ以外では使用されないため、最適化することができます)。

ローカルでも同じことができます vector(つまり、base、end_size、end_capacityポインターの場合)。

ISO C99には、この問題の解決策があります。 int *restrict foo。多くのC ++コンパイルはint *__restrict foofooが指すメモリがそのポインタを介してのみアクセスされることを約束するサポートをコンパイルします。2つの配列を取る関数で最も一般的に役立ち、それらが重複しないコンパイラーを約束する必要があります。そのため、コードを生成せずに自動ベクトル化してチェックし、フォールバックループを実行できます。

OPのコメント:

Rustでは、変更不可能な参照は、参照先の値を他の誰も変更していないことをグローバルに保証します(C ++と同等restrict)。

これが、Rustがこの最適化を行えるのにC ++ではできない理由を説明しています。


C ++の最適化

明らかにあなたは使うべきです auto size = input.size();に、関数の先頭で一度して、コンパイラーがループ不変であることをコンパイラーに認識させる必要があります。C ++の実装ではこの問題は解決されないため、自分で解決する必要があります。

またconst int *data = input.data();std::vector<int>「コントロールブロック」からデータポインターのロードを引き上げる必要がある場合もあります。 残念ながら、最適化には非常に慣用的ではないソースの変更が必要になる場合があります。

Rustは、コンパイラ開発者がコンパイラで実際に何が可能かを学んだ後に設計された、はるかに新しい言語です。CPUがi32.count_ones、回転、ビットスキャンなどを介して実行できるクールな機能の一部を移植可能に公開するなど、他の方法でも実際に表示されますstd::bitset::count()


1
OPのコードは、ベクトルが値によって取得されるかどうかをテストします。そのため、GCCはその場合に最適化できたとしても、最適化しません。
クルミ

1
標準では、operator<<これらのオペランドタイプの動作を定義しています。そのため、Standard C ++ではブラックボックスではなく、コンパイラはドキュメントに書かれていることを実行すると想定できます。多分彼らは非標準の振る舞いを追加するライブラリ開発者をサポートしたいと思うかもしれません...
MM

2
オプティマイザーには、標準で義務付けられている動作を与えることができます。私の指摘は、この最適化は標準で許可されていますが、コンパイラーベンダーは、この最適化を説明し、見逃す方法で実装することを選択することです
MM

2
@MMそれはランダムなオブジェクトを言っていませんでした、私は実装定義のベクトルを言いました。実装が、演算子<<によって変更され、実装で定義された方法でこのベクトルへのアクセスを許可する実装で定義されたベクトルを持つことを禁止する標準は何もありません。 coutから派生したユーザー定義クラスのオブジェクトを、を使用streambufしてストリームに関連付けることができますcout.rdbuf。同様に、から派生したオブジェクトostreamをに関連付けることができますcout.tie
ロスリッジ

2
@PeterCordes-ローカルベクトルについてはそれほど自信がありません。メンバー関数が外れるとすぐに、thisポインターが暗黙的に渡されるため、ローカルは事実上脱出しました。これは、実際にはコンストラクタと同じくらい早く発生する可能性があります。この単純なループについて考えてみます。私はgccのメインループ(from L34:からjne L34)のみをチェックしましたが、ベクトルメンバーがエスケープされたように動作します(反復ごとにメモリからロードする)。
BeeOnRope
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.