2017-01-11 7 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 

Я обнаружил, что маркировка to_state в noinline или изменения done быть глобальным, кажется, "исправить" неожиданное поведение.

Неожиданное поведение, которое я вижу, заключается в том, что если счетчик => 0, то counter_a следует увеличивать, иначе counter_b следует увеличивать. Из того, что я могу сказать, иногда это не происходит, но точно определяет, когда/почему было сложно.

Одна часть сборки, с которой я мог бы воспользоваться, - это test rax, rax; setns al и test 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 испытывают что-то не так.

Результат первого зависит от того rax < 0 при вызове функции (UB), а другой тест внутри цикла всегда будет «NS» (тестирование 32b адрес в rax => SF = 0 =>al = 1), так что fixed al == 1 для остальных циклов всегда будет выбирать counter_a.

Теперь я прочитал ваш вопрос, и у вас такое же подозрение (сначала я посмотрел код).

+0

Да, это была та часть, о которой я тоже подозревал. Вы видите где-нибудь в коде, который может позволить это сгенерировать? – CTT

+0

@ CTT не очень. Если бы это было «состояние», я бы ожидал противоположную логику (перечисление было A = 0, B = 1, тогда как 'al' установлено на 1/0 вместо). Я подумал, что это может быть частью «после получения возвращаемого значения' to_state() ', прежде чем удалять его из-за встроенного», но обратная логика этого не поддерживает. Так что, наверное, это намного сложнее. Я все еще не уверен на 100%, я правильно ее расшифровал. Также я не вижу ничего похожего на UB-файлы в C++-источниках, но я не C++-гуру. Одна вещь, чтобы проверить, действительно ли сглаживание по ссылке, но AFAIG (g = guess), это так. (Итак, я думаю, вы попали в ошибку компилятора .. 50%?) – Ped7g

+1

Это оказалось ошибкой компилятора. Исправление: https://reviews.llvm.org/rL291630 – CTT