2017-01-11 4 views
4

clang 3.9.1と最適化(-O2)でいくつかのコードをコンパイルすると、実行時に他のコンパイラ(clang 3.8とgcc 6.3)には見られなかった予期しない動作が発生しました。アセンブリへのC++のマッピング

私は、プログラムを単純化しようとしたときに特定の機能が動作の違いを引き起こしているように見える、意図しない未定義の動作(ubsanでコンパイルすると予期しない動作を取り除く)があると思っていました。

アセンブリをC++にマッピングして、どこが間違っているのかを調べて、なぜこれが起こっているのかを判断し、いくつかの部分があります。

Godbolt link

C++:

#include <atomic> 
#include <cstdint> 
#include <cstdlib> 
#include <thread> 
#include <cstdio> 

enum class FooState { A, B }; 

struct Foo { 
    std::atomic<std::int64_t> counter{0}; 
    std::atomic<std::int64_t> counter_a{0}; 
    std::atomic<std::int64_t> counter_b{0}; 
}; 

//__attribute__((noinline)) 
FooState to_state(const std::int64_t c) { 
    return c >= 0 ? FooState::A : FooState::B; 
} 

static const int NUM_MODIFIES = 100; 

int value_a = 0, value_b = 0; 
Foo foo; 
std::atomic<std::int64_t> total_sum{0}; 

void test_function() { 
    bool done = false; 
    while (!done) { 
    const std::int64_t count = 
     foo.counter.fetch_add(1, std::memory_order_seq_cst); 
    const FooState state = to_state(count); 

    int &val = FooState::A == state ? value_a : value_b; 
    if (val == NUM_MODIFIES) { 
     total_sum += val; 
     done = true; 
    } 

    std::atomic<std::int64_t> &c = 
     FooState::A == state ? foo.counter_a : foo.counter_b; 
    c.fetch_add(1, std::memory_order_seq_cst); 
    } 
} 

アセンブリ:

test_function():      # @test_function() 
     test rax, rax 
     setns al 
     lock 
     inc  qword ptr [rip + foo] 
     mov  ecx, value_a 
     mov  edx, value_b 
     cmovg rdx, rcx 
     cmp  dword ptr [rdx], 100 
     je  .LBB1_3 
     mov  ecx, foo+8 
     mov  edx, value_a 
.LBB1_2:        # =>This Inner Loop Header: Depth=1 
     test al, 1 
     mov  eax, foo+16 
     cmovne rax, rcx 
     lock 
     inc  qword ptr [rax] 
     test rax, rax 
     setns al 
     lock 
     inc  qword ptr [rip + foo] 
     mov  esi, value_b 
     cmovg rsi, rdx 
     cmp  dword ptr [rsi], 100 
     jne  .LBB1_2 
.LBB1_3: 
     lock 
     add  qword ptr [rip + total_sum], 100 
     test al, al 
     mov  eax, foo+8 
     mov  ecx, foo+16 
     cmovne rcx, rax 
     lock 
     inc  qword ptr [rcx] 
     ret 

noinlineとしてto_stateをマークするか、グローバルであることをdoneを変更すると、予期しない動作を "修正" ように見えることを私が発見しました。

私が見ている予期しない動作は、カウンタが0より大きい場合はcounter_aをインクリメントする必要があります。それ以外の場合はcounter_bをインクリメントする必要があります。私は時々、これは起こっていないが、いつ/ときに難しいのかを正確に判断することができますから。

私はいくつかの助けをすることができるアセンブリの一部はtest rax, rax; setns altest al, 1の部分です。最初のテストで確定的にalを設定しないと、その値がどのカウンタを増やすかを判断するのに使われますが、おそらく私は何かを誤解しているようです。

以下は、この問題を示す小さな例です。 clang 3.9と-O2でコンパイルすると永久にハングし、そうでなければ完了します。

#include <atomic> 
#include <cstdint> 
#include <cstdlib> 
#include <thread> 
#include <cstdio> 

enum class FooState { A, B }; 

struct Foo { 
    std::atomic<std::int64_t> counter{0}; 
    std::atomic<std::int64_t> counter_a{0}; 
    std::atomic<std::int64_t> counter_b{0}; 
}; 

//__attribute__((noinline)) 
FooState to_state(const std::int64_t c) { 
    return c >= 0 ? FooState::A : FooState::B; 
} 

//__attribute__((noinline)) 
FooState to_state2(const std::int64_t c) { 
    return c >= 0 ? FooState::A : FooState::B; 
} 

static const int NUM_MODIFIES = 100; 

int value_a = 0, value_b = 0; 
Foo foo; 
std::atomic<std::int64_t> total_sum{0}; 

void test_function() { 
    bool done = false; 
    while (!done) { 
    const std::int64_t count = 
     foo.counter.fetch_add(1, std::memory_order_seq_cst); 
    const FooState state = to_state(count); 

    int &val = FooState::A == state ? value_a : value_b; 
    if (val == NUM_MODIFIES) { 
     total_sum += val; 
     done = true; 
    } 

    std::atomic<std::int64_t> &c = 
     FooState::A == state ? foo.counter_a : foo.counter_b; 
    c.fetch_add(1, std::memory_order_seq_cst); 
    } 
} 

int main() { 
    std::thread thread = std::thread(test_function); 

    for (std::size_t i = 0; i <= NUM_MODIFIES; ++i) { 
    const std::int64_t count = 
     foo.counter.load(std::memory_order_seq_cst); 
    const FooState state = to_state2(count); 

    unsigned log_count = 0; 

    auto &inactive_val = FooState::A == state ? value_b : value_a; 
    inactive_val = i; 

    if (FooState::A == state) { 
     foo.counter_b.store(0, std::memory_order_seq_cst); 
     const auto accesses_to_wait_for = 
      foo.counter.exchange((std::numeric_limits<std::int64_t>::min)(), 
           std::memory_order_seq_cst); 
     while (accesses_to_wait_for != 
      foo.counter_a.load(std::memory_order_seq_cst)) { 
     std::this_thread::yield(); 

     if(++log_count <= 10) { 
      std::printf("#1 wait_for=%ld, val=%ld\n", accesses_to_wait_for, 
      foo.counter_a.load(std::memory_order_seq_cst)); 
     } 
     } 
    } else { 
     foo.counter_a.store(0, std::memory_order_seq_cst); 

     auto temp = foo.counter.exchange(0, std::memory_order_seq_cst); 
     std::int64_t accesses_to_wait_for = 0; 
     while (temp != INT64_MIN) { 
     ++accesses_to_wait_for; 
     --temp; 
     } 

     while (accesses_to_wait_for != 
      foo.counter_b.load(std::memory_order_seq_cst)) { 
     std::this_thread::yield(); 

     if (++log_count <= 10) { 
      std::printf("#2 wait_for=%ld, val=%ld\n", accesses_to_wait_for, 
      foo.counter_b.load(std::memory_order_seq_cst)); 
     } 
     } 
    } 

    std::printf("modify #%lu complete\n", i); 
    } 

    std::printf("modifies complete\n"); 

    thread.join(); 

    const std::size_t expected_result = NUM_MODIFIES; 
    std::printf("%s\n", total_sum == expected_result ? "ok" : "fail"); 
} 
+0

なぜコードをデバッグするためにアセンブリ言語を調べていますか? mvceを作成してデバッガを使用しますか? –

+4

あなたは*「予期しない動作」*と言い続けていますが、あなたが期待していない動作はまだ分かりません。明確にしていただけますか? – UnholySheep

+0

@UnholySheepごめんなさい。私はより多くの情報で投稿を更新しました。 – CTT

答えて

1

私は(、頭の中だけでシミュレーションを、それをデバッグしませんでした)100%わからないんだけど、私は両方の対test rax,rax + setns alが何か間違ったことをテストしていると思います。

最初の

結果は関数(UB)を呼び出す際にrax < 0かどうかに依存し、ループ内の他のテストは非常に、常に "NS"(= 1 rax => SF = 0 =>alに32Bアドレスをテストする)であろう固定al残りのループの== 1は常にcounter_aを選択します。

私はあなたの質問を読んで、あなたは同じ疑いを持っています(私は最初にコードを見ました)。

+0

はい、それは私が疑っていた部分でした。このコードが生成される可能性がある場所はどこにありますか? – CTT

+0

@CTTは本当にありません。 'state'自体であれば、逆の論理(enumはA = 0、B = 1、' al'は1/0に設定されていますが)です。私は 'to_state()'の戻り値を受け取った後、インラインで削除する前にそれが一部であるかもしれないと思いましたが、逆論理はこれをサポートしていません。だから、もっと複雑になるでしょう。私はまだそれを正しく解読したことを100%確信していません。また、私はUBのようなものはC++のソースでは見えませんが、私はC++の達人ではありません。参照によるエイリアシングが有効であるかどうかを確認するには、AFAIG(g =推測)であるかどうかを確認することです。 (だから私はあなたがコンパイラのバグを打つと思う.. 50%?) – Ped7g

+1

これはコンパイラのバグになった。修正は、ここにあります:https://reviews.llvm.org/rL291630 – CTT

関連する問題