マルチスレッド化プログラムは最適化モードのままですが、-O0で正常に実行されます


68

簡単なマルチスレッドプログラムを次のように記述しました。

static bool finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

それはでデバッグモードで正常に動作するVisual Studioの-O0GC Cとした後、結果をプリントアウト1秒。しかし、スタックモードになり、リリースモードまたはで何も印刷しません-O1 -O2 -O3


コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Samuel Liew

回答:


100

非アトミック、非守ら変数にアクセスする2つのスレッドが、あるUBこの懸念finished。あなたはこれを修正するためfinishedにタイプstd::atomic<bool>を作ることができます。

私の修正:

#include <iostream>
#include <future>
#include <atomic>

static std::atomic<bool> finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

出力:

result =1023045342
main thread id=140147660588864

コリルのライブデモ


誰かが「それはbool-おそらく1ビットだ」と思うかもしれません。これはどのように非アトミックにすることができますか?」(私が自分でマルチスレッド化を始めたときに行いました。)

しかし、ティアリングの欠如std::atomicがあなたに与える唯一のものではないことに注意してください。また、複数のスレッドからの読み取りと書き込みの同時アクセスが明確に定義されているため、変数の再読み取りで常に同じ値が表示されるとコンパイラが想定することはできません。

作るboolその他の問題の原因無防備、非アトミック缶:

  • コンパイラーは、変数をレジスターに最適化するか、CSEの複数のアクセスを1つに最適化して、ループからロードを引き上げることを決定する場合があります。
  • 変数は、CPUコア用にキャッシュされる場合があります。(実際には、CPUにはコヒーレントキャッシュがあります。これは実際の問題ではありませんが、C ++標準はatomic<bool>memory_order_relaxedストア/ロードで機能するが機能しない非コヒーレント共有メモリでの架空のC ++実装をカバーするのに十分緩いvolatileです。実際のC ++実装では実際に機能しますが、このための揮発性はUBになります。

これが起こらないようにするには、コンパイラーに明示的に行わないように指示する必要があります。


volatileこの問題との潜在的な関係についての進化する議論について、私は少し驚いています。したがって、私は私の2セントを費やしたいと思います。


4
私は1つを見てfunc()「それを最適化できる」と考えました。オプティマイザはスレッドをまったく気にしません。無限ループを検出し、喜んでそれを「while(True)」に変えます。godboltを見ると.org / z / Tl44iNこれを見ることができます。終了したらTrue戻ります。そうでない場合は、ラベルで無条件にそれ自体に戻ります(無限ループ).L5
Baldrickk


2
@val:volatileC ++ 11で乱用する理由は基本的にありません。なぜなら、atomic<T>andと同じasmを取得できるからですstd::memory_order_relaxed。実際のハードウェアでも機能します。キャッシュはコヒーレントなので、別のコアのストアがキャッシュにコミットすると、ロード命令は古い値を読み続けることができません。(MESI)
Peter Cordes

5
@PeterCordes Using volatileはまだUBです。間違いなく明確にUBが安全であるとは思わないでください。UBが間違っている可能性があり、それを試したときにうまくいく方法を考えることができないからです。それは人々を何度もやけどさせました。
David Schwartz

2
@Damonミューテックスにはリリース/取得のセマンティクスがあります。ミューテックスは前にロックされていた場合、コンパイラはその保護、離れて読み取りを最適化するために許可されていないfinishedstd::mutex作品(なしvolatileatomic)。実際、すべてのアトミックを「単純な」値+ミューテックススキームに置き換えることができます。それでも機能し、速度が低下します。atomic<T>内部ミューテックスの使用が許可されています。atomic_flagロックフリーのみが保証されています。
Erlkoenig

42

Scheffの答えは、コードを修正する方法を説明しています。この場合、実際に何が起こっているかについて少し情報を追加したいと思いました。

最適化レベル1()を使用して、コードをgodboltでコンパイルしました-O1。関数は次のようにコンパイルされます。

func():
  cmp BYTE PTR finished[rip], 0
  jne .L4
.L5:
  jmp .L5
.L4:
  mov eax, 0
  ret

それで、ここで何が起こっているのですか?まず、比較があります:cmp BYTE PTR finished[rip], 0-これfinishedはがfalseかどうかをチェックします。

false(別名true)でない場合は、最初の実行でループを終了する必要があります。これが達成さによってjne .L4どのjは umps N OT EのラベルにQUAL .L4の値は、ここでi0)後の使用および機能が戻るためのレジスタに格納されます。

ただし、それ偽の場合は、

.L5:
  jmp .L5

これは無条件のジャンプであり、.L5偶然にもジャンプコマンド自体にラベルを付けます。

つまり、スレッドは無限ビジーループに入ります。

なぜこれが起こったのですか?

オプティマイザに関する限り、スレッドはその対象外です。他のスレッドが同時に変数を読み書きしていないことを前提としています(データレースUBであるため)。アクセスを離れて最適化できないことを伝える必要があります。これがScheffの答えが出てくるところです。私は彼を繰り返すことに迷惑を掛けません。

オプティマイザは、finished変数が関数の実行中に変更される可能性があることを知らされていないため、finished関数自体によって変更されていないことがわかり、定数であると想定します。

最適化されたコードは、定数のブール値を使用して関数を入力した結果として生じる2つのコードパスを提供します。ループが無限に実行されるか、ループが実行されることはありません。

で、-O0(予想通り)コンパイラ離れループ本体との比較を最適化しません。

func():
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], 0
.L148:
  movzx eax, BYTE PTR finished[rip]
  test al, al
  jne .L147
  add QWORD PTR [rbp-8], 1
  jmp .L148
.L147:
  mov rax, QWORD PTR [rbp-8]
  pop rbp
  ret

したがって、最適化されていない関数が機能する場合、コードとデータ型が単純であるため、ここでの原子性の欠如は通常問題ではありません。おそらく、ここで遭遇する可能性のある最悪の事態は、本来あるべき値からi1つずれた値です。

データ構造を備えたより複雑なシステムは、データの破損や不適切な実行を引き起こす可能性がはるかに高くなります。


3
C ++ 11は、スレッドとスレッド対応メモリモデルを言語自体の一部にします。これは、コンパイラーが、atomicそれらの変数を書き込まないコード内の非変数への書き込みを発明できないことを意味します。たとえば、そのロード+ストア(アトミックRMWではない)が別のスレッドからの書き込みをステップ実行できるため、if (cond) foo=1;asmに変換できませんfoo = cond ? 1 : foo;。彼らはマルチスレッドプログラムを書くために有用になりたかったので、コンパイラはすでにそのようなものを避けたが、C ++ 11は、コンパイラは2つのスレッドが書いたコードを壊さないために持っていたこと、それは公式の作っa[1]およびa[2]
ピーター・コルド

2
しかし、はい、コンパイラがスレッドをまったく認識しない方法についての誇張以外は、あなたの答えは正しいです。データレースUBは、グローバルを含む非アトミック変数のロードを引き上げることを可能にするものであり、シングルスレッドコードに必要なその他の積極的な最適化です。 MCUプログラミング-電子回路のループ中にC ++ O2最適化が壊れます。SEはこの説明の私のバージョンです。
Peter Cordes

1
@PeterCordes:GCを使用するJavaの利点の1つは、古い使用と新しい使用の間にグローバルメモリバリアが介在しないと、オブジェクトのメモリがリサイクルされないことです。つまり、オブジェクトを検査するコアは常に、そのある値を参照します。参考文献が最初に公開された後のある時点で開催されました。グローバルメモリバリアは頻繁に使用されると非常に高価になる可能性がありますが、あまり使用しない場合でも、他の場所でのメモリバリアの必要性を大幅に減らすことができます。
スーパーキャット

1
はい、そういうことを言っていたのはわかっていましたが、100%という言葉はそれを意味するとは思いません。オプティマイザが「それらを完全に無視する」と言います。正しくない:最適化時にスレッドを本当に無視すると、単語の読み込みや単語/単語ストアのバイトの変更などが発生する可能性があることはよく知られています。これにより、実際には、1つのスレッドがcharまたはビットフィールドにアクセスして、隣接する構造体メンバーに書き込みます。詳細については、lwn.net / Articles / 478657を参照してください。また、C11 / C ++ 11のメモリモデルだけが、実際に望ましくないだけでなく、そのような最適化を違法にする方法を参照してください。
Peter Cordes

1
いいえ、それは良いです。ありがとう@PeterCordes。改善に感謝します。
Baldrickk

5

学習曲線を完全にするために; グローバル変数の使用は避けてください。あなたはそれを静的にすることで良い仕事をしたので、それは翻訳単位に対してローカルになります。

次に例を示します。

class ST {
public:
    int func()
    {
        size_t i = 0;
        while (!finished)
            ++i;
        return i;
    }
    void setFinished(bool val)
    {
        finished = val;
    }
private:
    std::atomic<bool> finished = false;
};

int main()
{
    ST st;
    auto result=std::async(std::launch::async, &ST::func, std::ref(st));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    st.setFinished(true);
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

上のライブwandbox


1
関数ブロック内finishedとして宣言することもできstaticます。それはまだ一度だけ初期化されます、そしてそれが定数に初期化されるなら、これはロックを必要としません。
Davislor

へのアクセスは、finishedより安価なstd::memory_order_relaxedロードとストアを使用することもできます。必須の注文wrtはありません。どちらかのスレッドの他の変数。staticただし、@ Davislorの提案が意味をなしているかどうかはわかりません。複数のスピンカウントスレッドがある場合、同じフラグですべてを停止する必要はありません。finishedただし、アトミックストアではなく、初期化のみにコンパイルされる方法で初期化を記述したいとします。(finished = false;デフォルトのイニシャライザC ++ 17構文で実行しているように。godbolt.org/ z / EjoKgq)。
Peter Cordes、

@PeterCordesオブジェクトにフラグを設定すると、異なるスレッドプールに対して複数のスレッドプールが存在するようになります。ただし、元のデザインにはすべてのスレッドに対して単一のフラグがありました。
Davislor
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.