При компиляции некоторого кода с clang 3.9.1 и оптимизаций (-O2) во время выполнения я столкнулся с неожиданным поведением, которое я не видел с другими компиляторами (clang 3.8 и gcc 6.3).Сопоставление C++ с сборкой
Я думал, что у меня может быть непреднамеренное неопределенное поведение (компиляция с ubsan удаляет неожиданное поведение), поэтому я попытался упростить программу и обнаружил, что одна конкретная функция, по-видимому, вызывает различия в поведении.
Теперь я сопоставляю сборку с C++, чтобы узнать, где это происходит, чтобы попытаться определить, почему это происходит, и есть несколько частей, с которыми мне трудно сопоставить обратную связь.
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");
}
Почему вы ищете язык ассемблера для отладки кода? Создать mvce и использовать отладчик? –
Вы продолжаете говорить «неожиданное поведение» *, но я все еще не уверен, какое поведение вы не ожидаете? Не могли бы вы прояснить? – UnholySheep
@UnholySheep Извините. Я обновил сообщение с дополнительной информацией. – CTT